Merge "Allow read-only sharing of Uri in voicemail content provider."
diff --git a/src/com/android/providers/contacts/VoicemailContentProvider.java b/src/com/android/providers/contacts/VoicemailContentProvider.java
index c55c11c..d6ddd92 100644
--- a/src/com/android/providers/contacts/VoicemailContentProvider.java
+++ b/src/com/android/providers/contacts/VoicemailContentProvider.java
@@ -26,6 +26,8 @@
 import android.content.ContentProvider;
 import android.content.ContentValues;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Binder;
@@ -91,7 +93,7 @@
     @Override
     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
             String sortOrder) {
-        UriData uriData = checkPermissionsAndCreateUriData(uri);
+        UriData uriData = checkPermissionsAndCreateUriDataForReadOperation(uri);
         SelectionBuilder selectionBuilder = new SelectionBuilder(selection);
         selectionBuilder.addClause(getPackageRestrictionClause());
         return getTableDelegate(uriData).query(uriData, projection, selectionBuilder.build(),
@@ -117,7 +119,12 @@
 
     @Override
     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
-        UriData uriData = checkPermissionsAndCreateUriData(uri);
+        UriData uriData = null;
+        if (mode.equals("r")) {
+            uriData = checkPermissionsAndCreateUriDataForReadOperation(uri);
+        } else {
+            uriData = checkPermissionsAndCreateUriData(uri);
+        }
         // openFileHelper() relies on "_data" column to be populated with the file path.
         return getTableDelegate(uriData).openFile(uriData, mode);
     }
@@ -261,6 +268,20 @@
      * Performs necessary voicemail permission checks common to all operations and returns
      * the structured representation, {@link UriData}, of the supplied uri.
      */
+    private UriData checkPermissionsAndCreateUriDataForReadOperation(Uri uri) {
+        // If the caller has been explicitly granted read permission to this URI then no need to
+        // check further.
+        if (context().checkCallingUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                == PackageManager.PERMISSION_GRANTED) {
+            return UriData.createUriData(uri);
+        }
+        return checkPermissionsAndCreateUriData(uri);
+    }
+
+    /**
+     * Performs necessary voicemail permission checks common to all operations and returns
+     * the structured representation, {@link UriData}, of the supplied uri.
+     */
     private UriData checkPermissionsAndCreateUriData(Uri uri) {
         mVoicemailPermissions.checkCallerHasOwnVoicemailAccess();
         UriData uriData = UriData.createUriData(uri);
diff --git a/tests/src/com/android/providers/contacts/ContactsActor.java b/tests/src/com/android/providers/contacts/ContactsActor.java
index 865c956..29d8eed 100644
--- a/tests/src/com/android/providers/contacts/ContactsActor.java
+++ b/tests/src/com/android/providers/contacts/ContactsActor.java
@@ -56,8 +56,6 @@
 import android.test.RenamingDelegatingContext;
 import android.test.mock.MockContentResolver;
 import android.test.mock.MockContext;
-import android.test.mock.MockResources;
-import android.util.TypedValue;
 
 import java.io.File;
 import java.io.IOException;
@@ -88,6 +86,7 @@
     private Account[] mAccounts = new Account[0];
 
     private Set<String> mGrantedPermissions = Sets.newHashSet();
+    private final Set<Uri> mGrantedUriPermissions = Sets.newHashSet();
 
     private CountryDetector mMockCountryDetector = new CountryDetector(null){
         @Override
@@ -145,7 +144,7 @@
             Class<? extends ContentProvider> providerClass, String authority) throws Exception {
         resolver = new MockContentResolver();
         context = new RestrictionMockContext(overallContext, packageName, resolver,
-                mGrantedPermissions);
+                mGrantedPermissions, mGrantedUriPermissions);
         this.packageName = packageName;
 
         RenamingDelegatingContext targetContextWrapper = new RenamingDelegatingContext(context,
@@ -196,6 +195,14 @@
         mGrantedPermissions.removeAll(Arrays.asList(permissions));
     }
 
+    public void addUriPermissions(Uri... uris) {
+        mGrantedUriPermissions.addAll(Arrays.asList(uris));
+    }
+
+    public void removeUriPermissions(Uri... uris) {
+        mGrantedUriPermissions.removeAll(Arrays.asList(uris));
+    }
+
     /**
      * Mock {@link Context} that reports specific well-known values for testing
      * data protection. The creator can override the owner package name, and
@@ -213,16 +220,19 @@
         private final ContentResolver mResolver;
         private final Resources mRes;
         private final Set<String> mGrantedPermissions;
+        private final Set<Uri> mGrantedUriPermissions;
 
         /**
          * Create a {@link Context} under the given package name.
          */
         public RestrictionMockContext(Context overallContext, String reportedPackageName,
-                ContentResolver resolver, Set<String> grantedPermissions) {
+                ContentResolver resolver, Set<String> grantedPermissions,
+                Set<Uri> grantedUriPermissions) {
             mOverallContext = overallContext;
             mReportedPackageName = reportedPackageName;
             mResolver = resolver;
             mGrantedPermissions = grantedPermissions;
+            mGrantedUriPermissions = grantedUriPermissions;
 
             mPackageManager = new ContactsMockPackageManager();
             mPackageManager.addPackage(1000, PACKAGE_GREY);
@@ -281,6 +291,20 @@
         }
 
         @Override
+        public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) {
+            return checkCallingUriPermission(uri, modeFlags);
+        }
+
+        @Override
+        public int checkCallingUriPermission(Uri uri, int modeFlags) {
+            if (mGrantedUriPermissions.contains(uri)) {
+                return PackageManager.PERMISSION_GRANTED;
+            } else {
+                return PackageManager.PERMISSION_DENIED;
+            }
+        }
+
+        @Override
         public int checkCallingOrSelfPermission(String permission) {
             return checkCallingPermission(permission);
         }
diff --git a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
index 119d8b3..702b55b 100644
--- a/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
+++ b/tests/src/com/android/providers/contacts/VoicemailProviderTest.java
@@ -16,10 +16,13 @@
 
 package com.android.providers.contacts;
 
+import com.android.common.io.MoreCloseables;
+
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.ParcelFileDescriptor;
 import android.provider.CallLog.Calls;
 import android.provider.VoicemailContract;
 import android.provider.VoicemailContract.Status;
@@ -27,6 +30,7 @@
 import android.test.MoreAsserts;
 
 import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Arrays;
@@ -225,6 +229,107 @@
             .build();
     }
 
