androidandroid-support-libraryandroid-contentproviderandroid-intent-chooser

Providing icon to system chooser via ChooserTargetService, FileProvider and grantUriPermission


I have some images stored in local app connected with certain contexts (like contacts). I'm using direct share (API 23+) via ChooserTargetService to show these to choose from and I want to ChooserTarget instances to have Icon filled with these images.

So I thought I can use android.support.v4.content.FileProvider for this (inside ChooserTargetService::onGetChooserTargets):

val file = File(File(filesDir, "images"), imageFileName)
val contentUri = FileProvider.getUriForFile(this, "com.company.fileprovider", file)
val icon = Icon.createWithContentUri(contentUri)

and in Manifest:

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.mycompany.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">

    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_provider_paths"/>
</provider>

but the problem is I get an exception

05-10 16:06:09.100 32444-32444/android:ui W/Icon: Unable to load image from URI: content://com.mycompany.fileprovider/images/icon_dice.png
java.lang.SecurityException: Permission Denial: reading android.support.v4.content.FileProvider uri content://com.mycompany.fileprovider/images/icon_dice.png from pid=32444, uid=1000 requires the provider be exported, or grantUriPermission()
    at android.os.Parcel.readException(Parcel.java:1684)
    at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:183)
    at android.database.DatabaseUtils.readExceptionWithFileNotFoundExceptionFromParcel(DatabaseUtils.java:146)
    at android.content.ContentProviderProxy.openTypedAssetFile(ContentProviderNative.java:692)
    at android.content.ContentResolver.openTypedAssetFileDescriptor(ContentResolver.java:1147)
    at android.content.ContentResolver.openAssetFileDescriptor(ContentResolver.java:984)
    at android.content.ContentResolver.openInputStream(ContentResolver.java:704)
    at android.graphics.drawable.Icon.loadDrawableInner(Icon.java:335)
    at android.graphics.drawable.Icon.loadDrawable(Icon.java:272)
    at com.android.internal.app.ChooserActivity$ChooserTargetInfo.<init>(ChooserActivity.java:645)
    at com.android.internal.app.ChooserActivity$ChooserListAdapter.addServiceResults(ChooserActivity.java:1003)
    at com.android.internal.app.ChooserActivity$1.handleMessage(ChooserActivity.java:126)
    at android.os.Handler.dispatchMessage(Handler.java:102)
    at android.os.Looper.loop(Looper.java:154)
    at android.app.ActivityThread.main(ActivityThread.java:6119)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

It's not possible to follow "exported" suggestion, becasue FileProvider unfortunatelly has it hardcoded not to allow it (from FileProvider.java android support library source code):

// Sanity check our security
if (info.exported) {
    throw new SecurityException("Provider must not be exported");
}
if (!info.grantUriPermissions) {
    throw new SecurityException("Provider must grant uri permissions");
}

so I tried to call

grantUriPermission("<something goes here>", contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)

but it's not obvious what should be put as package name first parameter. From the exception details you can deduct that code is in com.android.internal.app.ChooserActivity and is called by system.

Edit:

Using Icon.createWithFilePath is not possible, because you cannot access the file from different process:

W/Icon: Unable to load image from path: /data/user/0/com.mycompany.app/files/images/image.png
java.io.FileNotFoundException: /data/user/0/com.mycompany.app/files/images/image.png (Permission denied)

and if you try to set file to deprecated Context.MODE_WORLD_READABLE, you get:

java.lang.SecurityException: MODE_WORLD_READABLE no longer supported

on Andorid 7.


Solution

  • Why not create the Icon's image with the data from the file before you create the ChooserTarget. This is what Google's Messenger app does.

    File file = new File(new File(filesDir, "images"), imageFileName);
    Bitmap b = BitmapFactory.decodeStream(new FileInputStream(file));
    Icon icon = Icon.createWithBitmap(b);
    return new ChooserTarget(title, icon, score, cn, extras);
    

    You could even add some compression to the bitmap if you are so inclined.

    However, I do have to warn that you need to be wary of the amount and size of these that you're putting across the Binder...these are Parcelable objects and, as of 7.0, the binder can throw TransacationTooLargeException if you put too many or too large of Bitmaps into these ChooserTargets or any Parcelable sent across it.