1 /* 2 * Copyright (C) 2006 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 17 package com.android.providers.media; 18 19 import static com.android.providers.media.LocalUriMatcher.PICKER_ID; 20 import static com.android.providers.media.util.DatabaseUtils.replaceMatchAnyChar; 21 22 import android.content.ContentUris; 23 import android.content.ContentValues; 24 import android.database.Cursor; 25 import android.database.sqlite.SQLiteConstraintException; 26 import android.database.sqlite.SQLiteDatabase; 27 import android.database.sqlite.SQLiteQueryBuilder; 28 import android.net.Uri; 29 import android.provider.MediaStore; 30 import android.text.TextUtils; 31 import android.util.Log; 32 33 import androidx.annotation.NonNull; 34 35 import com.android.providers.media.photopicker.PickerSyncController; 36 37 import java.util.ArrayList; 38 import java.util.Arrays; 39 import java.util.List; 40 import java.util.Objects; 41 import java.util.stream.Collectors; 42 43 /** 44 * Manager class for the {@code media_grants} table in the {@link 45 * DatabaseHelper#EXTERNAL_DATABASE_NAME} database file. 46 * 47 * <p>Manages media grants for files in the {@code files} table based on package name. 48 */ 49 public class MediaGrants { 50 public static final String TAG = "MediaGrants"; 51 public static final String MEDIA_GRANTS_TABLE = "media_grants"; 52 public static final String FILE_ID_COLUMN = "file_id"; 53 public static final String PACKAGE_USER_ID_COLUMN = "package_user_id"; 54 public static final String GENERATION_GRANTED = "generation_granted"; 55 public static final String OWNER_PACKAGE_NAME_COLUMN = 56 MediaStore.MediaColumns.OWNER_PACKAGE_NAME; 57 58 private static final String CREATE_TEMPORARY_TABLE_QUERY = "CREATE TEMPORARY TABLE "; 59 private static final String MEDIA_GRANTS_AND_FILES_JOIN_TABLE_NAME = "media_grants LEFT JOIN " 60 + "files ON media_grants.file_id = files._id"; 61 62 private static final String WHERE_MEDIA_GRANTS_PACKAGE_NAME_IN = 63 "media_grants." + MediaGrants.OWNER_PACKAGE_NAME_COLUMN + " IN "; 64 65 private static final String WHERE_MEDIA_GRANTS_USER_ID = 66 "media_grants." + MediaGrants.PACKAGE_USER_ID_COLUMN + " = ? "; 67 68 private static final String WHERE_ITEM_IS_NOT_TRASHED = 69 "files." + MediaStore.Files.FileColumns.IS_TRASHED + " = ? "; 70 71 private static final String WHERE_ITEM_IS_NOT_PENDING = 72 "files." + MediaStore.Files.FileColumns.IS_PENDING + " = ? "; 73 74 private static final String WHERE_MEDIA_TYPE = 75 "files." + MediaStore.Files.FileColumns.MEDIA_TYPE + " IN "; 76 77 private static final String WHERE_MIME_TYPE = 78 "files." + MediaStore.Files.FileColumns.MIME_TYPE + " LIKE ? "; 79 80 private static final String WHERE_VOLUME_NAME_IN = 81 "files." + MediaStore.Files.FileColumns.VOLUME_NAME + " IN "; 82 83 private static final String TEMP_TABLE_NAME_FOR_DELETION = 84 "temp_table_for_media_grants_deletion"; 85 86 private static final String TEMP_TABLE_FOR_DELETION_FILE_ID_COLUMN_NAME = 87 "temp_table_for_media_grants_deletion.file_id"; 88 89 private static final String ARG_VALUE_FOR_FALSE = "0"; 90 91 private static final int VISUAL_MEDIA_TYPE_COUNT = 2; 92 private SQLiteQueryBuilder mQueryBuilder = new SQLiteQueryBuilder(); 93 private DatabaseHelper mExternalDatabase; 94 private LocalUriMatcher mUriMatcher; 95 MediaGrants(DatabaseHelper externalDatabaseHelper)96 public MediaGrants(DatabaseHelper externalDatabaseHelper) { 97 mExternalDatabase = externalDatabaseHelper; 98 mUriMatcher = new LocalUriMatcher(MediaStore.AUTHORITY); 99 mQueryBuilder.setTables(MEDIA_GRANTS_TABLE); 100 } 101 102 /** 103 * Adds media_grants for the provided URIs for the provided package name. 104 * 105 * @param packageName the package name that will receive access. 106 * @param uris list of content {@link android.net.Uri} that are recognized by 107 * mediaprovider. 108 * @param packageUserId the user_id of the package 109 */ addMediaGrantsForPackage(String packageName, List<Uri> uris, int packageUserId)110 void addMediaGrantsForPackage(String packageName, List<Uri> uris, int packageUserId) 111 throws IllegalArgumentException { 112 113 Objects.requireNonNull(packageName); 114 Objects.requireNonNull(uris); 115 116 mExternalDatabase.runWithTransaction( 117 (db) -> { 118 long generation_granted = DatabaseHelper.getGeneration(db); 119 for (Uri uri : uris) { 120 121 if (!isUriAllowed(uri)) { 122 throw new IllegalArgumentException( 123 "Illegal Uri, cannot create media grant for malformed uri: " 124 + uri.toString()); 125 } 126 127 Long id = ContentUris.parseId(uri); 128 final ContentValues values = new ContentValues(); 129 values.put(OWNER_PACKAGE_NAME_COLUMN, packageName); 130 values.put(FILE_ID_COLUMN, id); 131 values.put(PACKAGE_USER_ID_COLUMN, packageUserId); 132 values.put(GENERATION_GRANTED, generation_granted); 133 134 try { 135 mQueryBuilder.insert(db, values); 136 } catch (SQLiteConstraintException exception) { 137 // no-op 138 // this may happen due to the presence of a foreign key between the 139 // media_grants and files table. An SQLiteConstraintException 140 // exception my occur if: while inserting the grant for a file, the 141 // file itself is deleted. In this situation no operation is required. 142 } 143 } 144 145 Log.d( 146 TAG, 147 String.format( 148 "Successfully added %s media_grants for %s.", 149 uris.size(), packageName)); 150 151 return null; 152 }); 153 } 154 155 /** 156 * Returns the cursor for file data of items for which the passed package has READ_GRANTS. 157 * 158 * @param packageNames the package name that has access. 159 * @param packageUserId the user_id of the package 160 */ getMediaGrantsForPackages(String[] packageNames, int packageUserId, String[] mimeTypes, String[] availableVolumes)161 Cursor getMediaGrantsForPackages(String[] packageNames, int packageUserId, 162 String[] mimeTypes, String[] availableVolumes) 163 throws IllegalArgumentException { 164 Objects.requireNonNull(packageNames); 165 return mExternalDatabase.runWithoutTransaction((db) -> { 166 final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 167 queryBuilder.setDistinct(true); 168 queryBuilder.setTables(MEDIA_GRANTS_AND_FILES_JOIN_TABLE_NAME); 169 String[] selectionArgs = buildSelectionArg(queryBuilder, 170 QueryFilterBuilder.newInstance() 171 .setPackageNameSelection(packageNames) 172 .setUserIdSelection(packageUserId) 173 .setIsNotTrashedSelection(true) 174 .setIsNotPendingSelection(true) 175 .setIsOnlyVisualMediaType(true) 176 .setMimeTypeSelection(mimeTypes) 177 .setAvailableVolumes(availableVolumes) 178 .build()); 179 180 return queryBuilder.query(db, 181 new String[]{FILE_ID_COLUMN, PACKAGE_USER_ID_COLUMN}, null, 182 selectionArgs, null, null, null, null, null); 183 }); 184 } 185 removeMediaGrantsForPackage(@onNull String[] packages, @NonNull List<Uri> uris, int packageUserId)186 int removeMediaGrantsForPackage(@NonNull String[] packages, @NonNull List<Uri> uris, 187 int packageUserId) { 188 Objects.requireNonNull(packages); 189 Objects.requireNonNull(uris); 190 if (packages.length == 0) { 191 throw new IllegalArgumentException( 192 "Removing grants requires a non empty package name."); 193 } 194 195 return mExternalDatabase.runWithTransaction( 196 (db) -> { 197 // create a temporary table to be used as a selection criteria for local ids. 198 createTempTableWithLocalIdsAsColumn(uris, db); 199 200 // Create query builder and add selection args. 201 final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 202 queryBuilder.setDistinct(true); 203 queryBuilder.setTables(MEDIA_GRANTS_TABLE); 204 String[] selectionArgs = buildSelectionArg(queryBuilder, 205 QueryFilterBuilder.newInstance() 206 .setPackageNameSelection(packages) 207 .setUserIdSelection(packageUserId) 208 .setUriSelection(uris) 209 .build()); 210 // execute query. 211 int grantsRemoved = queryBuilder.delete(db, null, selectionArgs); 212 Log.d( 213 TAG, 214 String.format( 215 "Removed %s media_grants for %s user for %s.", 216 grantsRemoved, 217 String.valueOf(packageUserId), 218 Arrays.toString(packages))); 219 // Drop the temporary table. 220 deleteTempTableCreatedForLocalIdSelection(db); 221 return grantsRemoved; 222 }); 223 } 224 225 private static void createTempTableWithLocalIdsAsColumn(@NonNull List<Uri> uris, 226 @NonNull SQLiteDatabase db) { 227 228 // create a temporary table and insert the ids from received uris. 229 db.execSQL(String.format(CREATE_TEMPORARY_TABLE_QUERY + "%s (%s INTEGER)", 230 TEMP_TABLE_NAME_FOR_DELETION, FILE_ID_COLUMN)); 231 232 final SQLiteQueryBuilder queryBuilderTempTable = new SQLiteQueryBuilder(); 233 queryBuilderTempTable.setTables(TEMP_TABLE_NAME_FOR_DELETION); 234 235 List<List<Uri>> listOfSelectionArgsForId = splitArrayList(uris, 236 /* number of ids per query */ 50); 237 238 StringBuilder sb = new StringBuilder(); 239 List<Uri> selectionArgForIdSelection; 240 for (int itr = 0; itr < listOfSelectionArgsForId.size(); itr++) { 241 selectionArgForIdSelection = listOfSelectionArgsForId.get(itr); 242 if (itr == 0 || selectionArgForIdSelection.size() != listOfSelectionArgsForId.get( 243 itr - 1).size()) { 244 sb.setLength(0); 245 for (int i = 0; i < selectionArgForIdSelection.size() - 1; i++) { 246 sb.append("(?)").append(","); 247 } 248 sb.append("(?)"); 249 } 250 db.execSQL("INSERT INTO " + TEMP_TABLE_NAME_FOR_DELETION + " VALUES " + sb.toString(), 251 selectionArgForIdSelection.stream().map( 252 ContentUris::parseId).collect(Collectors.toList()).stream().toArray()); 253 } 254 } 255 256 private static <T> List<List<T>> splitArrayList(List<T> list, int chunkSize) { 257 List<List<T>> subLists = new ArrayList<>(); 258 for (int i = 0; i < list.size(); i += chunkSize) { 259 subLists.add(list.subList(i, Math.min(i + chunkSize, list.size()))); 260 } 261 return subLists; 262 } 263 264 private static void deleteTempTableCreatedForLocalIdSelection(SQLiteDatabase db) { 265 db.execSQL("DROP TABLE " + TEMP_TABLE_NAME_FOR_DELETION); 266 } 267 268 /** 269 * Removes any existing media grants for the given package from the external database. This will 270 * not alter the files or file metadata themselves. 271 * 272 * <p><strong>Note:</strong> Any files that are removed from the system because of any deletion 273 * operation or as a result of a package being uninstalled / orphaned will lead to deletion of 274 * database entry in files table. Any deletion in files table will automatically delete 275 * corresponding media_grants. 276 * 277 * <p>The action is performed for only specific {@code user}.</p> 278 * 279 * @param packages the package(s) name to clear media grants for. 280 * @param reason a logged reason why the grants are being cleared. 281 * @param user the user for which the grants need to be modified. 282 * 283 * @return the number of grants removed. 284 */ 285 int removeAllMediaGrantsForPackages(String[] packages, String reason, @NonNull Integer user) 286 throws IllegalArgumentException { 287 Objects.requireNonNull(packages); 288 if (packages.length == 0) { 289 throw new IllegalArgumentException( 290 "Removing grants requires a non empty package name."); 291 } 292 293 final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); 294 queryBuilder.setDistinct(true); 295 queryBuilder.setTables(MEDIA_GRANTS_TABLE); 296 String[] selectionArgs = buildSelectionArg(queryBuilder, QueryFilterBuilder.newInstance() 297 .setPackageNameSelection(packages) 298 .setUserIdSelection(user) 299 .build()); 300 return mExternalDatabase.runWithTransaction( 301 (db) -> { 302 int grantsRemoved = queryBuilder.delete(db, null, selectionArgs); 303 Log.d( 304 TAG, 305 String.format( 306 "Removed %s media_grants for %s user for %s. Reason: %s", 307 grantsRemoved, 308 String.valueOf(user), 309 Arrays.toString(packages), 310 reason)); 311 return grantsRemoved; 312 }); 313 } 314 315 /** 316 * Removes all existing media grants for all packages from the external database. This will not 317 * alter the files or file metadata themselves. 318 * 319 * @return the number of grants removed. 320 */ 321 int removeAllMediaGrants() { 322 return mExternalDatabase.runWithTransaction( 323 (db) -> { 324 int grantsRemoved = mQueryBuilder.delete(db, null, null); 325 Log.d(TAG, String.format("Removed %d existing media_grants", grantsRemoved)); 326 return grantsRemoved; 327 }); 328 } 329 330 /** 331 * Validates an incoming Uri to see if it's a valid media/picker uri that follows the {@link 332 * MediaProvider#PICKER_ID scheme} 333 * 334 * @return If the uri is a valid media/picker uri. 335 */ 336 private boolean isPickerUri(Uri uri) { 337 return mUriMatcher.matchUri(uri, /* allowHidden= */ false) == PICKER_ID; 338 } 339 340 /** 341 * Verifies if a URI is eligible for a media_grant. 342 * 343 * <p>Currently {@code MediaGrants} requires the file's id to be a local file. 344 * 345 * <p>This checks if the provided Uri is: 346 * 347 * <ol> 348 * <li>A Photopicker Uri 349 * <li>That the authority is the local picker authority and not a cloud provider. 350 * </ol> 351 * 352 * <p> 353 * 354 * @param uri the uri to validate 355 * @return is Allowed - true if the given Uri is supported by MediaProvider's media_grants. 356 */ 357 private boolean isUriAllowed(Uri uri) { 358 359 return isPickerUri(uri) 360 && PickerUriResolver.unwrapProviderUri(uri) 361 .getHost() 362 .equals(PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY); 363 } 364 365 /** 366 * Add required selection arguments like comparisons and WHERE checks to the 367 * {@link SQLiteQueryBuilder} qb. 368 * 369 * @param qb query builder on which the conditions/filters needs to be applied. 370 * @param queryFilter representing the types of selection arguments to be applied. 371 * @return array of selection args used to replace placeholders in query builder conditions. 372 */ 373 private String[] buildSelectionArg(SQLiteQueryBuilder qb, MediaGrantsQueryFilter queryFilter) { 374 List<String> selectArgs = new ArrayList<>(); 375 // Append where clause for package names. 376 if (queryFilter.mPackageNames != null && queryFilter.mPackageNames.length > 0) { 377 // Append the where clause for package name selection to the query builder. 378 qb.appendWhereStandalone( 379 WHERE_MEDIA_GRANTS_PACKAGE_NAME_IN + buildPlaceholderForWhereClause( 380 queryFilter.mPackageNames.length)); 381 382 // Add package names to selection args. 383 selectArgs.addAll(Arrays.asList(queryFilter.mPackageNames)); 384 } 385 386 // Append Where clause for Uris 387 if (queryFilter.mUris != null && !queryFilter.mUris.isEmpty()) { 388 // Append the where clause for local id selection to the query builder. 389 // this query would look like this example query: 390 // WHERE EXISTS (SELECT 1 from temp_table_for_media_grants_deletion WHERE 391 // temp_table_for_media_grants_deletion.file_id = media_grants.file_id) 392 qb.appendWhereStandalone(String.format("EXISTS (SELECT %s from %s WHERE %s = %s)", 393 TEMP_TABLE_FOR_DELETION_FILE_ID_COLUMN_NAME, 394 TEMP_TABLE_NAME_FOR_DELETION, 395 TEMP_TABLE_FOR_DELETION_FILE_ID_COLUMN_NAME, 396 MediaGrants.MEDIA_GRANTS_TABLE + "." + MediaGrants.FILE_ID_COLUMN)); 397 } 398 399 // Append where clause for userID. 400 if (queryFilter.mUserId != null) { 401 qb.appendWhereStandalone(WHERE_MEDIA_GRANTS_USER_ID); 402 selectArgs.add(String.valueOf(queryFilter.mUserId)); 403 } 404 405 if (queryFilter.mIsNotTrashed) { 406 qb.appendWhereStandalone(WHERE_ITEM_IS_NOT_TRASHED); 407 selectArgs.add(ARG_VALUE_FOR_FALSE); 408 } 409 410 if (queryFilter.mIsNotPending) { 411 qb.appendWhereStandalone(WHERE_ITEM_IS_NOT_PENDING); 412 selectArgs.add(ARG_VALUE_FOR_FALSE); 413 } 414 415 if (queryFilter.mIsOnlyVisualMediaType) { 416 qb.appendWhereStandalone(WHERE_MEDIA_TYPE + buildPlaceholderForWhereClause( 417 VISUAL_MEDIA_TYPE_COUNT)); 418 selectArgs.add(String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE)); 419 selectArgs.add(String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO)); 420 } 421 422 if (queryFilter.mAvailableVolumes != null && queryFilter.mAvailableVolumes.length > 0) { 423 qb.appendWhereStandalone( 424 WHERE_VOLUME_NAME_IN + buildPlaceholderForWhereClause( 425 queryFilter.mAvailableVolumes.length)); 426 selectArgs.addAll(Arrays.asList(queryFilter.mAvailableVolumes)); 427 } 428 429 addMimeTypesToQueryBuilderAndSelectionArgs(qb, selectArgs, queryFilter.mMimeTypeSelection); 430 431 return selectArgs.toArray(new String[selectArgs.size()]); 432 } 433 434 private void addMimeTypesToQueryBuilderAndSelectionArgs(SQLiteQueryBuilder qb, 435 List<String> selectionArgs, String[] mimeTypes) { 436 if (mimeTypes == null) { 437 return; 438 } 439 440 mimeTypes = replaceMatchAnyChar(mimeTypes); 441 ArrayList<String> whereMimeTypes = new ArrayList<>(); 442 for (String mimeType : mimeTypes) { 443 if (!TextUtils.isEmpty(mimeType)) { 444 whereMimeTypes.add(WHERE_MIME_TYPE); 445 selectionArgs.add(mimeType); 446 } 447 } 448 449 if (whereMimeTypes.isEmpty()) { 450 return; 451 } 452 qb.appendWhereStandalone(TextUtils.join(" OR ", whereMimeTypes)); 453 } 454 455 private String buildPlaceholderForWhereClause(int numberOfItemsInSelection) { 456 StringBuilder placeholder = new StringBuilder("("); 457 for (int itr = 0; itr < numberOfItemsInSelection; itr++) { 458 placeholder.append("?,"); 459 } 460 placeholder.deleteCharAt(placeholder.length() - 1); 461 placeholder.append(")"); 462 return placeholder.toString(); 463 } 464 465 static final class MediaGrantsQueryFilter { 466 467 private final List<Uri> mUris; 468 private final String[] mPackageNames; 469 private final Integer mUserId; 470 471 private final boolean mIsNotTrashed; 472 473 private final boolean mIsNotPending; 474 475 private final boolean mIsOnlyVisualMediaType; 476 private final String[] mMimeTypeSelection; 477 478 private final String[] mAvailableVolumes; 479 480 MediaGrantsQueryFilter(QueryFilterBuilder builder) { 481 this.mUris = builder.mUris; 482 this.mPackageNames = builder.mPackageNames; 483 this.mUserId = builder.mUserId; 484 this.mIsNotTrashed = builder.mIsNotTrashed; 485 this.mIsNotPending = builder.mIsNotPending; 486 this.mMimeTypeSelection = builder.mMimeTypeSelection; 487 this.mIsOnlyVisualMediaType = builder.mIsOnlyVisualMediaType; 488 this.mAvailableVolumes = builder.mAvailableVolumes; 489 } 490 } 491 492 // Static class Builder 493 static class QueryFilterBuilder { 494 495 private List<Uri> mUris; 496 private String[] mPackageNames; 497 private int mUserId; 498 499 private boolean mIsNotTrashed; 500 501 private boolean mIsNotPending; 502 503 private boolean mIsOnlyVisualMediaType; 504 private String[] mMimeTypeSelection; 505 506 private String[] mAvailableVolumes; 507 508 public static QueryFilterBuilder newInstance() { 509 return new QueryFilterBuilder(); 510 } 511 512 private QueryFilterBuilder() {} 513 514 // Setter methods 515 public QueryFilterBuilder setUriSelection(List<Uri> uris) { 516 this.mUris = uris; 517 return this; 518 } 519 520 public QueryFilterBuilder setPackageNameSelection(String[] packageNames) { 521 this.mPackageNames = packageNames; 522 return this; 523 } 524 525 public QueryFilterBuilder setUserIdSelection(int userId) { 526 this.mUserId = userId; 527 return this; 528 } 529 530 public QueryFilterBuilder setIsNotTrashedSelection(boolean isNotTrashed) { 531 this.mIsNotTrashed = isNotTrashed; 532 return this; 533 } 534 535 public QueryFilterBuilder setIsNotPendingSelection(boolean isNotPending) { 536 this.mIsNotPending = isNotPending; 537 return this; 538 } 539 540 public QueryFilterBuilder setIsOnlyVisualMediaType(boolean isOnlyVisualMediaType) { 541 this.mIsOnlyVisualMediaType = isOnlyVisualMediaType; 542 return this; 543 } 544 545 public QueryFilterBuilder setMimeTypeSelection(String[] mimeTypeSelection) { 546 this.mMimeTypeSelection = mimeTypeSelection; 547 return this; 548 } 549 550 public QueryFilterBuilder setAvailableVolumes(String[] availableVolumes) { 551 this.mAvailableVolumes = availableVolumes; 552 return this; 553 } 554 555 // build method to deal with outer class 556 // to return outer instance 557 public MediaGrantsQueryFilter build() { 558 return new MediaGrantsQueryFilter(this); 559 } 560 } 561 } 562