Loading AndroidManifest.xml +7 −0 Original line number Diff line number Diff line Loading @@ -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" Loading minimal/AndroidManifest.xml +7 −0 Original line number Diff line number Diff line Loading @@ -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> src/com/android/documentsui/ScopedAccessProvider.java 0 → 100644 +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); } } } } src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java +100 −0 Original line number Diff line number Diff line Loading @@ -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); } Loading @@ -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 Loading Loading @@ -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 + ")]"; } } } Loading
AndroidManifest.xml +7 −0 Original line number Diff line number Diff line Loading @@ -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" Loading
minimal/AndroidManifest.xml +7 −0 Original line number Diff line number Diff line Loading @@ -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>
src/com/android/documentsui/ScopedAccessProvider.java 0 → 100644 +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); } } } }
src/com/android/documentsui/prefs/ScopedAccessLocalPreferences.java +100 −0 Original line number Diff line number Diff line Loading @@ -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); } Loading @@ -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 Loading Loading @@ -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 + ")]"; } } }