1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.documentsui; 17 18 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.COL_GRANTED; 19 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES; 20 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COLUMNS; 21 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PACKAGES_COL_PACKAGE; 22 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS; 23 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COLUMNS; 24 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_DIRECTORY; 25 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_GRANTED; 26 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_PACKAGE; 27 import static android.os.storage.StorageVolume.ScopedAccessProviderContract.TABLE_PERMISSIONS_COL_VOLUME_UUID; 28 import static android.os.Environment.isStandardDirectory; 29 30 import static com.android.documentsui.base.SharedMinimal.DEBUG; 31 import static com.android.documentsui.base.SharedMinimal.getExternalDirectoryName; 32 import static com.android.documentsui.base.SharedMinimal.getInternalDirectoryName; 33 import static com.android.documentsui.base.SharedMinimal.getUriPermission; 34 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_ASK_AGAIN; 35 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_GRANTED; 36 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.PERMISSION_NEVER_ASK; 37 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.clearScopedAccessPreferences; 38 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getAllPackages; 39 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.getAllPermissions; 40 import static com.android.documentsui.prefs.ScopedAccessLocalPreferences.setScopedAccessPermissionStatus; 41 import static com.android.internal.util.Preconditions.checkArgument; 42 43 import android.annotation.Nullable; 44 import android.app.ActivityManager; 45 import android.app.GrantedUriPermission; 46 import android.content.ContentProvider; 47 import android.content.ContentProviderClient; 48 import android.content.ContentResolver; 49 import android.content.ContentValues; 50 import android.content.Context; 51 import android.content.Intent; 52 import android.content.UriMatcher; 53 import android.database.Cursor; 54 import android.database.MatrixCursor; 55 import android.net.Uri; 56 import android.os.Environment; 57 import android.os.UserHandle; 58 import android.os.storage.StorageManager; 59 import android.os.storage.StorageVolume; 60 import android.provider.DocumentsContract; 61 import android.util.ArraySet; 62 import android.util.Log; 63 64 import com.android.documentsui.base.Providers; 65 import com.android.documentsui.prefs.ScopedAccessLocalPreferences.Permission; 66 import com.android.internal.util.ArrayUtils; 67 68 import java.io.FileDescriptor; 69 import java.io.PrintWriter; 70 import java.util.ArrayList; 71 import java.util.Arrays; 72 import java.util.HashMap; 73 import java.util.HashSet; 74 import java.util.List; 75 import java.util.Map; 76 import java.util.Set; 77 78 //TODO(b/72055774): update javadoc once implementation is finished 79 /** 80 * Provider used to manage scoped access directory permissions. 81 * 82 * <p>It fetches data from 2 sources: 83 * 84 * <ul> 85 * <li>{@link com.android.documentsui.prefs.ScopedAccessLocalPreferences} for denied permissions. 86 * <li>{@link ActivityManager} for allowed permissions. 87 * </ul> 88 * 89 * <p>And returns the results in 2 tables: 90 * 91 * <ul> 92 * <li>{@link #TABLE_PACKAGES}: read-only table with the name of all packages 93 * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_PACKAGE}) that 94 * had a scoped access directory permission granted or denied. 95 * <li>{@link #TABLE_PERMISSIONS}: writable table with the name of all packages 96 * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_PACKAGE}) that 97 * had a scoped access directory 98 * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_DIRECTORY}) 99 * permission for a volume (column 100 * {@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_VOLUME_UUID}, which 101 * contains the volume UUID or {@code null} if it's the primary partition) granted or denied 102 * (column ({@link android.os.storage.StorageVolume.ScopedAccessProviderContract#COL_GRANTED}). 103 * </ul> 104 * 105 * <p><b>Note:</b> the {@code query()} methods return all entries; it does not support selection or 106 * projections. 107 */ 108 // TODO(b/72055774): add unit tests 109 public class ScopedAccessProvider extends ContentProvider { 110 111 private static final String TAG = "ScopedAccessProvider"; 112 private static final UriMatcher sMatcher = new UriMatcher(UriMatcher.NO_MATCH); 113 114 private static final int URI_PACKAGES = 1; 115 private static final int URI_PERMISSIONS = 2; 116 117 public static final String AUTHORITY = "com.android.documentsui.scopedAccess"; 118 119 static { 120 sMatcher.addURI(AUTHORITY, TABLE_PACKAGES + "/*", URI_PACKAGES); 121 sMatcher.addURI(AUTHORITY, TABLE_PERMISSIONS + "/*", URI_PERMISSIONS); 122 } 123 124 @Override 125 public boolean onCreate() { 126 return true; 127 } 128 129 @Override 130 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 131 String sortOrder) { 132 if (DEBUG) { 133 Log.v(TAG, "query(" + uri + "): proj=" + Arrays.toString(projection) 134 + ", sel=" + selection); 135 } 136 switch (sMatcher.match(uri)) { 137 case URI_PACKAGES: 138 return getPackagesCursor(); 139 case URI_PERMISSIONS: 140 if (ArrayUtils.isEmpty(selectionArgs)) { 141 throw new UnsupportedOperationException("selections cannot be empty"); 142 } 143 // For simplicity, we only support one package (which is what Settings is passing). 144 if (selectionArgs.length > 1) { 145 Log.w(TAG, "Using just first entry of " + Arrays.toString(selectionArgs)); 146 } 147 return getPermissionsCursor(selectionArgs[0]); 148 default: 149 throw new UnsupportedOperationException("Unsupported Uri " + uri); 150 } 151 } 152 153 private Cursor getPackagesCursor() { 154 final Context context = getContext(); 155 156 // First, get the packages that were denied 157 final Set<String> pkgs = getAllPackages(context); 158 159 // Second, query AM to get all packages that have a permission. 160 final ActivityManager am = 161 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 162 163 final List<GrantedUriPermission> amPkgs = am.getGrantedUriPermissions(null).getList(); 164 if (!amPkgs.isEmpty()) { 165 amPkgs.forEach((perm) -> pkgs.add(perm.packageName)); 166 } 167 168 if (ArrayUtils.isEmpty(pkgs)) { 169 if (DEBUG) Log.v(TAG, "getPackagesCursor(): nothing to do" ); 170 return null; 171 } 172 173 if (DEBUG) { 174 Log.v(TAG, "getPackagesCursor(): denied=" + pkgs + ", granted=" + amPkgs); 175 } 176 177 // Finally, create the cursor 178 final MatrixCursor cursor = new MatrixCursor(TABLE_PACKAGES_COLUMNS, pkgs.size()); 179 pkgs.forEach((pkg) -> cursor.addRow( new Object[] { pkg })); 180 return cursor; 181 } 182 183 // TODO(b/72055774): need to unit tests to handle scenarios where the root permission of 184 // a secondary volume mismatches a child permission (for example, child is allowed by root 185 // is denied). 186 private Cursor getPermissionsCursor(String packageName) { 187 final Context context = getContext(); 188 189 // List of volumes that were granted by AM at the root level - in that case, 190 // we can ignored individual grants from AM or denials from our preferences 191 final Set<String> grantedVolumes = new ArraySet<>(); 192 193 // List of directories (mapped by volume uuid) that were granted by AM so they can be 194 // ignored if also found on our preferences 195 final Map<String, Set<String>> grantedDirsByUuid = new HashMap<>(); 196 197 // Cursor rows 198 final List<Object[]> permissions = new ArrayList<>(); 199 200 // First, query AM to get all packages that have a permission. 201 final ActivityManager am = 202 (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 203 final List<GrantedUriPermission> uriPermissions = 204 am.getGrantedUriPermissions(packageName).getList(); 205 if (DEBUG) { 206 Log.v(TAG, "am returned =" + uriPermissions); 207 } 208 setGrantedPermissions(packageName, uriPermissions, permissions, grantedVolumes, 209 grantedDirsByUuid); 210 211 // Now gets the packages that were denied 212 final List<Permission> rawPermissions = getAllPermissions(context); 213 214 if (DEBUG) { 215 Log.v(TAG, "rawPermissions: " + rawPermissions); 216 } 217 218 // Merge the permissions granted by AM with the denied permissions saved on our preferences. 219 for (Permission rawPermission : rawPermissions) { 220 if (!packageName.equals(rawPermission.pkg)) { 221 if (DEBUG) { 222 Log.v(TAG, 223 "ignoring " + rawPermission + " because package is not " + packageName); 224 } 225 continue; 226 } 227 if (rawPermission.status != PERMISSION_NEVER_ASK 228 && rawPermission.status != PERMISSION_ASK_AGAIN) { 229 // We only care for status where the user denied a request. 230 if (DEBUG) { 231 Log.v(TAG, "ignoring " + rawPermission + " because of its status"); 232 } 233 continue; 234 } 235 if (grantedVolumes.contains(rawPermission.uuid)) { 236 if (DEBUG) { 237 Log.v(TAG, "ignoring " + rawPermission + " because whole volume is granted"); 238 } 239 continue; 240 } 241 final Set<String> grantedDirs = grantedDirsByUuid.get(rawPermission.uuid); 242 if (grantedDirs != null 243 && grantedDirs.contains(rawPermission.directory)) { 244 Log.w(TAG, "ignoring " + rawPermission + " because it was granted already"); 245 continue; 246 } 247 permissions.add(new Object[] { 248 packageName, rawPermission.uuid, 249 getExternalDirectoryName(rawPermission.directory), 0 250 }); 251 } 252 253 if (DEBUG) { 254 Log.v(TAG, "total permissions: " + permissions.size()); 255 } 256 257 // Then create the cursor 258 final MatrixCursor cursor = new MatrixCursor(TABLE_PERMISSIONS_COLUMNS, permissions.size()); 259 permissions.forEach((row) -> cursor.addRow(row)); 260 return cursor; 261 } 262 263 /** 264 * Converts the permissions returned by AM and add it to 3 buckets ({@code permissions}, 265 * {@code grantedVolumes}, and {@code grantedDirsByUuid}). 266 * 267 * @param packageName name of package that the permissions were granted to. 268 * @param uriPermissions permissions returend by AM 269 * @param permissions list of permissions that can be converted to a {@link #TABLE_PERMISSIONS} 270 * row. 271 * @param grantedVolumes volume uuids that were granted full access. 272 * @param grantedDirsByUuid directories that were granted individual acces (key is volume uuid, 273 * value is list of directories). 274 */ 275 private void setGrantedPermissions(String packageName, List<GrantedUriPermission> uriPermissions, 276 List<Object[]> permissions, Set<String> grantedVolumes, 277 Map<String, Set<String>> grantedDirsByUuid) { 278 final List<Permission> grantedPermissions = parseGrantedPermissions(uriPermissions); 279 280 for (Permission p : grantedPermissions) { 281 // First check if it's for the full volume 282 if (p.directory == null) { 283 if (p.uuid == null) { 284 // Should never happen - the Scoped Directory Access API does not allow it. 285 Log.w(TAG, "ignoring entry whose uuid and directory is null"); 286 continue; 287 } 288 grantedVolumes.add(p.uuid); 289 } else { 290 if (!ArrayUtils.contains(Environment.STANDARD_DIRECTORIES, p.directory)) { 291 if (DEBUG) Log.v(TAG, "Ignoring non-standard directory on " + p); 292 continue; 293 } 294 295 Set<String> dirs = grantedDirsByUuid.get(p.uuid); 296 if (dirs == null) { 297 // Life would be so much easier if Android had MultiMaps... 298 dirs = new HashSet<>(1); 299 grantedDirsByUuid.put(p.uuid, dirs); 300 } 301 dirs.add(p.directory); 302 } 303 } 304 305 if (DEBUG) { 306 Log.v(TAG, "grantedVolumes=" + grantedVolumes 307 + ", grantedDirectories=" + grantedDirsByUuid); 308 } 309 // Add granted permissions to full volumes. 310 grantedVolumes.forEach((uuid) -> permissions.add(new Object[] { 311 packageName, uuid, /* dir= */ null, 1 312 })); 313 314 // Add granted permissions to individual directories 315 grantedDirsByUuid.forEach((uuid, dirs) -> { 316 if (grantedVolumes.contains(uuid)) { 317 Log.w(TAG, "Ignoring individual grants to " + uuid + ": " + dirs); 318 } else { 319 dirs.forEach((dir) -> permissions.add(new Object[] {packageName, uuid, dir, 1})); 320 } 321 }); 322 } 323 324 /** 325 * Converts the permissions returned by AM to our own format. 326 */ 327 private List<Permission> parseGrantedPermissions(List<GrantedUriPermission> uriPermissions) { 328 final List<Permission> permissions = new ArrayList<>(uriPermissions.size()); 329 // TODO(b/72055774): we should query AUTHORITY_STORAGE or call DocumentsContract instead of 330 // hardcoding the logic here. 331 for (GrantedUriPermission uriPermission : uriPermissions) { 332 final Uri uri = uriPermission.uri; 333 final String authority = uri.getAuthority(); 334 if (!Providers.AUTHORITY_STORAGE.equals(authority)) { 335 Log.w(TAG, "Wrong authority on " + uri); 336 continue; 337 } 338 final List<String> pathSegments = uri.getPathSegments(); 339 if (pathSegments.size() < 2) { 340 Log.w(TAG, "wrong path segments on " + uri); 341 continue; 342 } 343 // TODO(b/72055774): make PATH_TREE private again if not used anymore 344 if (!DocumentsContract.PATH_TREE.equals(pathSegments.get(0))) { 345 Log.w(TAG, "wrong path tree on " + uri); 346 continue; 347 } 348 349 final String[] uuidAndDir = pathSegments.get(1).split(":"); 350 // uuid and dir are either UUID:DIR (for scoped directory) or UUID: (for full volume) 351 if (uuidAndDir.length != 1 && uuidAndDir.length != 2) { 352 Log.w(TAG, "could not parse uuid and directory on " + uri); 353 continue; 354 } 355 // TODO(b/72055774): to make things uglier, the Documents directory in the primary 356 // storage is a special case as its URI is "$ROOT_ID_HOME", instead of 357 // "${ROOT_ID_DEVICE}/Documents. This is another reason to move this logic to the 358 // provider... 359 final String uuid, dir; 360 if (Providers.ROOT_ID_HOME.equals(uuidAndDir[0])) { 361 uuid = null; 362 dir = Environment.DIRECTORY_DOCUMENTS; 363 } else { 364 uuid = Providers.ROOT_ID_DEVICE.equals(uuidAndDir[0]) 365 ? null // primary 366 : uuidAndDir[0]; // external volume 367 dir = uuidAndDir.length == 1 ? null : uuidAndDir[1]; 368 } 369 permissions 370 .add(new Permission(uriPermission.packageName, uuid, dir, PERMISSION_GRANTED)); 371 } 372 return permissions; 373 } 374 375 @Override 376 public String getType(Uri uri) { 377 return null; 378 } 379 380 @Override 381 public Uri insert(Uri uri, ContentValues values) { 382 throw new UnsupportedOperationException("insert(): unsupported " + uri); 383 } 384 385 @Override 386 public int delete(Uri uri, String selection, String[] selectionArgs) { 387 if (sMatcher.match(uri) != URI_PERMISSIONS) { 388 throw new UnsupportedOperationException("delete(): unsupported " + uri); 389 } 390 391 if (DEBUG) { 392 Log.v(TAG, "delete(" + uri + "): " + Arrays.toString(selectionArgs)); 393 } 394 395 // TODO(b/72055774): add unit tests for invalid input 396 checkArgument(selectionArgs != null && selectionArgs.length == 1, 397 "Must have exactly 1 args: package_name" + Arrays.toString(selectionArgs)); 398 final String packageName = selectionArgs[0]; 399 400 // Delete just our preferences - the URI permissions is handled externally 401 // TODO(b/72055774): move logic to revoke permissions here, so AppStorageSettings does 402 // not need to call am.clearGrantedUriPermissions(packageName) (then we could remove that 403 // method from ActivityManager) 404 return clearScopedAccessPreferences(getContext(), packageName); 405 } 406 407 @Override 408 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 409 if (sMatcher.match(uri) != URI_PERMISSIONS) { 410 throw new UnsupportedOperationException("update(): unsupported " + uri); 411 } 412 413 if (DEBUG) { 414 Log.v(TAG, "update(" + uri + "): " + Arrays.toString(selectionArgs) + " = " + values); 415 } 416 417 // TODO(b/72055774): add unit tests for invalid input 418 checkArgument(selectionArgs != null && selectionArgs.length == 3, 419 "Must have exactly 3 args: package_name, (nullable) uuid, (nullable) directory: " 420 + Arrays.toString(selectionArgs)); 421 final String packageName = selectionArgs[0]; 422 final String uuid = selectionArgs[1]; 423 final String dir = selectionArgs[2]; 424 final boolean granted = values.getAsBoolean(COL_GRANTED); 425 426 // First update the effective URI permission ... 427 if (!persistUriPermission(packageName, uuid, dir, granted)) { 428 // Failed - nothing left to do... 429 return 0; 430 } 431 432 // ...then our preferences. 433 setScopedAccessPermissionStatus(getContext(), packageName, uuid, 434 getInternalDirectoryName(dir), granted ? PERMISSION_GRANTED : PERMISSION_NEVER_ASK); 435 return 1; 436 } 437 438 /** 439 * Calls AM to persist a URI. 440 * 441 * @return whether the call succeeded. 442 */ 443 private boolean persistUriPermission(String packageName, @Nullable String uuid, 444 @Nullable String directory, boolean granted) { 445 final Context context = getContext(); 446 447 final ContentProviderClient storageClient = context.getContentResolver() 448 .acquireContentProviderClient(Providers.AUTHORITY_STORAGE); 449 450 final StorageManager sm = context.getSystemService(StorageManager.class); 451 452 StorageVolume volume = null; 453 if (uuid == null) { 454 if (directory == null) { 455 Log.w(TAG, "cannot grant full access to the primary volume"); 456 return false; 457 } 458 volume = sm.getPrimaryStorageVolume(); 459 } else { 460 for (StorageVolume candidate : sm.getVolumeList()) { 461 if (uuid.equals(candidate.getUuid())) { 462 volume = candidate; 463 break; 464 } 465 } 466 if (volume == null) { 467 Log.w(TAG, "didn't find volume for UUID=" + uuid); 468 return false; 469 } 470 if (directory != null && !isStandardDirectory(directory)) { 471 Log.w(TAG, "not a scoped directory: " + directory); 472 return false; 473 } 474 } 475 476 return getUriPermission(context, storageClient, volume, getInternalDirectoryName(directory), 477 UserHandle.getCallingUserId(), /* logMetrics= */ false, 478 (file, volumeLabel, isRoot, isPrimary, grantedUri, rootUri) -> { 479 updatePermission(context, grantedUri, packageName, granted); 480 return true; 481 }); 482 } 483 484 private void updatePermission(Context context, Uri grantedUri, String toPackage, 485 boolean granted) { 486 final int persistFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION 487 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; 488 final int grantFlags = persistFlags 489 | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION 490 | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; 491 492 final ContentResolver cr = context.getContentResolver(); 493 if (granted) { 494 context.grantUriPermission(toPackage, grantedUri, grantFlags); 495 cr.takePersistableUriPermission(toPackage, grantedUri, persistFlags); 496 } else { 497 context.revokeUriPermission(grantedUri, grantFlags); 498 // There's no need to release after revoking 499 } 500 } 501 502 @Override 503 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 504 final String prefix = " "; 505 506 final List<String> packages = new ArrayList<>(); 507 pw.print("Packages: "); 508 try (Cursor cursor = getPackagesCursor()) { 509 if (cursor == null || cursor.getCount() == 0) { 510 pw.println("N/A"); 511 } else { 512 pw.println(cursor.getCount()); 513 while (cursor.moveToNext()) { 514 final String pkg = cursor.getString(TABLE_PACKAGES_COL_PACKAGE); 515 packages.add(pkg); 516 pw.print(prefix); 517 pw.println(pkg); 518 } 519 } 520 } 521 522 pw.print("Permissions: "); 523 for (int i = 0; i < packages.size(); i++) { 524 final String pkg = packages.get(i); 525 try (Cursor cursor = getPermissionsCursor(pkg)) { 526 if (cursor == null) { 527 pw.println("N/A"); 528 } else { 529 pw.println(cursor.getCount()); 530 while (cursor.moveToNext()) { 531 pw.print(prefix); pw.print(cursor.getString(TABLE_PERMISSIONS_COL_PACKAGE)); 532 pw.print('/'); 533 final String uuid = cursor.getString(TABLE_PERMISSIONS_COL_VOLUME_UUID); 534 if (uuid != null) { 535 pw.print(uuid); pw.print('>'); 536 } 537 pw.print(cursor.getString(TABLE_PERMISSIONS_COL_DIRECTORY)); 538 pw.print(": "); pw.println(cursor.getInt(TABLE_PERMISSIONS_COL_GRANTED) == 1); 539 } 540 } 541 } 542 } 543 544 pw.print("Raw permissions: "); 545 final List<Permission> rawPermissions = getAllPermissions(getContext()); 546 if (rawPermissions.isEmpty()) { 547 pw.println("N/A"); 548 } else { 549 final int size = rawPermissions.size(); 550 pw.println(size); 551 for (int i = 0; i < size; i++) { 552 final Permission permission = rawPermissions.get(i); 553 pw.print(prefix); pw.println(permission); 554 } 555 } 556 } 557 } 558