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