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?