+    public void testUriPermissions() {
+        setUpForFullPermission();
+        final Uri uri1 = insertVoicemail();
+        final Uri uri2 = insertVoicemail();
+        // Give away all permissions before querying. Access to both uris should be denied.
+        setUpForNoPermission();
+        checkHasNoAccessToUri(uri1);
+        checkHasNoAccessToUri(uri2);
+
+        // Just grant permission to uri1. uri1 should pass but uri2 should still fail.
+        mActor.addUriPermissions(uri1);
+        checkHasReadOnlyAccessToUri(uri1);
+        checkHasNoAccessToUri(uri2);
+
+        // Cleanup.
+        mActor.removeUriPermissions(uri1);
+    }
+
+    private void checkHasNoAccessToUri(final Uri uri) {
+        checkHasNoReadAccessToUri(uri);
+        checkHasNoWriteAccessToUri(uri);
+    }
+
+    private void checkHasReadOnlyAccessToUri(final Uri uri) {
+        checkHasReadAccessToUri(uri);
+        checkHasNoWriteAccessToUri(uri);
+    }
+
+    private void checkHasReadAccessToUri(final Uri uri) {
+        Cursor cursor = null;
+        try {
+            cursor = mResolver.query(uri, null, null ,null, null);
+            assertEquals(1, cursor.getCount());
+            try {
+                ParcelFileDescriptor fd = mResolver.openFileDescriptor(uri, "r");
+                assertNotNull(fd);
+                fd.close();
+            } catch (FileNotFoundException e) {
+                fail(e.getMessage());
+            } catch (IOException e) {
+                fail(e.getMessage());
+            }
+        } finally {
+            MoreCloseables.closeQuietly(cursor);
+        }
+    }
+
+    private void checkHasNoReadAccessToUri(final Uri uri) {
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                mResolver.query(uri, null, null ,null, null);
+            }
+        });
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    mResolver.openFileDescriptor(uri, "r");
+                } catch (FileNotFoundException e) {
+                    fail(e.getMessage());
+                }
+            }
+        });
+    }
+
+    private void checkHasNoWriteAccessToUri(final Uri uri) {
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                mResolver.update(uri, getTestVoicemailValues(), null, null);
+            }
+        });
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                mResolver.delete(uri, null, null);
+            }
+        });
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    mResolver.openFileDescriptor(uri, "w");
+                } catch (FileNotFoundException e) {
+                    fail(e.getMessage());
+                }
+            }
+        });
+        EvenMoreAsserts.assertThrows(SecurityException.class, new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    mResolver.openFileDescriptor(uri, "rw");
+                } catch (FileNotFoundException e) {
+                    fail(e.getMessage());
+                }
+            }
+        });
+    }
+
     // Test to ensure that all operations fail when no voicemail permission is granted.
     public void testNoPermissions() {
         setUpForNoPermission();