summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Felipe Leme <felipeal@google.com> 2018-01-24 09:15:07 -0800
committer Felipe Leme <felipeal@google.com> 2018-01-25 11:02:06 -0800
commit2378729eba560218671d4e56ed248d957299a91e (patch)
treef22011d7c6f6dc7230fc5cf8e9b2b933602cf6b0
parentb054a3ee354492784df68d6f99b93a53101e1f15 (diff)
Initial integration of ScopedAccessDirectory and AM.
Now it calls AM to get granted permissions (although it does not call it to update permissions yet). Test: manual verification Test: atest CtsAppSecurityHostTestCases:ScopedDirectoryAccessTest#testResetDoNotAskAgain Bug: 63720392 Change-Id: I8b212163bef4b608dbd589e3303fbb14bfa66719
-rw-r--r--src/com/android/documentsui/ScopedAccessActivity.java6
-rw-r--r--src/com/android/documentsui/ScopedAccessProvider.java269
-rw-r--r--src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java7
3 files changed, 234 insertions, 48 deletions
diff --git a/src/com/android/documentsui/ScopedAccessActivity.java b/src/com/android/documentsui/ScopedAccessActivity.java
index 991d26421..cf37225c5 100644
--- a/src/com/android/documentsui/ScopedAccessActivity.java
+++ b/src/com/android/documentsui/ScopedAccessActivity.java
@@ -47,6 +47,7 @@ import android.app.Dialog;
import android.app.DialogFragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
+import android.app.GrantedUriPermission;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.DialogInterface;
@@ -353,8 +354,9 @@ public class ScopedAccessActivity extends Activity {
+ " or its root (" + rootUri + ")");
final ActivityManager am =
(ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE);
- for (UriPermission uriPermission : am.getGrantedUriPermissions(packageName).getList()) {
- final Uri uri = uriPermission.getUri();
+ for (GrantedUriPermission uriPermission : am.getGrantedUriPermissions(packageName)
+ .getList()) {
+ final Uri uri = uriPermission.uri;
if (uri == null) {
Log.w(TAG, "null URI for " + uriPermission);
continue;
diff --git a/src/com/android/documentsui/ScopedAccessProvider.java b/src/com/android/documentsui/ScopedAccessProvider.java
index 0c2db8179..922c2fae5 100644
--- a/src/com/android/documentsui/ScopedAccessProvider.java
+++ b/src/com/android/documentsui/ScopedAccessProvider.java
@@ -27,26 +27,33 @@ import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABL
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_VOLUME_UUID;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
-import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName;
import static com.android.documentsui.base.SharedMinimal.getExternalDirectoryName;
+import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN;
+import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_GRANTED;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getAllPackages;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getAllPermissions;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.setScopedAccessPermissionStatus;
-
+import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.statusAsString;
import static com.android.internal.util.Preconditions.checkArgument;
import android.app.ActivityManager;
+import android.app.GrantedUriPermission;
import android.content.ContentProvider;
import android.content.ContentValues;
+import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.util.ArraySet;
import android.util.Log;
+import com.android.documentsui.base.Providers;
import com.android.documentsui.prefs.ScopedAccessLocalPreferences.Permission;
import com.android.internal.util.ArrayUtils;
@@ -54,9 +61,11 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
-import java.util.stream.Collectors;
//TODO(b/72055774): update javadoc once implementation is finished
/**
@@ -120,55 +129,122 @@ public class ScopedAccessProvider extends ContentProvider {
case URI_PACKAGES:
return getPackagesCursor();
case URI_PERMISSIONS:
- return getPermissionsCursor(selectionArgs);
+ if (ArrayUtils.isEmpty(selectionArgs)) {
+ throw new UnsupportedOperationException("selections cannot be empty");
+ }
+ // For simplicity, we only support one package (which is what Settings is passing).
+ if (selectionArgs.length > 1) {
+ Log.w(TAG, "Using just first entry of " + Arrays.toString(selectionArgs));
+ }
+ return getPermissionsCursor(selectionArgs[0]);
default:
throw new UnsupportedOperationException("Unsupported Uri " + uri);
}
}
private Cursor getPackagesCursor() {
- // First get the packages that were denied
- final Set<String> pkgs = getAllPackages(getContext());
+ final Context context = getContext();
+
+ // First, get the packages that were denied
+ final Set<String> pkgs = getAllPackages(context);
+
+ // Second, query AM to get all packages that have a permission.
+ final ActivityManager am =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+ final List<GrantedUriPermission> amPkgs = am.getGrantedUriPermissions(null).getList();
+ if (!amPkgs.isEmpty()) {
+ amPkgs.forEach((perm) -> pkgs.add(perm.packageName));
+ }
if (ArrayUtils.isEmpty(pkgs)) {
- if (DEBUG) Log.v(TAG, "getPackagesCursor(): ignoring " + pkgs);
+ if (DEBUG) Log.v(TAG, "getPackagesCursor(): nothing to do" );
return null;
}
- // TODO(b/63720392): also need to query AM for granted permissions
+ if (DEBUG) {
+ Log.v(TAG, "getPackagesCursor(): denied=" + pkgs + ", granted=" + amPkgs);
+ }
- // Then create the cursor
+ // Finally, create the cursor
final MatrixCursor cursor = new MatrixCursor(TABLE_PACKAGES_COLUMNS, pkgs.size());
pkgs.forEach((pkg) -> cursor.addRow( new Object[] { pkg }));
return cursor;
}
- private Cursor getPermissionsCursor(String[] packageNames) {
- // First get the packages that were denied
- final List<Permission> rawPermissions = getAllPermissions(getContext());
+ // TODO(b/63720392): need to unit tests to handle scenarios where the root permission of
+ // a secondary volume mismatches a child permission (for example, child is allowed by root
+ // is denied).
+ private Cursor getPermissionsCursor(String packageName) {
+ final Context context = getContext();
- if (ArrayUtils.isEmpty(rawPermissions)) {
- if (DEBUG) Log.v(TAG, "getPermissionsCursor(): ignoring " + rawPermissions);
- return null;
+ // List of volumes that were granted by AM at the root level - in that case,
+ // we can ignored individual grants from AM or denials from our preferences
+ final Set<String> grantedVolumes = new ArraySet<>();
+
+ // List of directories (mapped by volume uuid) that were granted by AM so they can be
+ // ignored if also found on our preferences
+ final Map<String, Set<String>> grantedDirsByUuid = new HashMap<>();
+
+ // Cursor rows
+ final List<Object[]> permissions = new ArrayList<>();
+
+ // First, query AM to get all packages that have a permission.
+ final ActivityManager am =
+ (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+ final List<GrantedUriPermission> uriPermissions =
+ am.getGrantedUriPermissions(packageName).getList();
+ if (DEBUG) {
+ Log.v(TAG, "am returned =" + uriPermissions);
}
+ setGrantedPermissions(packageName, uriPermissions, permissions, grantedVolumes,
+ grantedDirsByUuid);
- // TODO(b/72055774): unit tests for filters (permissions and/or package name);
- final List<Object[]> permissions = rawPermissions.stream()
- .filter(permission -> ArrayUtils.contains(packageNames, permission.pkg)
- && permission.status == PERMISSION_NEVER_ASK)
- .map(permission -> new Object[] {
- permission.pkg,
- permission.uuid,
- getExternalDirectoryName(permission.directory),
- Integer.valueOf(0)
- })
- .collect(Collectors.toList());
+ // Now gets the packages that were denied
+ final List<Permission> rawPermissions = getAllPermissions(context);
- // TODO(b/63720392): need to add logic to handle scenarios where the root permission of
- // a secondary volume mismatches a child permission (for example, child is allowed by root
- // is denied).
+ if (DEBUG) {
+ Log.v(TAG, "rawPermissions: " + rawPermissions);
+ }
- // TODO(b/63720392): also need to query AM for granted permissions
+ // Merge the permissions granted by AM with the denied permissions saved on our preferences.
+ for (Permission rawPermission : rawPermissions) {
+ if (!packageName.equals(rawPermission.pkg)) {
+ if (DEBUG) {
+ Log.v(TAG,
+ "ignoring " + rawPermission + " because package is not " + packageName);
+ }
+ continue;
+ }
+ if (rawPermission.status != PERMISSION_NEVER_ASK
+ && rawPermission.status != PERMISSION_ASK_AGAIN) {
+ // We only care for status where the user denied a request.
+ if (DEBUG) {
+ Log.v(TAG, "ignoring " + rawPermission + " because of its status");
+ }
+ continue;
+ }
+ if (grantedVolumes.contains(rawPermission.uuid)) {
+ if (DEBUG) {
+ Log.v(TAG, "ignoring " + rawPermission + " because whole volume is granted");
+ }
+ continue;
+ }
+ final Set<String> grantedDirs = grantedDirsByUuid.get(rawPermission.uuid);
+ if (grantedDirs != null
+ && grantedDirs.contains(rawPermission.directory)) {
+ Log.w(TAG, "ignoring " + rawPermission + " because it was granted already");
+ continue;
+ }
+ permissions.add(new Object[] {
+ packageName, rawPermission.uuid,
+ getExternalDirectoryName(rawPermission.directory), 0
+ });
+ }
+
+ if (DEBUG) {
+ Log.v(TAG, "total permissions: " + permissions.size());
+ }
// Then create the cursor
final MatrixCursor cursor = new MatrixCursor(TABLE_PERMISSIONS_COLUMNS, permissions.size());
@@ -176,6 +252,108 @@ public class ScopedAccessProvider extends ContentProvider {
return cursor;
}
+ /**
+ * Converts the permissions returned by AM and add it to 3 buckets ({@code permissions},
+ * {@code grantedVolumes}, and {@code grantedDirsByUuid}).
+ *
+ * @param packageName name of package that the permissions were granted to.
+ * @param uriPermissions permissions returend by AM
+ * @param permissions list of permissions that can be converted to a {@link #TABLE_PERMISSIONS}
+ * row.
+ * @param grantedVolumes volume uuids that were granted full access.
+ * @param grantedDirsByUuid directories that were granted individual acces (key is volume uuid,
+ * value is list of directories).
+ */
+ private void setGrantedPermissions(String packageName, List<GrantedUriPermission> uriPermissions,
+ List<Object[]> permissions, Set<String> grantedVolumes,
+ Map<String, Set<String>> grantedDirsByUuid) {
+ final List<Permission> grantedPermissions = parseGrantedPermissions(uriPermissions);
+
+ for (Permission p : grantedPermissions) {
+ // First check if it's for the full volume
+ if (p.directory == null) {
+ if (p.uuid == null) {
+ // Should never happen - the Scoped Directory Access API does not allow it.
+ Log.w(TAG, "ignoring entry whose uuid and directory is null");
+ continue;
+ }
+ grantedVolumes.add(p.uuid);
+ } else {
+ if (!ArrayUtils.contains(Environment.STANDARD_DIRECTORIES, p.directory)) {
+ if (DEBUG) Log.v(TAG, "Ignoring non-standard directory on " + p);
+ continue;
+ }
+
+ Set<String> dirs = grantedDirsByUuid.get(p.uuid);
+ if (dirs == null) {
+ // Life would be so much easier if Android had MultiMaps...
+ dirs = new HashSet<>(1);
+ grantedDirsByUuid.put(p.uuid, dirs);
+ }
+ dirs.add(p.directory);
+ }
+ }
+
+ if (DEBUG) {
+ Log.v(TAG, "grantedVolumes=" + grantedVolumes
+ + ", grantedDirectories=" + grantedDirsByUuid);
+ }
+ // Add granted permissions to full volumes.
+ grantedVolumes.forEach((uuid) -> permissions.add(new Object[] {
+ packageName, uuid, /* dir= */ null, 1
+ }));
+
+ // Add granted permissions to individual directories
+ grantedDirsByUuid.forEach((uuid, dirs) -> {
+ if (grantedVolumes.contains(uuid)) {
+ Log.w(TAG, "Ignoring individual grants to " + uuid + ": " + dirs);
+ } else {
+ dirs.forEach((dir) -> permissions.add(new Object[] {packageName, uuid, dir, 1}));
+ }
+ });
+ }
+
+ /**
+ * Converts the permissions returned by AM to our own format.
+ */
+ private List<Permission> parseGrantedPermissions(List<GrantedUriPermission> uriPermissions) {
+ final List<Permission> permissions = new ArrayList<>(uriPermissions.size());
+ // TODO(b/72055774): we should query AUTHORITY_STORAGE or call DocumentsContract instead of
+ // hardcoding the logic here.
+ for (GrantedUriPermission uriPermission : uriPermissions) {
+ final Uri uri = uriPermission.uri;
+ final String authority = uri.getAuthority();
+ if (!Providers.AUTHORITY_STORAGE.equals(authority)) {
+ Log.w(TAG, "Wrong authority on " + uri);
+ continue;
+ }
+ final List<String> pathSegments = uri.getPathSegments();
+ if (pathSegments.size() < 2) {
+ Log.w(TAG, "wrong path segments on " + uri);
+ continue;
+ }
+ // TODO(b/72055774): make PATH_TREE private again if not used anymore
+ if (!DocumentsContract.PATH_TREE.equals(pathSegments.get(0))) {
+ Log.w(TAG, "wrong path tree on " + uri);
+ continue;
+ }
+
+ final String[] uuidAndDir = pathSegments.get(1).split(":");
+ // uuid and dir are either UUID:DIR (for scoped directory) or UUID: (for full volume)
+ if (uuidAndDir.length != 1 && uuidAndDir.length != 2) {
+ Log.w(TAG, "could not parse uuid and directory on " + uri);
+ continue;
+ }
+ final String uuid = Providers.ROOT_ID_DEVICE.equals(uuidAndDir[0])
+ ? null // primary
+ : uuidAndDir[0]; // external volume
+ final String dir = uuidAndDir.length == 1 ? null : uuidAndDir[1];
+ permissions
+ .add(new Permission(uriPermission.packageName, uuid, dir, PERMISSION_GRANTED));
+ }
+ return permissions;
+ }
+
@Override
public String getType(Uri uri) {
return null;
@@ -246,22 +424,23 @@ public class ScopedAccessProvider extends ContentProvider {
}
pw.print("Permissions: ");
- final String[] selection = new String[packages.size()];
- packages.toArray(selection);
- try (Cursor cursor = getPermissionsCursor(selection)) {
- if (cursor == null) {
- pw.println("N/A");
- } else {
- pw.println(cursor.getCount());
- while (cursor.moveToNext()) {
- pw.print(prefix); pw.print(cursor.getString(TABLE_PERMISSIONS_COL_PACKAGE));
- pw.print('/');
- final String uuid = cursor.getString(TABLE_PERMISSIONS_COL_VOLUME_UUID);
- if (uuid != null) {
- pw.print(uuid); pw.print('>');
+ for (int i = 0; i < packages.size(); i++) {
+ final String pkg = packages.get(i);
+ try (Cursor cursor = getPermissionsCursor(pkg)) {
+ if (cursor == null) {
+ pw.println("N/A");
+ } else {
+ pw.println(cursor.getCount());
+ while (cursor.moveToNext()) {
+ pw.print(prefix); pw.print(cursor.getString(TABLE_PERMISSIONS_COL_PACKAGE));
+ pw.print('/');
+ final String uuid = cursor.getString(TABLE_PERMISSIONS_COL_VOLUME_UUID);
+ if (uuid != null) {
+ pw.print(uuid); pw.print('>');
+ }
+ pw.print(cursor.getString(TABLE_PERMISSIONS_COL_DIRECTORY));
+ pw.print(": "); pw.println(cursor.getInt(TABLE_PERMISSIONS_COL_GRANTED) == 1);
}
- pw.print(cursor.getString(TABLE_PERMISSIONS_COL_DIRECTORY));
- pw.print(": "); pw.println(cursor.getInt(TABLE_PERMISSIONS_COL_GRANTED) == 1);
}
}
}
diff --git a/src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java b/src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java
index 3b104c471..5b3accc81 100644
--- a/src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java
+++ b/src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java
@@ -54,11 +54,14 @@ public class ScopedAccessLocalPreferences {
public static final int PERMISSION_ASK = 0;
public static final int PERMISSION_ASK_AGAIN = 1;
public static final int PERMISSION_NEVER_ASK = -1;
+ // NOTE: this status is not used on preferences, but on permissions granted by AM
+ public static final int PERMISSION_GRANTED = 2;
@IntDef(flag = true, value = {
PERMISSION_ASK,
PERMISSION_ASK_AGAIN,
PERMISSION_NEVER_ASK,
+ PERMISSION_GRANTED
})
@Retention(RetentionPolicy.SOURCE)
public @interface PermissionStatus {}
@@ -181,6 +184,8 @@ public class ScopedAccessLocalPreferences {
return "PERMISSION_ASK_AGAIN";
case PERMISSION_NEVER_ASK:
return "PERMISSION_NEVER_ASK";
+ case PERMISSION_GRANTED:
+ return "PERMISSION_GRANTED";
default:
return "UNKNOWN";
}
@@ -211,7 +216,7 @@ public class ScopedAccessLocalPreferences {
public final String directory;
public final int status;
- private Permission(String pkg, String uuid, String directory, Integer status) {
+ public Permission(String pkg, String uuid, String directory, Integer status) {
this.pkg = pkg;
this.uuid = TextUtils.isEmpty(uuid) ? null : uuid;
this.directory = directory;