How to grant temporary access to custom content provider using FLAG_GRANT_READ_URI_PERMISSION

24,171

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
            )
        );
}
Share:
24,171
Jin
Author by

Jin

Updated on July 09, 2022

Comments

  • Jin
    Jin almost 2 years

    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?

    • Jens
      Jens over 11 years
      Is there a <uses-permission> for android.permission.permRead in your app that implements the provider & picker activity?
    • Jin
      Jin over 11 years
      Hi, Jens. I have also tried that. When both of apps have <uses-permission android:name="android.permission.permRead" /> in manifest file, it works fine. But this is the kind of permanent permission. I want to work out temporary access permission with FLAG_GRANT_READ_URI_PERMISSION(as described by google android developer). So the App B won't have android.permission.permRead, instead, it receive the intent with FLAG_GRANT_READ_URI_PERMISSION flag "true" from App A. But this fails.
    • Jens
      Jens over 11 years
      You should only need one uses-permission, declared in the app that declares the picker activity that calls #addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION). You should probably include more of your manifest & code in the question.
    • Jin
      Jin over 11 years
      Sure. I edited the question. Yes App A doesn't need uses-permission. But App B shouldn't have <uses-permission android:name="android.permission.permRead" />either, because if it has, there's no need to addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION).
    • Jens
      Jens over 11 years
      No - application A, which defines the provider, should also define the permission(s) you use for read/write. It must also declare a uses-permission for both the read (& write, if you in the future wish to grant write permission also) - you can't grant a permission you yourself do not hold.
    • Jin
      Jin over 11 years
      It still cannot work. The weird thing is: if I use putExtra("name", uri2.toString()) to transfer the URI, it may fail because the system may not bundle temporary acecss grant to a string. So I use setData(uri2), but when I use getIntent().getData() on the other side, it shows uri is null. Other part of intent can all be successfully received.