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