How to grant temporary access to custom content provider using FLAG_GRANT_READ_URI_PERMISSION

android, android-contentprovider, android-intent

Solution

It seems that `FLAG_GRANT_READ_URI_PERMISSION` affects only `Uri Intent.mData` but not uris in extras.

I found similar problems when playing with `ACTION_SEND`, which takes an uri in `EXTRA_STREAM`. Providing the same uri in `setData()` will work, but doesn't conform to the rules and leads to unexpected behavior (e.g. Gmail recipient).

As of Jelly Bean intents can contain ClipData, which should solve the problem. For `ACTION_SEND` it's generated automatically from extras.

`grantUriPermission` will work, but requires `revokeUriPermission`. To do the same job as `startActivity(Intent.createChooser(intent, title))` you will have to pick the target (`ACTION_PICK_ACTIVITY`), grant permissions to its package and revoke them when not needed anymore (onActivityResult?).

Here's some code:

Receiver app:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.handler" >

    <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" >
        …
        <activity
            android:name=".ActivityView"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>
                <action android:name="android.intent.action.SEND"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <data android:mimeType="*/*"/>
            </intent-filter>
        </activity>
    </application>
</manifest>

ActivityView.java:

protected void onCreate(Bundle savedInstanceState) {
    Intent intent = getIntent();
    if ((intent != null)) {
        procUri(intent.getData());
        procUri(intent.<Uri>getParcelableExtra(Intent.EXTRA_STREAM));
    }
}

private void procUri(Uri uri) {
    if (uri != null) {
        InputStream i;
        try {
            i = getContentResolver().openInputStream(uri);
            byte[] b = new byte[i.available()];
            i.read(b);
            …
        } catch (Throwable e) {
            …
        }
    }
}

Sender app:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.sender" >

    <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" >
        …
        <activity
            android:name=".ActivitySend"
            android:label="@string/app_name" >
        </activity>
        <provider
            android:name=".MyContentProvider"
            android:authorities="com.example.sender.content"
            android:enabled="true"
            android:exported="false"
            android:grantUriPermissions="true" >
        </provider>
    </application>
</manifest>

ActivitySend.java:

// OK!
private void doTestView(Uri uri, String type) {
    startActivity(
            new Intent(Intent.ACTION_VIEW)
                .setDataAndType(uri, type)
                .addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)
                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        );
}

// error prior to JellyBean
// ok on JellyBean, even without explicit FLAG_GRANT_READ_URI_PERMISSION (due to generated ClipData)
private void doTestSend(Uri uri, String type, CharSequence title) {
    startActivity(
            Intent.createChooser(
                new Intent(Intent.ACTION_SEND)
                    .setType(type)
                    .putExtra(Intent.EXTRA_STREAM, uri)
                    .putExtra(Intent.EXTRA_SUBJECT, title)
                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)
                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                ,
                title
            )
        );
}

// working but not ok, unexpected results!
private void doTestSend2(Uri uri, String type, CharSequence title) {
    startActivity(
            Intent.createChooser(
                new Intent(Intent.ACTION_SEND)
                    .setDataAndType(uri, type)
                    .putExtra(Intent.EXTRA_STREAM, uri)
                    .putExtra(Intent.EXTRA_SUBJECT, title)
                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)
                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                ,
                title
            )
        );
}

Problem

I am trying to query custom content provider(App A) from another app(App B). I can do that when there is no permission protection for content provider. Specifically, I build custom content provider on App A and sent an intent containing the URI to App B. Here is intent-sending part in App A. ``` class InsertOnClickListener implements OnClickListener{ public void onClick(View v) { ContentValues values = new ContentValues(); values.put(DataBaseConfiguation.TableConfiguation.USER_NAME, "Jack"); Uri uri = getContentResolver().insert(DataBaseConfiguation.TableConfiguation.CONTENT_URI, values); System.out.println("uri------------------->" + uri); // the uri above should be like "content://com.catking.contentprovider.MyContentProvider/user" Uri uri2 = Uri.parse("content://com.catking.contentprovider.MyContentProvider/user"); Cursor c = managedQuery(uri2, null, null, null, null); String sendvalue = null; if (c.moveToFirst()) { do{ System.out.println("User name:"+c.getString(c.getColumnIndex(DataBaseConfiguation.TableConfiguation.USER_NAME)).toString()); sendvalue = c.getString(c.getColumnIndex(DataBaseConfiguation.TableConfiguation.USER_NAME)).toString(); } while (c.moveToNext()); } Intent sendIntent = new Intent(); sendIntent.setClassName("com.android.web", "com.android.web.Provid"); sendIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); sendIntent.putExtra("name", uri2.toString()); sendIntent.setType("text/plain"); startActivity(sendIntent); } } ``` followed by manifest file of App A. ``` <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name=".ContentProviderTestActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <provider android:authorities="com.catking.contentprovider.MyContentProvider" android:exported="true" android:grantUriPermissions="true" android:name="com.catking.contentprovider.MyContentProvider" android:readPermission="android.permission.permRead" android:writePermission="android.permission.permWrite" > </provider> </application> ``` Then App B(class Provid) get the URI and query the corresponding data in content provider(using following code). ``` public class Provid extends Activity { public void onCreate(Bundle savedInstanceState) { Bundle extras = getIntent().getExtras(); String userNameuri; if (extras != null) { userNameuri = extras.getString("name"); Uri allTitles = Uri.parse(userNameuri); Cursor c = managedQuery(allTitles, null, null, null, null); if (c.moveToFirst()) { do{ System.out.println("Name is"+c.getString(c.getColumnIndex(DataBaseConfiguation.TableConfiguation.USER_NAME)).toString()); } while (c.moveToNext()); } } } ``` } Here's App B's manifest file. ``` <uses-permission android:name="android.permission.INTERNET" /> <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <activity android:name="._GetWebResoureActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.SEND" > </action> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="*/*" /> </intent-filter> </activity> <receiver android:name="StaticReceiver11" > <intent-filter> <action android:name="android.intent.action.MYSEND" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </receiver> <activity android:name="Provid" android:label="@string/title_activity_provid" > <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="*/*" /> </intent-filter> </activity> </application> ``` However, when I query content provider from App B, errors occur: ``` java.lang.RuntimeException: Unable to start activity ComponentInfo {com.android.web/com.android.web.Provid}: java.lang.SecurityException: Permission Denial: opening provider com.ck.contentprovider.MyContentProvider from ProcessRecord{426c6ea8 17032:com.android.web/u0a95} (pid=17032, uid=10095) requires android.permission.permRead or android.permission.permWrite ``` It seems that App B did not make use of the temporary permission to access. In other word, how to utilize FLAG_GRANT_READ_URI_PERMISSION from App B? I have also tried directly adding Uri to intent(using setData()), instead of Uri.toString()(using putExtra()). ``` sendIntent.setData(uri2); ``` and ``` Uri userNameuri = getIntent().getData(); ``` But the "userNameuri" got in App B is null. I am totally confused... updated I tried "grantUriPermission("com.android.getCPaccess", uri2, Intent.FLAG_GRANT_READ_URI_PERMISSION)" according to a previous post What is the correct permission handling when sending sensitive app data as email attachment? And it works indeed. It can work without using FLAG_GRANT_READ_URI_PERMISSION. But the permission is not "temporary". It have to be ended manually by revokeUriPermission(). So, I am wondering if there is a way to grant temporary permission as introduced in FLAG_GRANT_READ_URI_PERMISSION, or it's a bug at all?

Original source

Related problems