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.
Related
So I have an android app which opens and displays PDF's, I have the user select pdfs like this
fun openFile() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/pdf"))
putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Files.getContentUri("external"))
}
activity?.startActivityForResult(intent, REQUEST_CODE_PDF_FILE)
}
And then I retrieve the URI from the activity result display it and save the URI. However the next time the app is opened I want to be open that same file, right now when I try opening the saved URI I get the following:
ava.lang.SecurityException: Permission Denial: opening provider com.android.providers.media.MediaDocumentsProvider from ProcessRecord{c3dfcb2 32587:ca.thing.testapp/u0a237} (pid=32587, uid=10237) requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs
at android.os.Parcel.createExceptionOrNull(Parcel.java:2425)
at android.os.Parcel.createException(Parcel.java:2409)
at android.os.Parcel.readException(Parcel.java:2392)
at android.os.Parcel.readException(Parcel.java:2334)
at android.app.IActivityManager$Stub$Proxy.getContentProvider(IActivityManager.java:5850)
at android.app.ActivityThread.acquireProvider(ActivityThread.java:6973)
So clearly after closing and reopening the app I no longer have permission to use that selected file. So I imagine what I need to do is make a copy of that file into some cache dir that I do have permissions in so that I can display it when the app is reopened. How would I go about doing that?
You should take persistable uri permission in onActivityResult in order to use the uri later.
Making a copy is not needed.
I'm making an app where you are able to select images or documents from storage and they are then displayed in a list kinda like Google Drive so you can choose one and it opens and you can view it. This list is implemented using a RecyclerView, and I store the uri and file type in a json in SharedPreferences. I get the content using:
val getFile = registerForActivityResult(
ActivityResultContracts.GetContent()
) {
if (it != null) {
val fileLink = FileLink()
fileLink.setUri(it.toString())
fileLink.setType(contentResolver.getType(it)!!)
fileLink.setName(getFileName(it)!!)
list.add(fileLink)
presenter.saveChangesToList(list)
adapter.notifyDataSetChanged()
}
}
And then I just call:
getFile.launch("image/*|application/pdf")
This didn't let me pick stuff from the internal storage (the files appeared grayed out) and it didn't actually filter the selectable files. It worked perfectly with images taken with the camera though.
To fix the "not being able to pick stuff from storage" I changed GetContent() to OpenDocument():
val getFile = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) {
if (it != null) {
val fileLink = FileLink()
fileLink.setUri(it.toString())
fileLink.setType(contentResolver.getType(it)!!)
fileLink.setName(getFileName(it)!!)
list.add(fileLink)
presenter.saveChangesToList(list)
adapter.notifyDataSetChanged()
}
}
And then I call:
getFile.launch(
arrayOf(
"application/pdf",
"image/*"
)
)
With this, I am able to select files from storage and it also lets me restrict so you can only pick either images or pdfs. My problem now is that, if I get stuff like this, I can open the document once. If I close my app and open it again and try to open the document again, I get this error:
java.lang.SecurityException: UID 10156 does not have permission to content://com.android.providers.downloads.documents/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2FNewPdf
I open the documents using this code:
viewHolder.itemView.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(
Uri.parse(fileList[position].getUri().toString()),
fileList[position].getType()
)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(intent)
}
Could I get some help with this? I'm guessing it has to do with the permission to read uris, but I have no idea how to fix it. I also don't understand why I can open the files correctly the first time, but not after I restart the app.
You did not call takePersistableUriPermission() on a ContentResolver as soon as you were handed the Uri to the selected content.
As a result, your rights to access the content will expire — your rights to access the content will be limited to the one activity instance that received the Uri, not other activities or future app processes.
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.
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 have image stored in the internal storage of the app. I can grab the path and I can succesfully set it to image view. But when I am trying to let the user open it using the gallery (intent), it displays black screen.
myIntent.setDataAndType(Uri.fromFile(file), mimetype);
intent = Intent.createChooser(myIntent, "Choose a viewer");
startActivity(intent);
I am pretty sure it has to do with permission that gallery cant access private storage of my app for some reason. But is there way to do that "beside moving the file to external storage"
Thanks
Use FileProvider to serve the file from internal storage. Quoting the documentation:
FileProvider is a special subclass of ContentProvider that facilitates secure sharing of files associated with an app by creating a content:// Uri for a file instead of a file:/// Uri.
A content URI allows you to grant read and write access using temporary access permissions. When you create an Intent containing a content URI, in order to send the content URI to a client app, you can also call Intent.setFlags() to add permissions. These permissions are available to the client app for as long as the stack for a receiving Activity is active.
Here is an easy 'single file based solution'
When ever you add a file, let Media Store Content Provider knows about it using
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
Uri.fromFile(imageAdded)));
i have done some thing like this to show images from gallery.
private void pickFromGallery() {
Crop.pickImage(this);
}
<--->
crop is an android class.
this is a function in crop class.
public static void pickImage(Activity activity) {
Intent intent = (new Intent("android.intent.action.GET_CONTENT")).setType("image/*");
try {
activity.startActivityForResult(intent, 9162);
} catch (ActivityNotFoundException var3) {
Toast.makeText(activity, string.crop__pick_error, 0).show();
}
}