openInputStream cause SecurityException:Permission Denial (Android N+) - android

I need help with this problem since I'm new to Android.
My app support JellyBean (16) up to Oreo (26).
I have an UploadService that requires openInputStream() to upload data because the new behavior in Nougat.
This code works fine in Marshmallow and below, but always give me SecurityException crash on Nougat. And it crashes on the line where openInputStream() is called with error:
java.lang.SecurityException: Permission Denial: reading com.miui.gallery.provider.GalleryOpenProvider uri content://com.miui.gallery.open/raw/%2Fstorage%2Femulated%2F0%2FDCIM%2FCamera%2FIMG_20171008_182834.jpg from pid=30846, uid=10195 requires the provider be exported, or grantUriPermission()
The file uri could be from various app (gallery, camera, etc). I've narrowed down the problem to uri that comes from ACTION_GET_CONTENT intent (anything that comes from camera intent or MediaRecorder works fine).
I think it's because the uri lost its permission when passed into the service, but adding Intent.FLAG_GRANT_WRITE_URI_PERMISSION and Intent.FLAG_GRANT_READ_URI_PERMISSION doesn't help.
Also tried adding FLAG_GRANT_PERSISTABLE_URI_PERMISSION flag, but it still crashes and getContentResolver().takePersistableUriPermission() causes another SecurityException crash saying the said uri hasn't been granted persistable uri...
UploadService.java
//.......... code to prepare for upload
if ( contentResolver != null && schemeContentFile ) {
mMime = UtilMedia.getMime(this, uri);
try {
InputStream is = contentResolver.openInputStream(uri);
byte[] mBytes = getBytes(is);
Bundle fileDetail = UtilMedia.getFileDetailFromUri(this, uri);
Log.d("AndroidRuntime", TAG + " " + mMime + " " + UtilToString.bundleToString(fileDetail) + " imageFile " + mFile);
currTitle = fileDetail.getString(OpenableColumns.DISPLAY_NAME, "");
MediaType type = MediaType.parse(mMime);
requestFile = RequestBody.create(type, mBytes);
} catch ( IOException e ) {
e.printStackTrace();
} catch ( SecurityException e ) {
e.printStackTrace();
}
}
//............continue to upload
Thank You in advance.
EDIT (Additional Info)
In case this is important. The activity calling the service is calling finish() after it sends all the required data to the service, letting user to use the app, while the upload resumed in the background (with notification to tell user). And also, the upload works based on queue, and user can choose to upload multiple files in the activity. The first file always gets uploaded, but the files after always return with the crash.

I finally managed to fix this. Apparently it is because the permission for the given uri is only valid as long as the receiving activity is active. So, sending the uri to a background service (upload service) will result in SecurityException as expected, unless the uri is a persistable uri (ie. from ACTION_OPEN_DOCUMENT).
So my solution is to copy the file to a file my app created and use FileProvider.getUriForFile() function to get the uri and send it instead to the background service and delete the copy when my service finished uploading. This works fine even after the calling activity has finished.

The following code is working fine in my device (android 7):
public void pickImage(View view) {
try {
Intent photoPickerIntent = new Intent(Intent.ACTION_GET_CONTENT);
photoPickerIntent.setType("image/*");
photoPickerIntent.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
startActivityForResult(photoPickerIntent, RC_PICK_IMAGE);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
}
}
onActivityResult:
Uri uri = data.getData();
InputStream in = getContentResolver().openInputStream(uri);

Related

Is there any way of writing files to SDCard with flutter File.writeAsStringSync

