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

Commit 9ed473f6 authored by android-build-team Robot's avatar android-build-team Robot
Browse files

Snap for 4571561 from a1569c56 to pi-release

Change-Id: I037a82189256a07248b400759601e602d79ddfc8
parents 7172e3da a1569c56
Loading
Loading
Loading
Loading
+4 −2
Original line number Diff line number Diff line
@@ -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;
+224 −45
Original line number Diff line number Diff line
@@ -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);
        }

        // 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
            });
        }

        // TODO(b/63720392): also need to query AM for granted permissions
        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,9 +424,9 @@ public class ScopedAccessProvider extends ContentProvider {
        }

        pw.print("Permissions: ");
        final String[] selection = new String[packages.size()];
        packages.toArray(selection);
        try (Cursor cursor = getPermissionsCursor(selection)) {
        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 {
@@ -265,6 +443,7 @@ public class ScopedAccessProvider extends ContentProvider {
                    }
                }
            }
        }

        pw.print("Raw permissions: ");
        final List<Permission> rawPermissions = getAllPermissions(getContext());
+6 −1
Original line number Diff line number Diff line
@@ -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;