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

Commit 4a61b069 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Initial implementation of ScopedAccessProvider."

parents 67ded238 be868446
Loading
Loading
Loading
Loading
+7 −0
Original line number Diff line number Diff line
@@ -133,6 +133,13 @@
            </intent-filter>
        </activity>

        <provider
            android:name=".ScopedAccessProvider"
            android:authorities="com.android.documentsui.scopedAccess"
            android:permission="android.permission.MANAGE_SCOPED_ACCESS_DIRECTORY_PERMISSIONS"
            android:exported="true">
        </provider>

        <provider
            android:name=".picker.LastAccessedProvider"
            android:authorities="com.android.documentsui.lastAccessed"
+7 −0
Original line number Diff line number Diff line
@@ -47,5 +47,12 @@
            </intent-filter>
        </receiver>

        <provider
            android:name=".ScopedAccessProvider"
            android:authorities="com.android.documentsui.scopedAccess"
            android:permission="android.permission.MANAGE_SCOPED_ACCESS_DIRECTORY_PERMISSIONS"
            android:exported="true">
        </provider>

    </application>
</manifest>
+242 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.documentsui;

import static com.android.documentsui.base.Shared.VERBOSE;
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 android.app.ActivityManager;
import android.content.ContentProvider;
import android.content.ContentValues;
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;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Provider used to manage scoped access directory permissions.
 *
 * <p>It fetches data from 2 sources:
 *
 * <ul>
 * <li>{@link com.android.documentsui.prefs.ScopedAccessLocalPreferences} for denied permissions.
 * <li>{@link ActivityManager} for allowed permissions.
 * </ul>
 *
 * <p>And returns the results in 2 tables:
 *
 * <ul>
 * <li>{@link #TABLE_PACKAGES}: read-only table with the name of all packages
 * (column ({@link #COL_PACKAGE}) that had a scoped access directory permission granted or denied.
 * <li>{@link #TABLE_PERMISSIONS}: writable table with the name of all packages
 * (column ({@link #COL_PACKAGE}) that had a scoped access directory
 * (column ({@link #COL_DIRECTORY}) permission granted or denied (column ({@link #COL_GRANTED}).
 * </ul>
 *
 * <p><b>Note:</b> the {@code query()} methods return all entries; it does not support selection or
 * projections.
 */
// TODO(b/63720392): add unit tests
public class ScopedAccessProvider extends ContentProvider {

    private static final String TAG = "ScopedAccessProvider";
    private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    // TODO(b/63720392): move constants below to @hide values on DocumentsContract so Settings can
    // use them

    // Packages that have scoped access permissions
    private static final int URI_PACKAGES = 1;
    private static final String TABLE_PACKAGES = "packages";

    // Permissions per packages
    private static final int URI_PERMISSIONS = 2;
    private static final String TABLE_PERMISSIONS = "permissions";

    // Columns
    private static final String COL_PACKAGE = "package_name";
    private static final String COL_DIRECTORY = "directory";
    private static final String COL_GRANTED = "granted";

    public static final String AUTHORITY = "com.android.documentsui.scopedAccess";

    static {
        sMatcher.addURI(AUTHORITY, TABLE_PACKAGES + "/*", URI_PACKAGES);
        sMatcher.addURI(AUTHORITY, TABLE_PERMISSIONS + "/*", URI_PERMISSIONS);
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        if (VERBOSE) {
            Log.v(TAG, "query(" + uri + "): proj=" + Arrays.toString(projection)
                + ", sel=" + selection);
        }
        switch (sMatcher.match(uri)) {
            case URI_PACKAGES:
                return getPackagesCursor();
            case URI_PERMISSIONS:
                return getPermissionsCursor();
            default:
                throw new UnsupportedOperationException("Unsupported Uri " + uri);
        }
    }

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

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

        // Then create the cursor
        final MatrixCursor cursor = new MatrixCursor(new String[] {COL_PACKAGE}, 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);
        }

        return cursor;
    }

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

        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.directory, Integer.valueOf(1) })
                .collect(Collectors.toList());

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

        // Then create the cursor
        final MatrixCursor cursor = new MatrixCursor(
                new String[] {COL_PACKAGE, COL_DIRECTORY, COL_GRANTED}, permissions.size());
        for (int i = 0; i < permissions.size(); i++) {
            cursor.addRow(permissions.get(i));
        }
        return cursor;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }

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

        if (VERBOSE) 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 (VERBOSE) 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 (VERBOSE) Log.v(TAG, "update(" + uri + "): " + selection + " = " + values);

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

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

        pw.print("Packages: ");
        try (Cursor cursor = getPackagesCursor()) {
            if (cursor == null) {
                pw.println("N/A");
            } else {
                pw.println(cursor.getCount());
                while (cursor.moveToNext()) {
                    pw.print(prefix); pw.println(cursor.getString(0));
                }
            }
        }

        pw.print("Permissions: ");
        try (Cursor cursor = getPermissionsCursor()) {
            if (cursor == null) {
                pw.println("N/A");
            } else {
                pw.println(cursor.getCount());
                while (cursor.moveToNext()) {
                    pw.print(prefix); pw.print(cursor.getString(0));
                    pw.print(" / "); pw.print(cursor.getString(1));
                    pw.print(": "); pw.println(cursor.getInt(2) == 1);
                }
            }
        }

        pw.print("Raw permissions: ");
        final ArrayList<Permission> rawPermissions = getAllPermissions(getContext());
        if (rawPermissions.isEmpty()) {
            pw.println("N/A");
        } else {
            final int size = rawPermissions.size();
            pw.println(size);
            for (int i = 0; i < size; i++) {
                final Permission permission = rawPermissions.get(i);
                pw.print(prefix); pw.println(permission);
            }
        }
    }
}
+100 −0
Original line number Diff line number Diff line
@@ -22,15 +22,24 @@ import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.UserHandle;
import android.preference.PreferenceManager;
import android.util.ArraySet;
import android.util.Log;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Map.Entry;
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
public class ScopedAccessLocalPreferences {

    private static final String TAG = "ScopedAccessLocalPreferences";

    private static SharedPreferences getPrefs(Context context) {
        return PreferenceManager.getDefaultSharedPreferences(context);
    }
@@ -47,6 +56,9 @@ public class ScopedAccessLocalPreferences {
    @Retention(RetentionPolicy.SOURCE)
    public @interface PermissionStatus {}

    private static final String KEY_REGEX = "^.+\\|(.+)\\|.*\\|(.+)$";
    private static final Pattern KEY_PATTERN = Pattern.compile(KEY_REGEX);

    /**
     * Methods below are used to keep track of denied user requests on scoped directory access so
     * the dialog is not offered when user checked the 'Do not ask again' box
@@ -104,4 +116,92 @@ public class ScopedAccessLocalPreferences {
    public static void clearPackagePreferences(Context context, String packageName) {
        ScopedAccessLocalPreferences.clearScopedAccessPreferences(context, packageName);
    }

    /**
     * Gets all packages that have entries in the preferences
     */
    public static ArraySet<String> getAllPackages(Context context) {
        final SharedPreferences prefs = getPrefs(context);
        final ArraySet<String> pkgs = new ArraySet<>();

        for (Entry<String, ?> pref : prefs.getAll().entrySet()) {
            final String key = pref.getKey();
            final String pkg = getPackage(key);
            if (pkg == null) {
                Log.w(TAG, "getAllPackages(): error parsing pref '" + key + "'");
                continue;
            }
            pkgs.add(pkg);
        }
        return pkgs;
    }

    /**
     * Gets all permissions.
     */
    public static ArrayList<Permission> getAllPermissions(Context context) {
        final SharedPreferences prefs = getPrefs(context);
        final ArrayList<Permission> permissions = new ArrayList<>();

        for (Entry<String, ?> pref : prefs.getAll().entrySet()) {
            final String key = pref.getKey();
            final Object value = pref.getValue();
            final Integer status;
            try {
                status = (Integer) value;
            } catch (Exception e) {
                Log.w(TAG, "error gettting value for key '" + key + "': " + value);
                continue;
            }
            permissions.add(getPermission(key, status));
        }

        return permissions;
    }

    public static String statusAsString(@PermissionStatus int status) {
        switch (status) {
            case PERMISSION_ASK:
                return "PERMISSION_ASK";
            case PERMISSION_ASK_AGAIN:
                return "PERMISSION_ASK_AGAIN";
            case PERMISSION_NEVER_ASK:
                return "PERMISSION_NEVER_ASK";
            default:
                return "UNKNOWN";
        }
    }

    private static String getPackage(String key) {
        final Matcher matcher = KEY_PATTERN.matcher(key);
        return matcher.matches() ? matcher.group(1) : null;
    }

    private static Permission getPermission(String key, Integer status) {
        final Matcher matcher = KEY_PATTERN.matcher(key);
        if (!matcher.matches()) return null;

        final String pkg = matcher.group(1);
        final String directory = matcher.group(2);

        return new Permission(pkg, directory, status);
    }

    public static final class Permission {
        public final String pkg;
        public final String directory;
        public final int status;

        private Permission(String pkg, String directory, Integer status) {
            this.pkg = pkg;
            this.directory = directory;
            this.status = status.intValue();
        }

        @Override
        public String toString() {
            return "Permission: [pkg=" + pkg + ", dir=" + directory + ", status="
                    + statusAsString(status) + " (" + status + ")]";
        }
    }
}