I am trying to write to a file that is located in the SDCard, I found out that I need special permission for removable storage something that is not found in any known permission handler plugin for flutter (i tried simple_permission and permission_handler with no use).
I tried to acquire those permissions using the android side of things, so I wrote a simple function that would show the dialog and the user would allow the app to modify the content of the SDCard.
even after acquiring the rights to the SDCARD, I still get the same permissions denied error when trying to save files to the SDCard when using File.writeAsStringSync method.
I want to know if there is any known way/hack/workaround to save files in SDCards in flutter.
The android code i used is the same from this answer : https://stackoverflow.com/a/55024683/6641693
NOTE : I am targetting android 7 and beyond but not android 11.
I solved This, by ditching the dart file saving and using the android SAF.
First, what I did was try to get the sdCard modification permissions.
After that, I get to save the files I need.
here is the code I used to get the permissions ( aka the "allow this app to modify content on your sdCard" dialog )
public void takeCardUriPermission(String sdCardRootPath) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
File sdCard = new File(sdCardRootPath);
StorageManager storageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
StorageVolume storageVolume = storageManager.getStorageVolume(sdCard);
Intent intent = storageVolume.createAccessIntent(null);
try {
startActivityForResult(intent, 4010);
} catch (ActivityNotFoundException e) {
Log.e("TUNE-IN ANDROID", "takeCardUriPermission: "+e);
}
}
}
protected void onActivityResult(int requestCode, int resultCode, #Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 4010) {
Uri uri = data.getData();
grantUriPermission(getPackageName(), uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_READ_URI_PERMISSION);
final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_READ_URI_PERMISSION);
getContentResolver().takePersistableUriPermission(uri, takeFlags);
methodChannel.invokeMethod("resolveWithSDCardUri",getUri().toString());
}
}
public Uri getUri() {
List<UriPermission> persistedUriPermissions = getContentResolver().getPersistedUriPermissions();
if (persistedUriPermissions.size() > 0) {
UriPermission uriPermission = persistedUriPermissions.get(0);
return uriPermission.getUri();
}
return null;
}
So in order to start the whole permissions acquiring process, you have to first call takeCardUriPermission and passing the URI of the sdCard path.
Note: on my FlutterActivity, i am able to get the sdCardPath directly using getExternalCacheDirs()[1].toString()
After calling takeCardUriPermission and once the allow button is pressed (or the decline) an activity result event will be called and the onActivtyResult method will be called. the requestCode check is useful when you have multiple events and you need to filter this one out.
The activity result code will give the app permissions to modify the files on the sdCard.
The getUri method is the one that we will be using afterwards when trying to save bytes to a file, it returns the URI of the SDCard that we selected (you can have multiple sdCards).
Saving Files
What I used to save a file is a straightforward method. First we need to get the URI of the sdCard and create a Documentfile out of it, then we go through the hierarchy of that directory (DocumentFile can reference files and directories) to find the needed file based on it's URI.
We do this search by splitting the file URI into parts and then navigating the hierarchy by testing if each part exists or not. Once we test all the parts we would have reached our file, if it exists, or we were stuck at the last directory we got to.
the resulting of this iteration is a DocumentFile that we can execute operations on and with.
the following is the full file saving code :
String filepath = (String) arguments.get("filepath");
final byte[] bytes = methodCall.argument("bytes");
try{
if(filepath==null || bytes==null)throw new Exception("Arguments Not found");
DocumentFile documentFile = DocumentFile.fromTreeUri(getApplicationContext(), getUri());
String[] parts = filepath.split("/");
for (int i = 0; i < parts.length; i++) {
DocumentFile nextfile = documentFile.findFile(parts[i]);
if(nextfile!=null){
documentFile=nextfile;
}
}
if(documentFile!=null && documentFile.isFile()){
OutputStream out = getContentResolver().openOutputStream(documentFile.getUri());
out.write(bytes);
out.close();
}else{
throw new Exception("File Not Found");
}
}catch (Exception e){
result.error("400",e.getMessage(),e);
return;
}
result.success(true);
Note: in my code, I am calling this under the MethodChannel's MethodCallHandler which will give me the argument I need: filePath which is the String URI of the file I want to write to and the bytes byte array representing the data I want to save. The same can be said for the result.success
The file writing code is simple: open the file, write the data and close the file.

ActivityResultContracts.TakePicture()

In androidx.activity version 1.2.0-alpha05 API for TakePicture contract has been changed:
The TakePicture contract now returns a boolean indicating success rather than a thumbnail Bitmap as this was very rarely supported by camera apps when writing the image to the provided Uri
While in alpha04 callback received a Bitmap object, now only a Boolean object that describes success is received by the callback.
So now the Uri Parameter of the launch method of the launcher must not be null, but must be the destination where the picture is saved. Did not manage to create an Uri object that is accepted by the launcher and that can be used for my app to read the result picture.
Does anybody have an example for me for a valid Uri object that can be provided to the launcher?
I can't find any example on the internet
Here is an example.
File file = new File(getFilesDir(), "picFromCamera");
Uri uri = FileProvider.getUriForFile(this, getApplicationContext().getPackageName() + ".provider", file);
ActivityResultLauncher<Uri> mGetContent = registerForActivityResult(
new ActivityResultContracts.TakePicture(),
new ActivityResultCallback<Boolean>() {
#Override
public void onActivityResult(Boolean result) {
// do what you need with the uri here ...
}
});
mGetContent.launch(uri);
Note1: You are likely to run into FileUriExposedException , need to expose this uri for the camera app to access
Related: android.os.FileUriExposedException: file:///storage/emulated/0/test.txt exposed beyond app through Intent.getData()
Note2: If you have <uses-permission android:name="android.permission.CAMERA" /> declared in your manifest, you need to have permission before launching, otherwise java.lang.SecurityException: Permission Denial
thanks #babyishTank for good answer, after adding following changes works for me
val directory = File(context.filesDir, "camera_images")
if(!directory.exists()){
directory.mkdirs()
}
val file = File(directory,"${Calendar.getInstance().timeInMillis}.png")
Uri uri = FileProvider.getUriForFile(this, getApplicationContext().getPackageName() + ".provider", file);
ActivityResultLauncher<Uri> mGetContent = registerForActivityResult(
new ActivityResultContracts.TakePicture(),
new ActivityResultCallback<Boolean>() {
#Override
public void onActivityResult(Boolean result) {
// do what you need with the uri here ...
}
});
mGetContent.launch(uri);
and in filepaths.xml add following code
<files-path
name="images"
path="camera_images/"/>

