I received several crash report by my app with a java.lang.SecurityException. This occurs when the app try to obtain a persistent permission on a image URI user chosed from its images.
The method to choose images is through an intent:
public static Intent openGalleryToSelectImages(Activity a)
{
Intent intent = new Intent();
// Set action
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
intent.setAction(Intent.ACTION_OPEN_DOCUMENT);
}
else {
intent.setAction(Intent.ACTION_GET_CONTENT);
}
// Set MIME type and allow multiple selection
intent.setType("image/*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
a.startActivityForResult(Intent.createChooser(intent,"Select Picture"), Communication.REQUEST_SELECT_IMAGES_FROM_GALLERY);
return intent;
}
Then I execute some things on the received array of URIs.
The crash occurs when, for each URI, I try to get persistent read permission:
activity.grantUriPermission(activity.getPackageName(), uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
activity.getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
but I cannot figure out what's wrong with that.
In addiction to this, the app works fine on several smartphones, so it seems bound to a specific vendor.
EDIT:
I cannot perform deep analysis on devices caused crash.
On my development device the URI for the images is something like:
content://com.android.providers.media.documents/document/image%3A796
You can't grant permissions to yourself.
You ask another app, such as Gallery, to give you an Uri to a resource and if that app is well written, it will grant your app read access to that Uri upon return.
Unfortunately some Gallery apps (including that of a prominent Android phone manufacturer) don't grant you anything.
This is how we end up having to request READ_EXTERNAL_STORAGE permission anyway. It also means that you need to request the runtime permission on Android 6+ when appropriate.
I ended up catching the SecurityException and requesting runtime permission at that moment - when I absolutely needed it.
Lesson learned the hard way: When you get back positive result in onRequestPermissionsResult, your activity is in stopped state. You can't save state and will lose any changes to member variables if you leave immediately (like by calling startActivity in the callback). Put what you need into member variables now but defer startActivity to onResume.
Related
I have an app requesting for the access to a folder.
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.putExtra("android.provider.extra.SHOW_ADVANCED", true);
mStartChooseFolderForResult.launch(intent);
then in OnActivityResult:
public void onActivityResult(ActivityResult result)
{
if (result.getResultCode() == Activity.RESULT_OK)
{
Intent intent = result.getData();
Uri uri = intent.getData();
ContentResolver cr = getActivity().getContentResolver();
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
cr.takePersistableUriPermission(uri, flags);
...
}
}
When I run the app manually, the persistable permission is granted and it works fine even after a reboot.
But when I run the app in a expresso test after a reboot, I get the following error:
No persistable permission grants found for UID...
If I run the app manually and select the folder of the espresso test and then run the espresso test, then it works fine
Here is my test code (of course, I call Intents.init() before and Intenst.release() after):
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.setData(Uri.parse(DEFAULT_FOLDER_STRING + "%2F" + testFolder));
Instrumentation.ActivityResult result = new
Instrumentation.ActivityResult(Activity.RESULT_OK, intent);
intending(hasAction(Intent.ACTION_OPEN_DOCUMENT_TREE)).respondWith(result);
onView(withText("select")).perform(click());
How can I fix this error?
How can I fix this error?
My guess is that you will need to eliminate the test.
If all you were doing with the returned Intent was looking at it, your mock return Intent would be fine. However, you are actually trying to make a system call (takePersistableUriPermission()) using the details from that Intent. And, the error is pretty straightforward: the OS has no sign of a persistable permission grant for Uri.parse(DEFAULT_FOLDER_STRING + "%2F" + testFolder). Hence, when you call takePersistableUriPermission(), the OS is very confused.
If you wish to keep this test, you would need to rewrite your activity/fragment to not call takePersistableUriPermission() itself. Instead, that logic would need to be moved elsewhere (e.g., a viewmodel), where your test uses a test double (e.g., a mock or fake), so that your test does not actually call takePersistableUriPermission().
In my application a user can open the gallery and select their images by using androidX method:
registeredIntent = registerForActivityResult(new ActivityResultContracts.GetMultipleContents(), this);
And the callback for the result is sent in this method:
#Override
public void onActivityResult(List<Uri> result) {
if(result != null && result.size() > 0) {
sliderImageAdapter.addItems(result);
binding.includeResultTask.imageSlider.setSliderAdapter(sliderImageAdapter, true);
}
}
So far this works pretty well, the images are shown in my gallery component by adding the Uris in my adapter, the problem is that I save those in my database and when I restart the application the permissions are revoked and my application crash.
I tried by adding this in my callback for each item:
requireActivity().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
But I get this error:
Permission Denial: opening provider com.android.providers.media.MediaDocumentsProvider from ProcessRecord{41a7dce 26094:it.ilogreco.levelup/u0a482} (pid=26094, uid=10482) requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
I have even tried to use this for each item:
context.grantUriPermission(context.getPackageName(), uri, FLAG_GRANT_READ_URI_PERMISSION);
but I get this error:
UID 10482 does not have permission to content://com.android.providers.media.documents/document/image%3A108953 [user 0]; you could obtain access using ACTION_OPEN_DOCUMENT or related APIs
I load the Uris one time and save them to the database with the registerForActivityResult, I can't open the media gallery again with the API because it doesn't make sense, any idea how to get those permissions? I would like to avoid copying the content in the callback.
Switch from ActivityResultContracts.GetMultipleContents to ActivityResultContracts.OpenMultipleDocuments. Then, your takePersistableUriPermission() calls should work. You can only get persistable Uri permissions on documents obtained via the Storage Access Framework, which is what OpenMultipleDocuments uses.
Using a FilePicker I am able to have the user choose a file to upload, pass it to an IntentService, and upload it immediately via that intentService if device has network.
But if there is no network, I need to save the Uri and attempt upload later once the devices gets network. This re-attempt is failing. It throws "Permission denial" exception when I try to start the service during the re-attempt. Please let me know what might be wrong. Appreciate your help.
FILEPICKER
-----------
openFilePickDialog = new Intent();
openFilePickDialog.setType("*/*");
openFilePickDialog.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(openFilePickDialog, "Select File"), PickFileId);
During re-attempt of upload
----------------------------
Intent iUploadService = new Intent(context, UploadService.class);
String uriString = pendingUpload.uriString;
iUploadService.setData(Uri.parse(uriString));
iUploadService.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
iUploadService.putExtra(UploadService.ACTION, UploadService.ACTION_UPLOAD);
context.startService(iUploadService);
Stacktrace
-----------
java.lang.SecurityException: UID 10140 does not have permission to content://com.android.externalstorage.documents/document/primary%3AAndroid%2Fdata%2Fcom.aaaa.bbbb%2Ffiles%2FFolder%2FAttachments%2F1587129056397_IMG-20200414-WA0004.jpg [user 0]; you could obtain access using ACTION_OPEN_DOCUMENT or related APIs
at android.os.Parcel.createException(Parcel.java:2071)
at android.os.Parcel.readException(Parcel.java:2039)
at android.os.Parcel.readException(Parcel.java:1987)
at android.app.IActivityManager$Stub$Proxy.startService(IActivityManager.java:5166)
at android.app.ContextImpl.startServiceCommon(ContextImpl.java:1601)
at android.app.ContextImpl.startService(ContextImpl.java:1571)
at android.content.ContextWrapper.startService(ContextWrapper.java:669)
Dont use ACTION_GET_CONTENT as as you have seen the permission to read does not live long.
Instead use ACTION_OPEN_DOCUMENT and take persistable uri permission in onActivityResult.
I use the Intent mechanism to have the user select an image via the standard way
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*"
intent.addCategory(Intent.CATEGORY_OPENABLE)
ctx.startActivityForResult(intent, RequestCodes.SelectPhoto)
then I pass the Uri to another activity to maybe crop the photo. I need the Uri before to do some pre-checks.
On the Android emulators, the default providers such as Photos (apparently) give my whole app permission to open the Uri, not just the requesting activity. However, there is a "weird" provider in Asia, com.miui.gallery.provider.GalleryOpenProvider that doesn't -- an evil SecurityException happens in the cropper.
So I try to use ACTION_OPEN_DOCUMENT, which per the specs say that it will give my whole app permission until device reboot, but unfortunately that one doesn't support Google Photos in the cloud, in the emulator.
So I am looking for a way to determine if com.miui.gallery.provider.GalleryOpenProvider is going to be on the list for GET_CONTENT, and if so either prevent it, or otherwise fall back to using ACTION_OPEN_DOCUMENT. I'd like to avoid copying the stream before giving the Uri to the cropper, the crop activity treats it as readonly anyway.
This the full function to start the crop (kotlin). CropActivity is a modification of the old open-source Gallery app com.android.gallery3d.
private fun startCrop(ctx: Activity, uri: Uri) {
val intent = Intent(ctx, CropActivity::class.java)
intent.data = uri
val file = this.createImageFile(ctx, "photofinal")
if (file == null) {
this.showStorageUnavailable(ctx)
return
}
val outputUri = Uri.fromFile(file)
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
intent.putExtra(CropExtras.KEY_MIN_CROP_SIDE, Config.minimumImageDimension)
intent.putExtra(CropExtras.KEY_MOST_OBLONG_ASPECT, Config.maxPhotoAspectRatio)
intent.putExtra(CropExtras.KEY_EXIF_ORIENTATION, exifOrientation)
ctx.startActivityForResult(intent, RequestCodes.CropPhoto)
}
then I pass the Uri to another activity to maybe crop the photo
Pass that Uri in the "data" facet of the Intent, and add FLAG_GRANT_READ_URI_PERMISSION to transfer read access to the other component. See this sample app:
#Override
public void onActivityResult(int requestCode, int resultCode,
Intent resultData) {
if (resultCode==Activity.RESULT_OK) {
getActivity()
.startService(new Intent(getActivity(), DurablizerService.class)
.setData(resultData.getData())
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION));
}
}
Here, I happen to be passing the Uri to a service, but the same principle holds for an activity.
See also this blog post for more about Uri access lifetimes.
Or, don't use separate activities, but do something else (e.g., multiple fragments).
On the Android emulators, the default providers such as Photos (apparently) give my whole app permission to open the Uri, not just the requesting activity.
That would occur if the Uri has a file scheme or is from an exported permission-less ContentProvider.
So I try to use ACTION_OPEN_DOCUMENT, which per the specs say that it will give my whole app permission until device reboot
It is subject to the same general rules as the Uri values you get from ACTION_GET_CONTENT.
So I am looking for a way to determine if com.miui.gallery.provider.GalleryOpenProvider is going to be on the list for GET_CONTENT
That's not strictly possible. Any app could return a Uri from that provider. In practice, that provider may only be used by its hosting app. If you found the package name for that provider's app, and you used queryIntentActivities() on PackageManager with your ACTION_GET_CONTENT Intent, you could determine if an activity from that app is in the list of ACTION_GET_CONTENT implementations.
However, if you use FLAG_GRANT_READ_URI_PERMISSION, as I note earlier, that should not be necessary.
if so either prevent it
Other than by rolling your own "chooser"-style UI, that's not strictly possible.
I am checking user permissions allowance on Android API >=23 using this way.
#RequiresApi(api = Build.VERSION_CODES.M)
private void requestPermissions() throws RuntimeException {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, 1234); //TODO: MOVE TO CONST
}
Problem is that snippet mentioned above displays separated intent without the possibility to Explain why the app needs permissions (see attached image please). So I am using the modal window for displaying explanation why the app needs permissions and when the user clicked on the O.K button, intent with permission request is displayed.
This is not user-friendly at all and I would like to do the same thing (permission check + allowance) in the standard permission requesting way for Android API >= 23 like on:
https://developer.android.com/training/permissions/requesting.html
Is it possible please or is this permission unique with something and have to be processed only this way?
Many thanks for any advice.