summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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;