How to open Context.MODE_PRIVATE files with external apps

I have a string (called comments) that contains some text that I want to display using an external app. I initially create the file like so:
String end = "rtf";
FileOutputStream outputStream;
try {
outputStream = openFileOutput("document." + end, Context.MODE_PRIVATE);
outputStream.write(comments.getBytes());
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
However I am unable to open the file with an external application when I try the following:
String type = "text/rtf";
Intent intent = new Intent (Intent.ACTION_VIEW);
File file = new File(getFilesDir() + "/document." + end);
Uri fileUri = Uri.fromFile(file);
intent.setDataAndType(fileUri,type);
startActivityForResult(intent, Intent.FLAG_GRANT_READ_URI_PERMISSION);
The message that I receive when I open try to the document with the external app is:
"open failed: EACCESS (Permission denied)."
Please advise. Thanks.
However I am unable to open the file with an external application when I try the following:
Correct. Intent.FLAG_GRANT_READ_URI_PERMISSION is for use with a ContentProvider, not for bare file:// Uri values, such as you are using. Use FileProvider to add such a ContentProvider to your app. See also the "Sharing Files" training module and this sample app.
Bear in mind that there's a good chance that your next problem will be an ActivityNotFoundException, as relatively few Android devices will have an app that will support the text/rtf MIME type.

Handling content:// urls shared from Android DownloadManager

So, I want my app to be able to upload files that the user shares with it. I got it to handle files shared from my various file explorers through file:// urls, then handling things shared from, say, the Gallery via content:// URLs. All good, except then I randomly decided to test sharing from the DownloadManager.
Here's the code:
#Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if(intent.getAction() == Intent.ACTION_SEND) {
Uri fileUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
if(fileUri != null) {
if(fileUri.getScheme().equals("content")) {
try {
Uri data = intent.getData();
InputStream input = getContentResolver().openInputStream(fileUri);
//Do something with InputStream
} catch(Exception e) {
//Log exception/show error
}
}
}
}
}
However, whenever I try to pass the Uri "data" into openInputStream I get a NullPointerException as getData() is returning null, even though other questions I've seen on this topic have said to use getData().
And when I try to pass "fileUri" into openInputStream, as I'm doing in the code I get the security exception:
06-20 12:41:24.310: E/AndroidRuntime(12375): java.lang.SecurityException: Permission Denial: reading com.android.providers.downloads.DownloadProvider uri content://downloads/all_downloads/433 from pid=12375, uid=10212 requires android.permission.ACCESS_ALL_DOWNLOADS, or grantUriPermission()
I've tried adding this permission but I'm aware that it isn't open for any app to use.
So, I have no idea how else to try and pull the file data out of the Intent, hopefully there's something obvious that I'm missing.

Public URI on Android where I can store a VCARD so that Android vcard import activity can see it? (NO SDCARD)

I need to import a vcard in android on a phone that has no SD Card, no external storage. I have access to the vcard file as a string/stream (whichever). I want android to handle the import by calling the following:
Intent i = new Intent();
i.setAction(android.content.Intent.ACTION_VIEW);
i.putExtra("account_name", accountName);
i.putExtra("account_type", accountType);
i.setDataAndType(Uri.parse("file://" + importFile.getAbsolutePath()), "text/x-vcard");
startActivity(i);
I am getting a permission error because I cannot find a place I can put the file that the default android activity can have read permission. I tried saving a file like this:
FileOutputStream openFileOutput = ctx.openFileOutput("contacts.vcf", Context.MODE_WORLD_WRITEABLE);
try {
openFileOutput.write(fileAsString.getBytes());
openFileOutput.flush();
} catch (Exception e) {
e.printStackTrace();
} finally{
try{openFileOutput.close();}catch (Exception e) {}
}
And then passing it the URI by using:
File fileStreamPath = ctx.getFileStreamPath("contacts.vcf");
Uri uri = Uri.parse("file://" + fileStreamPath.getAbsolutePath()), "text/x-vcard");
But i get the permission error.
I looked at the activity, and the only thing it accepts is a URI, it doesn't take a stream, or a string so I cannot send it the vcard file in either of these ways, it has to be a URI (which is complicating this problem greatly...).
So my question:
How can I get this vcard which I currently have as a string, into a public URI so that the android activity that imports vcards can have access to it?
Thanks very much for reading.

Categories

Resources