Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 33230343 authored by Felipe Leme's avatar Felipe Leme
Browse files

Initial implementation of ScopedAccessProvider.update()

It does not interact with ActivityManager yet, but updates its preferences.

Test: manual verification
Bug: 63720392

Change-Id: I7dac63c3b7fd96fe72a6795e38178a8c6e7afdc5
parent 9de58077
Loading
Loading
Loading
Loading
+5 −4
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.documentsui;
import static android.os.Environment.isStandardDirectory;
import static android.os.storage.StorageVolume.EXTRA_DIRECTORY_NAME;
import static android.os.storage.StorageVolume.EXTRA_STORAGE_VOLUME;

import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ALREADY_DENIED;
import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_ALREADY_GRANTED;
import static com.android.documentsui.ScopedAccessMetrics.SCOPED_DIRECTORY_ACCESS_DENIED;
@@ -31,11 +32,13 @@ import static com.android.documentsui.ScopedAccessMetrics.logInvalidScopedAccess
import static com.android.documentsui.ScopedAccessMetrics.logValidScopedAccessRequest;
import static com.android.documentsui.base.SharedMinimal.DEBUG;
import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT;
import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getScopedAccessPermissionStatus;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.setScopedAccessPermissionStatus;

import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
@@ -116,10 +119,8 @@ public class ScopedAccessActivity extends Activity {
            finish();
            return;
        }
        String directoryName = intent.getStringExtra(EXTRA_DIRECTORY_NAME );
        if (directoryName == null) {
            directoryName = DIRECTORY_ROOT;
        }
        String directoryName =
                getInternalDirectoryName(intent.getStringExtra(EXTRA_DIRECTORY_NAME));
        final StorageVolume volume = (StorageVolume) storageVolume;
        if (getScopedAccessPermissionStatus(getApplicationContext(), getCallingPackage(),
                volume.getUuid(), directoryName) == PERMISSION_NEVER_ASK) {
+67 −45
Original line number Diff line number Diff line
@@ -15,8 +15,10 @@
 */
package com.android.documentsui;

import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_GRANTED;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COLUMNS;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COL_PACKAGE;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COLUMNS;
import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_DIRECTORY;
@@ -25,10 +27,16 @@ 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.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK;
import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN;
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.internal.util.Preconditions.checkArgument;

import android.app.ActivityManager;
import android.content.ContentProvider;
@@ -37,7 +45,6 @@ import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.util.ArraySet;
import android.util.Log;

import com.android.documentsui.prefs.ScopedAccessLocalPreferences.Permission;
@@ -48,8 +55,10 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

//TODO(b/72055774): update javadoc once implementation is finished
/**
 * Provider used to manage scoped access directory permissions.
 *
@@ -79,7 +88,7 @@ import java.util.stream.Collectors;
 * <p><b>Note:</b> the {@code query()} methods return all entries; it does not support selection or
 * projections.
 */
// TODO(b/63720392): add unit tests
// TODO(b/72055774): add unit tests
public class ScopedAccessProvider extends ContentProvider {

    private static final String TAG = "ScopedAccessProvider";
@@ -111,7 +120,7 @@ public class ScopedAccessProvider extends ContentProvider {
            case URI_PACKAGES:
                return getPackagesCursor();
            case URI_PERMISSIONS:
                return getPermissionsCursor();
                return getPermissionsCursor(selectionArgs);
            default:
                throw new UnsupportedOperationException("Unsupported Uri " + uri);
        }
@@ -119,7 +128,7 @@ public class ScopedAccessProvider extends ContentProvider {

    private Cursor getPackagesCursor() {
        // First get the packages that were denied
        final ArraySet<String> pkgs = getAllPackages(getContext());
        final Set<String> pkgs = getAllPackages(getContext());

        if (ArrayUtils.isEmpty(pkgs)) {
            if (DEBUG) Log.v(TAG, "getPackagesCursor(): ignoring " + pkgs);
@@ -130,41 +139,40 @@ public class ScopedAccessProvider extends ContentProvider {

        // Then create the cursor
        final MatrixCursor cursor = new MatrixCursor(TABLE_PACKAGES_COLUMNS, pkgs.size());
        final Object[] column = new Object[1];
        for (int i = 0; i < pkgs.size(); i++) {
            final String pkg = pkgs.valueAt(i);
            column[0] = pkg;
            cursor.addRow(column);
        }

        pkgs.forEach((pkg) -> cursor.addRow( new Object[] { pkg }));
        return cursor;
    }

    // TODO(b/63720392): decide how to handle ROOT_DIRECTORY - convert to null?
    private Cursor getPermissionsCursor() {
    private Cursor getPermissionsCursor(String[] packageNames) {
        // First get the packages that were denied
        final ArrayList<Permission> rawPermissions = getAllPermissions(getContext());
        final List<Permission> rawPermissions = getAllPermissions(getContext());

        if (ArrayUtils.isEmpty(rawPermissions)) {
            if (DEBUG) Log.v(TAG, "getPermissionsCursor(): ignoring " + rawPermissions);
            return null;
        }

        // TODO(b/72055774): unit tests for filters (permissions and/or package name);
        final List<Object[]> permissions = rawPermissions.stream()
                .filter(permission -> permission.status == PERMISSION_ASK_AGAIN
                        || permission.status == PERMISSION_NEVER_ASK)
                .map(permission ->
                        new Object[] { permission.pkg, permission.uuid, permission.directory,
                                Integer.valueOf(1) })
                .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());

        // 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).

        // TODO(b/63720392): also need to query AM for granted permissions

        // Then create the cursor
        final MatrixCursor cursor = new MatrixCursor(TABLE_PERMISSIONS_COLUMNS, permissions.size());
        for (int i = 0; i < permissions.size(); i++) {
            cursor.addRow(permissions.get(i));
        }
        permissions.forEach((row) -> cursor.addRow(row));
        return cursor;
    }

@@ -175,58 +183,72 @@ public class ScopedAccessProvider extends ContentProvider {

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        if (sMatcher.match(uri) != URI_PERMISSIONS) {
        throw new UnsupportedOperationException("insert(): unsupported " + uri);
    }

        if (DEBUG) Log.v(TAG, "insert(" + uri + "): " + values);

        // TODO(b/63720392): implement
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        if (sMatcher.match(uri) != URI_PERMISSIONS) {
        throw new UnsupportedOperationException("delete(): unsupported " + uri);
    }

        if (DEBUG) Log.v(TAG, "delete(" + uri + "): " + selection);

        // TODO(b/63720392): implement
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        if (sMatcher.match(uri) != URI_PERMISSIONS) {
            throw new UnsupportedOperationException("update(): unsupported " + uri);
        }

        if (DEBUG) Log.v(TAG, "update(" + uri + "): " + selection + " = " + values);
        if (DEBUG) {
            Log.v(TAG, "update(" + uri + "): " + Arrays.toString(selectionArgs) + " = " + values);
        }

        final boolean newValue = values.getAsBoolean(COL_GRANTED);

        // TODO(b/63720392): implement
        if (!newValue) {
            // TODO(b/63720392): need to call AM to disable it
            Log.w(TAG, "Disabling permission is not supported yet");
            return 0;
        }

        // TODO(b/72055774): add unit tests for invalid input
        checkArgument(selectionArgs != null && selectionArgs.length == 3,
                "Must have exactly 3 args: package_name, (nullable) uuid, (nullable) directory: "
                        + Arrays.toString(selectionArgs));
        final String packageName = selectionArgs[0];
        final String uuid = selectionArgs[1];
        final String dir = getInternalDirectoryName(selectionArgs[2]);

        // TODO(b/63720392): for now just set it as ASK so it's still listed on queries.
        // But the right approach is to call AM to grant the permission and then remove the entry
        // from our preferences
        setScopedAccessPermissionStatus(getContext(), packageName, uuid, dir, PERMISSION_ASK);

        return 1;
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        final String prefix = "  ";

        final List<String> packages = new ArrayList<>();
        pw.print("Packages: ");
        try (Cursor cursor = getPackagesCursor()) {
            if (cursor == null) {
            if (cursor == null || cursor.getCount() == 0) {
                pw.println("N/A");
            } else {
                pw.println(cursor.getCount());
                while (cursor.moveToNext()) {
                    pw.print(prefix); pw.println(cursor.getString(0));
                    final String pkg = cursor.getString(TABLE_PACKAGES_COL_PACKAGE);
                    packages.add(pkg);
                    pw.print(prefix);
                    pw.println(pkg);
                }
            }
        }

        pw.print("Permissions: ");
        try (Cursor cursor = getPermissionsCursor()) {
        final String[] selection = new String[packages.size()];
        packages.toArray(selection);
        try (Cursor cursor = getPermissionsCursor(selection)) {
            if (cursor == null) {
                pw.println("N/A");
            } else {
@@ -245,7 +267,7 @@ public class ScopedAccessProvider extends ContentProvider {
        }

        pw.print("Raw permissions: ");
        final ArrayList<Permission> rawPermissions = getAllPermissions(getContext());
        final List<Permission> rawPermissions = getAllPermissions(getContext());
        if (rawPermissions.isEmpty()) {
            pw.println("N/A");
        } else {
+19 −0
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.documentsui.base;

import android.annotation.Nullable;
import android.os.Build;
import android.util.Log;

@@ -36,6 +37,24 @@ public final class SharedMinimal {
    // Special directory name representing the full volume of a scoped directory request.
    public static final String DIRECTORY_ROOT = "ROOT_DIRECTORY";

    /**
     * Gets the name of a directory name in the format that's used internally by the app
     * (i.e., mapping {@code null} to {@link #DIRECTORY_ROOT});
     * if necessary.
     */
    public static String getInternalDirectoryName(@Nullable String name) {
        return name == null ? DIRECTORY_ROOT : name;
    }

    /**
     * Gets the name of a directory name in the format that is used externally
     * (i.e., mapping {@link #DIRECTORY_ROOT} to {@code null} if necessary);
     */
    @Nullable
    public static String getExternalDirectoryName(String name) {
        return name.equals(DIRECTORY_ROOT) ? null : name;
    }

    private SharedMinimal() {
        throw new UnsupportedOperationException("provides static fields only");
    }
+23 −7
Original line number Diff line number Diff line
@@ -15,6 +15,10 @@
 */
package com.android.documentsui.prefs;

import static com.android.documentsui.base.SharedMinimal.DEBUG;
import static com.android.documentsui.base.SharedMinimal.DIRECTORY_ROOT;
import static com.android.internal.util.Preconditions.checkArgument;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.content.Context;
@@ -29,14 +33,16 @@ import android.util.Log;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Methods for accessing the local preferences with regards to scoped directory access.
 */
//TODO(b/63720392): add unit tests
//TODO(b/72055774): add unit tests
public class ScopedAccessLocalPreferences {

    private static final String TAG = "ScopedAccessLocalPreferences";
@@ -80,7 +86,14 @@ public class ScopedAccessLocalPreferences {

    public static void setScopedAccessPermissionStatus(Context context, String packageName,
            @Nullable String uuid, String directory, @PermissionStatus int status) {
        checkArgument(!TextUtils.isEmpty(directory),
                "Cannot pass empty directory - did you mean %s?", DIRECTORY_ROOT);
        final String key = getScopedAccessDenialsKey(packageName, uuid, directory);
        if (DEBUG) {
            Log.d(TAG, "Setting permission of " + packageName + ":" + uuid + ":" + directory
                    + " to " + statusAsString(status));
        }

        getPrefs(context).edit().putInt(key, status).apply();
    }

@@ -101,7 +114,7 @@ public class ScopedAccessLocalPreferences {
        }
    }

    private static String getScopedAccessDenialsKey(String packageName, String uuid,
    private static String getScopedAccessDenialsKey(String packageName, @Nullable String uuid,
            String directory) {
        final int userId = UserHandle.myUserId();
        return uuid == null
@@ -121,10 +134,10 @@ public class ScopedAccessLocalPreferences {
    /**
     * Gets all packages that have entries in the preferences
     */
    public static ArraySet<String> getAllPackages(Context context) {
    public static Set<String> getAllPackages(Context context) {
        final SharedPreferences prefs = getPrefs(context);
        final ArraySet<String> pkgs = new ArraySet<>();

        final ArraySet<String> pkgs = new ArraySet<>();
        for (Entry<String, ?> pref : prefs.getAll().entrySet()) {
            final String key = pref.getKey();
            final String pkg = getPackage(key);
@@ -140,7 +153,7 @@ public class ScopedAccessLocalPreferences {
    /**
     * Gets all permissions.
     */
    public static ArrayList<Permission> getAllPermissions(Context context) {
    public static List<Permission> getAllPermissions(Context context) {
        final SharedPreferences prefs = getPrefs(context);
        final ArrayList<Permission> permissions = new ArrayList<>();

@@ -173,6 +186,7 @@ public class ScopedAccessLocalPreferences {
        }
    }

    @Nullable
    private static String getPackage(String key) {
        final Matcher matcher = KEY_PATTERN.matcher(key);
        return matcher.matches() ? matcher.group(1) : null;
@@ -191,6 +205,8 @@ public class ScopedAccessLocalPreferences {

    public static final class Permission {
        public final String pkg;

        @Nullable
        public final String uuid;
        public final String directory;
        public final int status;