• 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 android.Manifest.permission.ACCESS_MEDIA_LOCATION;
20 import static android.Manifest.permission.QUERY_ALL_PACKAGES;
21 import static android.app.AppOpsManager.permissionToOp;
22 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
23 import static android.app.PendingIntent.FLAG_IMMUTABLE;
24 import static android.app.PendingIntent.FLAG_ONE_SHOT;
25 import static android.content.ContentResolver.QUERY_ARG_SQL_GROUP_BY;
26 import static android.content.ContentResolver.QUERY_ARG_SQL_HAVING;
27 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION;
28 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS;
29 import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER;
30 import static android.content.pm.PackageManager.PERMISSION_DENIED;
31 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
32 import static android.database.Cursor.FIELD_TYPE_BLOB;
33 import static android.provider.CloudMediaProviderContract.EXTRA_ASYNC_CONTENT_PROVIDER;
34 import static android.provider.CloudMediaProviderContract.METHOD_GET_ASYNC_CONTENT_PROVIDER;
35 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE;
36 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE;
37 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT;
38 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE;
39 import static android.provider.MediaStore.GET_BACKUP_FILES;
40 import static android.provider.MediaStore.MATCH_DEFAULT;
41 import static android.provider.MediaStore.MATCH_EXCLUDE;
42 import static android.provider.MediaStore.MATCH_INCLUDE;
43 import static android.provider.MediaStore.MATCH_ONLY;
44 import static android.provider.MediaStore.MEDIA_IGNORE_FILENAME;
45 import static android.provider.MediaStore.MY_UID;
46 import static android.provider.MediaStore.MediaColumns.OWNER_PACKAGE_NAME;
47 import static android.provider.MediaStore.PER_USER_RANGE;
48 import static android.provider.MediaStore.QUERY_ARG_DEFER_SCAN;
49 import static android.provider.MediaStore.QUERY_ARG_MATCH_FAVORITE;
50 import static android.provider.MediaStore.QUERY_ARG_MATCH_PENDING;
51 import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED;
52 import static android.provider.MediaStore.QUERY_ARG_REDACTED_URI;
53 import static android.provider.MediaStore.QUERY_ARG_RELATED_URI;
54 import static android.provider.MediaStore.READ_BACKED_UP_FILE_PATHS;
55 import static android.provider.MediaStore.getVolumeName;
56 import static android.system.OsConstants.F_GETFL;
57 
58 import static com.android.providers.media.AccessChecker.getWhereForConstrainedAccess;
59 import static com.android.providers.media.AccessChecker.getWhereForOwnerPackageMatch;
60 import static com.android.providers.media.AccessChecker.getWhereForUserSelectedAccess;
61 import static com.android.providers.media.AccessChecker.hasAccessToCollection;
62 import static com.android.providers.media.AccessChecker.hasUserSelectedAccess;
63 import static com.android.providers.media.DatabaseBackupAndRecovery.LEVEL_DB_READ_LIMIT;
64 import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME;
65 import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
66 import static com.android.providers.media.LocalCallingIdentity.APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID;
67 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_ACCESS_MTP;
68 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_INSTALL_PACKAGES;
69 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_DELEGATOR;
70 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED;
71 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_READ;
72 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE;
73 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_MANAGER;
74 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED;
75 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SELF;
76 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SHELL;
77 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SYSTEM_GALLERY;
78 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_EXTERNAL_STORAGE;
79 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART;
80 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART_FILE_ID;
81 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART_ID;
82 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMS;
83 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMS_ID;
84 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS;
85 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS_ID;
86 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS_ID_ALBUMS;
87 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES;
88 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ALL_MEMBERS;
89 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ID;
90 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ID_MEMBERS;
91 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA;
92 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID;
93 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID_GENRES;
94 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID_GENRES_ID;
95 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS;
96 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID;
97 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID_MEMBERS;
98 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID_MEMBERS_ID;
99 import static com.android.providers.media.LocalUriMatcher.CLI;
100 import static com.android.providers.media.LocalUriMatcher.DOWNLOADS;
101 import static com.android.providers.media.LocalUriMatcher.DOWNLOADS_ID;
102 import static com.android.providers.media.LocalUriMatcher.FILES;
103 import static com.android.providers.media.LocalUriMatcher.FILES_ID;
104 import static com.android.providers.media.LocalUriMatcher.FS_ID;
105 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA;
106 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA_ID;
107 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA_ID_THUMBNAIL;
108 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS;
109 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS_ID;
110 import static com.android.providers.media.LocalUriMatcher.MEDIA_SCANNER;
111 import static com.android.providers.media.LocalUriMatcher.PICKER_ID;
112 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_ALBUMS_ALL;
113 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_ALBUMS_LOCAL;
114 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_MEDIA_ALL;
115 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_MEDIA_LOCAL;
116 import static com.android.providers.media.LocalUriMatcher.VERSION;
117 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA;
118 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA_ID;
119 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA_ID_THUMBNAIL;
120 import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS;
121 import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS_ID;
122 import static com.android.providers.media.LocalUriMatcher.VOLUMES;
123 import static com.android.providers.media.LocalUriMatcher.VOLUMES_ID;
124 import static com.android.providers.media.PickerUriResolver.getMediaUri;
125 import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND;
126 import static com.android.providers.media.scan.MediaScanner.REASON_IDLE;
127 import static com.android.providers.media.util.DatabaseUtils.bindList;
128 import static com.android.providers.media.util.FileUtils.DEFAULT_FOLDER_NAMES;
129 import static com.android.providers.media.util.FileUtils.PATTERN_PENDING_FILEPATH_FOR_SQL;
130 import static com.android.providers.media.util.FileUtils.buildPrimaryVolumeFile;
131 import static com.android.providers.media.util.FileUtils.extractDisplayName;
132 import static com.android.providers.media.util.FileUtils.extractFileExtension;
133 import static com.android.providers.media.util.FileUtils.extractFileName;
134 import static com.android.providers.media.util.FileUtils.extractOwnerPackageNameFromRelativePath;
135 import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName;
136 import static com.android.providers.media.util.FileUtils.extractRelativePath;
137 import static com.android.providers.media.util.FileUtils.extractRelativePathWithDisplayName;
138 import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
139 import static com.android.providers.media.util.FileUtils.extractVolumeName;
140 import static com.android.providers.media.util.FileUtils.extractVolumePath;
141 import static com.android.providers.media.util.FileUtils.fromFuseFile;
142 import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath;
143 import static com.android.providers.media.util.FileUtils.isCrossUserEnabled;
144 import static com.android.providers.media.util.FileUtils.isDataOrObbPath;
145 import static com.android.providers.media.util.FileUtils.isDataOrObbRelativePath;
146 import static com.android.providers.media.util.FileUtils.isDownload;
147 import static com.android.providers.media.util.FileUtils.isExternalMediaDirectory;
148 import static com.android.providers.media.util.FileUtils.isObbOrChildRelativePath;
149 import static com.android.providers.media.util.FileUtils.sanitizePath;
150 import static com.android.providers.media.util.FileUtils.toFuseFile;
151 import static com.android.providers.media.util.Logging.LOGV;
152 import static com.android.providers.media.util.Logging.TAG;
153 import static com.android.providers.media.util.PermissionUtils.checkPermissionSelf;
154 import static com.android.providers.media.util.PermissionUtils.checkPermissionShell;
155 import static com.android.providers.media.util.StringUtils.componentStateToString;
156 import static com.android.providers.media.util.SyntheticPathUtils.REDACTED_URI_ID_PREFIX;
157 import static com.android.providers.media.util.SyntheticPathUtils.REDACTED_URI_ID_SIZE;
158 import static com.android.providers.media.util.SyntheticPathUtils.createSparseFile;
159 import static com.android.providers.media.util.SyntheticPathUtils.extractSyntheticRelativePathSegements;
160 import static com.android.providers.media.util.SyntheticPathUtils.getRedactedRelativePath;
161 import static com.android.providers.media.util.SyntheticPathUtils.isPickerPath;
162 import static com.android.providers.media.util.SyntheticPathUtils.isRedactedPath;
163 import static com.android.providers.media.util.SyntheticPathUtils.isSyntheticPath;
164 
165 import android.Manifest;
166 import android.annotation.IntDef;
167 import android.app.AppOpsManager;
168 import android.app.AppOpsManager.OnOpActiveChangedListener;
169 import android.app.AppOpsManager.OnOpChangedListener;
170 import android.app.DownloadManager;
171 import android.app.PendingIntent;
172 import android.app.RecoverableSecurityException;
173 import android.app.RemoteAction;
174 import android.app.compat.CompatChanges;
175 import android.compat.annotation.ChangeId;
176 import android.compat.annotation.EnabledAfter;
177 import android.content.BroadcastReceiver;
178 import android.content.ClipData;
179 import android.content.ClipDescription;
180 import android.content.ComponentName;
181 import android.content.ContentProvider;
182 import android.content.ContentProviderClient;
183 import android.content.ContentProviderOperation;
184 import android.content.ContentProviderResult;
185 import android.content.ContentResolver;
186 import android.content.ContentUris;
187 import android.content.ContentValues;
188 import android.content.Context;
189 import android.content.Intent;
190 import android.content.IntentFilter;
191 import android.content.OperationApplicationException;
192 import android.content.SharedPreferences;
193 import android.content.pm.ApplicationInfo;
194 import android.content.pm.PackageInstaller.SessionInfo;
195 import android.content.pm.PackageManager;
196 import android.content.pm.PackageManager.NameNotFoundException;
197 import android.content.pm.PermissionGroupInfo;
198 import android.content.pm.ProviderInfo;
199 import android.content.res.AssetFileDescriptor;
200 import android.content.res.Configuration;
201 import android.content.res.Resources;
202 import android.database.Cursor;
203 import android.database.MatrixCursor;
204 import android.database.sqlite.SQLiteConstraintException;
205 import android.database.sqlite.SQLiteDatabase;
206 import android.graphics.Bitmap;
207 import android.graphics.BitmapFactory;
208 import android.graphics.drawable.Icon;
209 import android.icu.util.ULocale;
210 import android.media.ExifInterface;
211 import android.media.ThumbnailUtils;
212 import android.mtp.MtpConstants;
213 import android.net.Uri;
214 import android.os.Binder;
215 import android.os.Binder.ProxyTransactListener;
216 import android.os.Build;
217 import android.os.Bundle;
218 import android.os.CancellationSignal;
219 import android.os.Environment;
220 import android.os.IBinder;
221 import android.os.ParcelFileDescriptor;
222 import android.os.ParcelFileDescriptor.OnCloseListener;
223 import android.os.Parcelable;
224 import android.os.Process;
225 import android.os.RemoteException;
226 import android.os.SystemClock;
227 import android.os.Trace;
228 import android.os.UserHandle;
229 import android.os.UserManager;
230 import android.os.storage.StorageManager;
231 import android.os.storage.StorageManager.StorageVolumeCallback;
232 import android.os.storage.StorageVolume;
233 import android.preference.PreferenceManager;
234 import android.provider.AsyncContentProvider;
235 import android.provider.BaseColumns;
236 import android.provider.Column;
237 import android.provider.DocumentsContract;
238 import android.provider.ExportedSince;
239 import android.provider.IAsyncContentProvider;
240 import android.provider.MediaStore;
241 import android.provider.MediaStore.Audio;
242 import android.provider.MediaStore.Audio.AudioColumns;
243 import android.provider.MediaStore.Audio.Playlists;
244 import android.provider.MediaStore.Downloads;
245 import android.provider.MediaStore.Files;
246 import android.provider.MediaStore.Files.FileColumns;
247 import android.provider.MediaStore.Images;
248 import android.provider.MediaStore.Images.ImageColumns;
249 import android.provider.MediaStore.MediaColumns;
250 import android.provider.MediaStore.Video;
251 import android.system.ErrnoException;
252 import android.system.Os;
253 import android.system.OsConstants;
254 import android.system.StructStat;
255 import android.text.TextUtils;
256 import android.text.format.DateUtils;
257 import android.util.ArrayMap;
258 import android.util.ArraySet;
259 import android.util.DisplayMetrics;
260 import android.util.Log;
261 import android.util.LongSparseArray;
262 import android.util.Pair;
263 import android.util.Size;
264 import android.util.SparseArray;
265 import android.webkit.MimeTypeMap;
266 
267 import androidx.annotation.GuardedBy;
268 import androidx.annotation.Keep;
269 import androidx.annotation.NonNull;
270 import androidx.annotation.Nullable;
271 import androidx.annotation.RequiresApi;
272 import androidx.annotation.VisibleForTesting;
273 
274 import com.android.modules.utils.BackgroundThread;
275 import com.android.modules.utils.build.SdkLevel;
276 import com.android.providers.media.DatabaseHelper.OnFilesChangeListener;
277 import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener;
278 import com.android.providers.media.dao.FileRow;
279 import com.android.providers.media.fuse.ExternalStorageServiceImpl;
280 import com.android.providers.media.fuse.FuseDaemon;
281 import com.android.providers.media.metrics.PulledMetrics;
282 import com.android.providers.media.photopicker.PhotoPickerActivity;
283 import com.android.providers.media.photopicker.PickerDataLayer;
284 import com.android.providers.media.photopicker.PickerSyncController;
285 import com.android.providers.media.photopicker.data.ExternalDbFacade;
286 import com.android.providers.media.photopicker.data.PickerDbFacade;
287 import com.android.providers.media.playlist.Playlist;
288 import com.android.providers.media.scan.MediaScanner;
289 import com.android.providers.media.scan.MediaScanner.ScanReason;
290 import com.android.providers.media.scan.ModernMediaScanner;
291 import com.android.providers.media.util.CachedSupplier;
292 import com.android.providers.media.util.DatabaseUtils;
293 import com.android.providers.media.util.FileUtils;
294 import com.android.providers.media.util.ForegroundThread;
295 import com.android.providers.media.util.IsoInterface;
296 import com.android.providers.media.util.Logging;
297 import com.android.providers.media.util.LongArray;
298 import com.android.providers.media.util.Metrics;
299 import com.android.providers.media.util.MimeUtils;
300 import com.android.providers.media.util.PermissionUtils;
301 import com.android.providers.media.util.Preconditions;
302 import com.android.providers.media.util.SQLiteQueryBuilder;
303 import com.android.providers.media.util.SpecialFormatDetector;
304 import com.android.providers.media.util.StringUtils;
305 import com.android.providers.media.util.UserCache;
306 import com.android.providers.media.util.XAttrUtils;
307 import com.android.providers.media.util.XmpInterface;
308 
309 import com.google.common.base.Strings;
310 import com.google.common.hash.Hashing;
311 
312 import java.io.File;
313 import java.io.FileDescriptor;
314 import java.io.FileInputStream;
315 import java.io.FileNotFoundException;
316 import java.io.FileOutputStream;
317 import java.io.IOException;
318 import java.io.OutputStream;
319 import java.io.PrintWriter;
320 import java.lang.annotation.Retention;
321 import java.lang.annotation.RetentionPolicy;
322 import java.lang.reflect.InvocationTargetException;
323 import java.lang.reflect.Method;
324 import java.nio.charset.StandardCharsets;
325 import java.nio.file.Path;
326 import java.util.ArrayList;
327 import java.util.Arrays;
328 import java.util.Collection;
329 import java.util.Collections;
330 import java.util.HashSet;
331 import java.util.LinkedHashMap;
332 import java.util.List;
333 import java.util.Locale;
334 import java.util.Map;
335 import java.util.Objects;
336 import java.util.Optional;
337 import java.util.Set;
338 import java.util.UUID;
339 import java.util.concurrent.CountDownLatch;
340 import java.util.concurrent.ExecutionException;
341 import java.util.concurrent.TimeUnit;
342 import java.util.concurrent.TimeoutException;
343 import java.util.function.Consumer;
344 import java.util.function.Supplier;
345 import java.util.function.UnaryOperator;
346 import java.util.regex.Matcher;
347 import java.util.regex.Pattern;
348 import java.util.stream.Collectors;
349 
350 /**
351  * Media content provider. See {@link android.provider.MediaStore} for details.
352  * Separate databases are kept for each external storage card we see (using the
353  * card's ID as an index).  The content visible at content://media/external/...
354  * changes with the card.
355  */
356 public class MediaProvider extends ContentProvider {
357     /**
358      * Enables checks to stop apps from inserting and updating to private files via media provider.
359      */
360     @ChangeId
361     @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.R)
362     static final long ENABLE_CHECKS_FOR_PRIVATE_FILES = 172100307L;
363 
364     /**
365      * Regex of a selection string that matches a specific ID.
366      */
367     static final Pattern PATTERN_SELECTION_ID = Pattern.compile(
368             "(?:image_id|video_id)\\s*=\\s*(\\d+)");
369 
370     /** File access by uid requires the transcoding transform */
371     private static final int FLAG_TRANSFORM_TRANSCODING = 1 << 0;
372 
373     /** File access by uid is a synthetic path corresponding to a redacted URI */
374     private static final int FLAG_TRANSFORM_REDACTION = 1 << 1;
375 
376     /** File access by uid is a synthetic path corresponding to a picker URI */
377     private static final int FLAG_TRANSFORM_PICKER = 1 << 2;
378 
379     /**
380      * These directory names aren't declared in Environment as final variables, and so we need to
381      * have the same values in separate final variables in order to have them considered constant
382      * expressions.
383      * These directory names are intentionally in lower case to ease the case insensitive path
384      * comparison.
385      */
386     private static final String DIRECTORY_MUSIC_LOWER_CASE = "music";
387     private static final String DIRECTORY_PODCASTS_LOWER_CASE = "podcasts";
388     private static final String DIRECTORY_RINGTONES_LOWER_CASE = "ringtones";
389     private static final String DIRECTORY_ALARMS_LOWER_CASE = "alarms";
390     private static final String DIRECTORY_NOTIFICATIONS_LOWER_CASE = "notifications";
391     private static final String DIRECTORY_PICTURES_LOWER_CASE = "pictures";
392     private static final String DIRECTORY_MOVIES_LOWER_CASE = "movies";
393     private static final String DIRECTORY_DOWNLOADS_LOWER_CASE = "download";
394     private static final String DIRECTORY_DCIM_LOWER_CASE = "dcim";
395     private static final String DIRECTORY_DOCUMENTS_LOWER_CASE = "documents";
396     private static final String DIRECTORY_AUDIOBOOKS_LOWER_CASE = "audiobooks";
397     private static final String DIRECTORY_RECORDINGS_LOWER_CASE = "recordings";
398     private static final String DIRECTORY_ANDROID_LOWER_CASE = "android";
399 
400     private static final String DIRECTORY_MEDIA = "media";
401     private static final String DIRECTORY_THUMBNAILS = ".thumbnails";
402 
403     /**
404      * Hard-coded filename where the current value of
405      * {@link DatabaseHelper#getOrCreateUuid} is persisted on a physical SD card
406      * to help identify stale thumbnail collections.
407      */
408     private static final String FILE_DATABASE_UUID = ".database_uuid";
409 
410     /**
411      * Specify what default directories the caller gets full access to. By default, the caller
412      * shouldn't get full access to any default dirs.
413      * But for example, we do an exception for System Gallery apps and allow them full access to:
414      * DCIM, Pictures, Movies.
415      */
416     static final String INCLUDED_DEFAULT_DIRECTORIES =
417             "android:included-default-directories";
418 
419     /**
420      * Value indicating that operations should include database rows matching the criteria defined
421      * by this key only when calling package has write permission to the database row or column is
422      * {@column MediaColumns#IS_PENDING} and is set by FUSE.
423      * <p>
424      * Note that items <em>not</em> matching the criteria will also be included, and as part of this
425      * match no additional write permission checks are carried out for those items.
426      */
427     private static final int MATCH_VISIBLE_FOR_FILEPATH = 32;
428 
429     private static final int NON_HIDDEN_CACHE_SIZE = 50;
430 
431     /**
432      * This is required as idle maintenance maybe stopped anytime; we do not want to query
433      * and accumulate values to update for a long time, instead we want to batch query and update
434      * by a limited number.
435      */
436     private static final int IDLE_MAINTENANCE_ROWS_LIMIT = 1000;
437 
438     /**
439      * Where clause to match pending files from FUSE. Pending files from FUSE will not have
440      * PATTERN_PENDING_FILEPATH_FOR_SQL pattern.
441      */
442     private static final String MATCH_PENDING_FROM_FUSE = String.format("lower(%s) NOT REGEXP '%s'",
443             MediaColumns.DATA, PATTERN_PENDING_FILEPATH_FOR_SQL);
444 
445     /**
446      * This flag is replaced with {@link MediaStore#QUERY_ARG_DEFER_SCAN} from S onwards and only
447      * kept around for app compatibility in R.
448      */
449     private static final String QUERY_ARG_DO_ASYNC_SCAN = "android:query-arg-do-async-scan";
450 
451     /**
452      * Time between two polling attempts for availability of FuseDaemon thread.
453      */
454     private static final long POLLING_TIME_IN_MILLIS = 100;
455 
456     /**
457      * Enable option to defer the scan triggered as part of MediaProvider#update()
458      */
459     @ChangeId
460     @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.R)
461     static final long ENABLE_DEFERRED_SCAN = 180326732L;
462 
463     /**
464      * Enable option to include database rows of files from recently unmounted
465      * volume in MediaProvider#query
466      */
467     @ChangeId
468     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R)
469     static final long ENABLE_INCLUDE_ALL_VOLUMES = 182734110L;
470 
471     /**
472      * Set of {@link Cursor} columns that refer to raw filesystem paths.
473      */
474     private static final ArrayMap<String, Object> sDataColumns = new ArrayMap<>();
475 
476     static {
sDataColumns.put(MediaStore.MediaColumns.DATA, null)477         sDataColumns.put(MediaStore.MediaColumns.DATA, null);
sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null)478         sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null);
sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null)479         sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null);
sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null)480         sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null);
sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null)481         sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null);
482     }
483 
484     private static final int sUserId = UserHandle.myUserId();
485 
486     /**
487      * Please use {@link getDownloadsProviderAuthority()} instead of using this directly.
488      */
489     private static final String DOWNLOADS_PROVIDER_AUTHORITY = "downloads";
490 
491     @GuardedBy("mPendingOpenInfo")
492     private final Map<Integer, PendingOpenInfo> mPendingOpenInfo = new ArrayMap<>();
493 
494     @GuardedBy("mNonHiddenPaths")
495     private final LRUCache<String, Integer> mNonHiddenPaths = new LRUCache<>(NON_HIDDEN_CACHE_SIZE);
496 
updateVolumes()497     public void updateVolumes() {
498         mVolumeCache.update();
499         // Update filters to reflect mounted volumes so users don't get
500         // confused by metadata from ejected volumes
501         ForegroundThread.getExecutor().execute(() -> {
502             mExternalDatabase.setFilterVolumeNames(mVolumeCache.getExternalVolumeNames());
503         });
504     }
505 
506     @NonNull
getVolume(@onNull String volumeName)507     public MediaVolume getVolume(@NonNull String volumeName) throws FileNotFoundException {
508         return mVolumeCache.findVolume(volumeName, mCallingIdentity.get().getUser());
509     }
510 
511     @NonNull
getVolumePath(@onNull String volumeName)512     public File getVolumePath(@NonNull String volumeName) throws FileNotFoundException {
513         // Ugly hack to keep unit tests passing, where we don't always have a
514         // Context to discover volumes with
515         if (getContext() == null) {
516             return Environment.getExternalStorageDirectory();
517         }
518 
519         return mVolumeCache.getVolumePath(volumeName, mCallingIdentity.get().getUser());
520     }
521 
522     @NonNull
getAllowedVolumePaths(String volumeName)523     private Collection<File> getAllowedVolumePaths(String volumeName)
524             throws FileNotFoundException {
525         // This method is used to verify whether a path belongs to a certain volume name;
526         // we can't always use the calling user's identity here to determine exactly which
527         // volume is meant, because the MediaScanner may scan paths belonging to another user,
528         // eg a clone user.
529         // So, for volumes like external_primary, just return allowed paths for all users.
530         List<UserHandle> users = mUserCache.getUsersCached();
531         ArrayList<File> allowedPaths = new ArrayList<>();
532         for (UserHandle user : users) {
533             try {
534                 Collection<File> volumeScanPaths = mVolumeCache.getVolumeScanPaths(volumeName,
535                         user);
536                 allowedPaths.addAll(volumeScanPaths);
537             } catch (FileNotFoundException e) {
538                 Log.e(TAG, volumeName + " has no associated path for user: " + user);
539             }
540         }
541 
542         return allowedPaths;
543     }
544 
545     /**
546      * Frees any cache held by MediaProvider.
547      *
548      * @param bytes number of bytes which need to be freed
549      */
freeCache(long bytes)550     public void freeCache(long bytes) {
551         mTranscodeHelper.freeCache(bytes);
552     }
553 
onAnrDelayStarted(@onNull String packageName, int uid, int tid, int reason)554     public void onAnrDelayStarted(@NonNull String packageName, int uid, int tid, int reason) {
555         mTranscodeHelper.onAnrDelayStarted(packageName, uid, tid, reason);
556     }
557 
558     private volatile Locale mLastLocale = Locale.getDefault();
559 
560     private StorageManager mStorageManager;
561     private PackageManager mPackageManager;
562     private UserManager mUserManager;
563     private PickerUriResolver mPickerUriResolver;
564 
565     private UserCache mUserCache;
566     private VolumeCache mVolumeCache;
567 
568     private int mExternalStorageAuthorityAppId;
569     private int mDownloadsAuthorityAppId;
570     private Size mThumbSize;
571 
572     /**
573      * Map from UID to cached {@link LocalCallingIdentity}. Values are only
574      * maintained in this map while the UID is actively working with a
575      * performance-critical component, such as camera.
576      */
577     @GuardedBy("mCachedCallingIdentity")
578     private final SparseArray<LocalCallingIdentity> mCachedCallingIdentity = new SparseArray<>();
579 
580     private final OnOpActiveChangedListener mActiveListener = (code, uid, packageName, active) -> {
581         synchronized (mCachedCallingIdentity) {
582             if (active) {
583                 // TODO moltmann: Set correct featureId
584                 mCachedCallingIdentity.put(uid,
585                         LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid,
586                             packageName, null));
587             } else {
588                 mCachedCallingIdentity.remove(uid);
589             }
590         }
591     };
592 
593     /**
594      * Map from UID to cached {@link LocalCallingIdentity}. Values are only
595      * maintained in this map until there's any change in the appops needed or packages
596      * used in the {@link LocalCallingIdentity}.
597      */
598     @GuardedBy("mCachedCallingIdentityForFuse")
599     private final SparseArray<LocalCallingIdentity> mCachedCallingIdentityForFuse =
600             new SparseArray<>();
601 
602     private final OnOpChangedListener mModeListener = new OnOpChangedListener() {
603 
604         /**
605          * Callback method called as part of {@link OnOpChangedListener}.
606          * Calls {@link #onOpChanged(String, String, int)} with cached userId(s).
607          *
608          * @param packageName - package for which AppOp changed
609          * @param op - AppOp for which the mode changed.
610          */
611         public void onOpChanged(String op, String packageName) {
612             // In case no userId is supplied, we drop grants for all cached users.
613             List<UserHandle> userHandles = mUserCache.getUsersCached();
614             for (UserHandle user : userHandles) {
615                 onOpChanged(op, packageName, user.getIdentifier());
616             }
617         }
618 
619         /**
620          * Callback method called as part of {@link OnOpChangedListener}.
621          * When an AppOp is written -
622          * 1. We invalidate saved LocalCallingIdentity object for the package. This
623          *    is needed to ensure we read the new permission state
624          * 2. If the AppOp change was on the read media appOps, we clear any stale
625          *    grants,
626          *
627          * @param packageName - package for which AppOp changed
628          * @param op - AppOp for which the mode changed.
629          * @param userId - userSpace where the package is located
630          */
631         public void onOpChanged(String op, String packageName, int userId) {
632             invalidateLocalCallingIdentityCache(packageName, "op " + op /* reason */);
633             removeMediaGrantsOnModeChange(packageName, op, userId);
634         }
635     };
636 
637     /**
638      * Removes media_grants for the given {@code packageName} and {@code userId} if the AppOp
639      * change resulted in a state of "Allow All" or "Deny All" for read
640      * permission.
641      */
removeMediaGrantsOnModeChange(String packageName, String op, int userId)642     private void removeMediaGrantsOnModeChange(String packageName, String op, int userId) {
643         // b/265963379: onModeChanged is always called with op=OPSTR_READ_EXTERNAL_STORAGE even if
644         // the appOp mode changed for other read media app ops. Handle all read media app op changes
645         // until the bug is fixed.
646         if (!SdkLevel.isAtLeastU() || !isReadMediaAppOp(op)) {
647             return;
648         }
649         Context context = getContext();
650         PackageManager packageManager = context.getPackageManager();
651         try {
652             int uid = packageManager.getPackageUidAsUser(packageName,
653                     PackageManager.PackageInfoFlags.of(0), userId);
654             if (!LocalCallingIdentity.fromExternal(context, mUserCache, uid)
655                     .checkCallingPermissionUserSelected()) {
656                 // Revoke media grants if permission state is not "Select flow".
657                 mMediaGrants.removeAllMediaGrantsForPackage(
658                         packageName,
659                         /*reason=*/ "Mode changed: " + op,
660                         userId);
661             }
662         } catch (NameNotFoundException e) {
663             Log.d(TAG, "Unable to resolve uid. Ignoring the AppOp change for "
664                     + packageName + ", User : " + userId);
665         }
666     }
667 
668     /**
669      * Returns {@code true} if the given {@code op} is one of the appOp
670      * related to read media appOps
671      */
isReadMediaAppOp(String op)672     private boolean isReadMediaAppOp(String op) {
673         return AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE.equals(op)
674                 || AppOpsManager.OPSTR_READ_MEDIA_IMAGES.equals(op)
675                 || AppOpsManager.OPSTR_READ_MEDIA_VIDEO.equals(op)
676                 || AppOpsManager.OPSTR_READ_MEDIA_VISUAL_USER_SELECTED.equals(op);
677     }
678 
679     /**
680      * Retrieves a cached calling identity or creates a new one. Also, always sets the app-op
681      * description for the calling identity.
682      */
getCachedCallingIdentityForFuse(int uid)683     private LocalCallingIdentity getCachedCallingIdentityForFuse(int uid) {
684         synchronized (mCachedCallingIdentityForFuse) {
685             PermissionUtils.setOpDescription("via FUSE");
686             LocalCallingIdentity identity = mCachedCallingIdentityForFuse.get(uid);
687             if (identity == null) {
688                identity = LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid);
689                if (uidToUserId(uid) == sUserId) {
690                    mCachedCallingIdentityForFuse.put(uid, identity);
691                } else {
692                    // In some app cloning designs, MediaProvider user 0 may
693                    // serve requests for apps running as a "clone" user; in
694                    // those cases, don't keep a cache for the clone user, since
695                    // we don't get any invalidation events for these users.
696                }
697             }
698             return identity;
699         }
700     }
701 
702     /**
703      * Calling identity state about on the current thread. Populated on demand,
704      * and invalidated by {@link #onCallingPackageChanged()} when each remote
705      * call is finished.
706      */
707     private final ThreadLocal<LocalCallingIdentity> mCallingIdentity = ThreadLocal
708             .withInitial(() -> {
709                 PermissionUtils.setOpDescription("via MediaProvider");
710                 synchronized (mCachedCallingIdentity) {
711                     final LocalCallingIdentity cached = mCachedCallingIdentity
712                             .get(Binder.getCallingUid());
713                     return (cached != null) ? cached
714                             : LocalCallingIdentity.fromBinder(getContext(), this, mUserCache);
715                 }
716             });
717 
718     /**
719      * We simply propagate the UID that is being tracked by
720      * {@link LocalCallingIdentity}, which means we accurately blame both
721      * incoming Binder calls and FUSE calls.
722      */
723     private final ProxyTransactListener mTransactListener = new ProxyTransactListener() {
724         @Override
725         public Object onTransactStarted(IBinder binder, int transactionCode) {
726             if (LOGV) Trace.beginSection(Thread.currentThread().getStackTrace()[5].getMethodName());
727             // Check if mCallindIdentity was created within a fuse or content provider transaction
728             if (mCallingIdentity.get().isValidProviderOrFuseCallingIdentity()) {
729                 return Binder.setCallingWorkSourceUid(mCallingIdentity.get().uid);
730             }
731             // If mCallingIdentity was not created for a fuse or content provider transaction,
732             // we should reset it, the next time it is retrieved it will be created for the
733             // appropriate caller.
734             mCallingIdentity.remove();
735             return Binder.setCallingWorkSourceUid(Binder.getCallingUid());
736         }
737 
738         @Override
739         public void onTransactEnded(Object session) {
740             final long token = (long) session;
741             Binder.restoreCallingWorkSource(token);
742             if (LOGV) Trace.endSection();
743         }
744     };
745 
746     // In memory cache of path<->id mappings, to speed up inserts during media scan
747     @GuardedBy("mDirectoryCache")
748     private final ArrayMap<String, Long> mDirectoryCache = new ArrayMap<>();
749 
750     private static final String[] sDataOnlyColumn = new String[] {
751         FileColumns.DATA
752     };
753 
754     private static final String ID_NOT_PARENT_CLAUSE =
755             "_id NOT IN (SELECT parent FROM files WHERE parent IS NOT NULL)";
756 
757     private static final String CANONICAL = "canonical";
758 
759     private static final String ALL_VOLUMES = "all_volumes";
760 
761     private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() {
762         @Override
763         public void onReceive(Context context, Intent intent) {
764             switch (intent.getAction()) {
765                 case Intent.ACTION_PACKAGE_REMOVED:
766                 case Intent.ACTION_PACKAGE_ADDED:
767                     Uri uri = intent.getData();
768                     String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
769                     if (pkg != null) {
770                         invalidateLocalCallingIdentityCache(pkg, "package " + intent.getAction());
771                         if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
772                             mUserCache.invalidateWorkProfileOwnerApps(pkg);
773                             mPickerSyncController.notifyPackageRemoval(pkg);
774                             invalidateDentryForExternalStorage(pkg);
775                         }
776                     } else {
777                         Log.w(TAG, "Failed to retrieve package from intent: " + intent.getAction());
778                     }
779                     break;
780             }
781         }
782     };
783 
invalidateDentryForExternalStorage(String packageName)784     private void invalidateDentryForExternalStorage(String packageName) {
785         for (MediaVolume vol : mVolumeCache.getExternalVolumes()) {
786             try {
787                 invalidateFuseDentry(String.format(Locale.ROOT,
788                         "%s/Android/media/%s/", getVolumePath(vol.getName()).getAbsolutePath(),
789                         packageName));
790             } catch (FileNotFoundException e) {
791                 Log.e(TAG, "External volume path not found for " + vol.getName(), e);
792             }
793         }
794     }
795 
796     private final BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
797         @Override
798         public void onReceive(Context context, Intent intent) {
799             switch (intent.getAction()) {
800                 case Intent.ACTION_USER_REMOVED:
801                     /**
802                      * Removing media files for user being deleted. This would impact if the deleted
803                      * user have been using same MediaProvider as the current user i.e. when
804                      * isMediaSharedWithParent is true.On removal of such user profile,
805                      * the owner's MediaProvider would need to clean any media files stored
806                      * by the removed user profile.
807                      */
808                     UserHandle userToBeRemoved  = intent.getParcelableExtra(Intent.EXTRA_USER);
809                     if(userToBeRemoved.getIdentifier() != sUserId){
810                         mExternalDatabase.runWithTransaction((db) -> {
811                             db.execSQL("delete from files where _user_id=?",
812                                     new String[]{String.valueOf(userToBeRemoved.getIdentifier())});
813                             return null ;
814                         });
815                     }
816                     break;
817             }
818         }
819     };
820 
invalidateLocalCallingIdentityCache(String packageName, String reason)821     private void invalidateLocalCallingIdentityCache(String packageName, String reason) {
822         synchronized (mCachedCallingIdentityForFuse) {
823             try {
824                 int packageUid = getContext().getPackageManager().getPackageUid(packageName, 0);
825                 if (mCachedCallingIdentityForFuse.contains(packageUid)) {
826                     mCachedCallingIdentityForFuse.get(packageUid).dump(reason);
827                     mCachedCallingIdentityForFuse.remove(packageUid);
828                 }
829             } catch (NameNotFoundException ignored) {
830             }
831         }
832     }
833 
updateQuotaTypeForUri(@onNull Uri uri, int mediaType, @NonNull String volumeName)834     private void updateQuotaTypeForUri(@NonNull Uri uri, int mediaType,
835             @NonNull String volumeName) {
836         // Quota type is only updated for external primary volume
837         if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
838             return;
839         }
840 
841         Trace.beginSection("MP.updateQuotaTypeForUri");
842         File file;
843         try {
844             file = queryForDataFile(uri, null);
845             if (!file.exists()) {
846                 // This can happen if an item is inserted in MediaStore before it is created
847                 return;
848             }
849 
850             if (mediaType == FileColumns.MEDIA_TYPE_NONE) {
851                 // This might be because the file is hidden; but we still want to
852                 // attribute its quota to the correct type, so get the type from
853                 // the extension instead.
854                 mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
855             }
856 
857             updateQuotaTypeForFileInternal(file, mediaType);
858         } catch (FileNotFoundException | IllegalArgumentException e) {
859             // Ignore
860             Log.w(TAG, "Failed to update quota for uri: " + uri, e);
861         } finally {
862             Trace.endSection();
863         }
864     }
865 
updateQuotaTypeForFileInternal(File file, int mediaType)866     private void updateQuotaTypeForFileInternal(File file, int mediaType) {
867         try {
868             switch (mediaType) {
869                 case FileColumns.MEDIA_TYPE_AUDIO:
870                     mStorageManager.updateExternalStorageFileQuotaType(file,
871                             StorageManager.QUOTA_TYPE_MEDIA_AUDIO);
872                     break;
873                 case FileColumns.MEDIA_TYPE_VIDEO:
874                     mStorageManager.updateExternalStorageFileQuotaType(file,
875                             StorageManager.QUOTA_TYPE_MEDIA_VIDEO);
876                     break;
877                 case FileColumns.MEDIA_TYPE_IMAGE:
878                     mStorageManager.updateExternalStorageFileQuotaType(file,
879                             StorageManager.QUOTA_TYPE_MEDIA_IMAGE);
880                     break;
881                 default:
882                     mStorageManager.updateExternalStorageFileQuotaType(file,
883                             StorageManager.QUOTA_TYPE_MEDIA_NONE);
884                     break;
885             }
886         } catch (IOException e) {
887             Log.w(TAG, "Failed to update quota type for " + file.getPath(), e);
888         }
889     }
890 
891     /**
892      * Since these operations are in the critical path of apps working with
893      * media, we only collect the {@link Uri} that need to be notified, and all
894      * other side-effect operations are delegated to {@link BackgroundThread} so
895      * that we return as quickly as possible.
896      */
897     private final OnFilesChangeListener mFilesListener = new OnFilesChangeListener() {
898         @Override
899         public void onInsert(@NonNull DatabaseHelper helper, @NonNull FileRow insertedRow) {
900             if (helper.isDatabaseRecovering()) {
901                 // Do not perform any trigger operation if database is recovering
902                 return;
903             }
904 
905             handleInsertedRowForFuse(insertedRow.getId());
906             acceptWithExpansion(helper::notifyInsert, insertedRow.getVolumeName(),
907                     insertedRow.getId(), insertedRow.getMediaType(), insertedRow.isDownload());
908 
909             mDatabaseBackupAndRecovery.updateNextRowIdXattr(helper, insertedRow.getId());
910 
911             helper.postBackground(() -> {
912                 if (helper.isExternal() && !isFuseThread()) {
913                     // Update the quota type on the filesystem
914                     Uri fileUri = MediaStore.Files.getContentUri(insertedRow.getVolumeName(),
915                             insertedRow.getId());
916                     updateQuotaTypeForUri(fileUri, insertedRow.getMediaType(),
917                             insertedRow.getVolumeName());
918                 }
919 
920                 // Tell our SAF provider so it knows when views are no longer empty
921                 MediaDocumentsProvider.onMediaStoreInsert(getContext(), insertedRow.getVolumeName(),
922                         insertedRow.getMediaType(), insertedRow.getId());
923 
924                 if (mExternalDbFacade.onFileInserted(insertedRow.getMediaType(),
925                         insertedRow.isPending())) {
926                     mPickerSyncController.notifyMediaEvent();
927                 }
928 
929                 mDatabaseBackupAndRecovery.backupVolumeDbData(helper, insertedRow);
930             });
931         }
932 
933         @Override
934         public void onUpdate(@NonNull DatabaseHelper helper, @NonNull FileRow oldRow,
935                 @NonNull FileRow newRow) {
936             if (helper.isDatabaseRecovering()) {
937                 // Do not perform any trigger operation if database is recovering
938                 return;
939             }
940 
941             final boolean isDownload = oldRow.isDownload() || newRow.isDownload();
942             final Uri fileUri = MediaStore.Files.getContentUri(oldRow.getVolumeName(),
943                     oldRow.getId());
944             handleUpdatedRowForFuse(oldRow.getPath(), oldRow.getOwnerPackageName(), oldRow.getId(),
945                     newRow.getId());
946             handleOwnerPackageNameChange(oldRow.getPath(), oldRow.getOwnerPackageName(),
947                     newRow.getOwnerPackageName());
948             acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(),
949                     oldRow.getMediaType(), isDownload);
950 
951             mDatabaseBackupAndRecovery.updateNextRowIdAndSetDirty(helper, oldRow, newRow);
952 
953             helper.postBackground(() -> {
954                 if (helper.isExternal()) {
955                     // Update the quota type on the filesystem
956                     updateQuotaTypeForUri(fileUri, newRow.getMediaType(), oldRow.getVolumeName());
957                 }
958 
959                 if (mExternalDbFacade.onFileUpdated(oldRow.getId(),
960                         oldRow.getMediaType(), newRow.getMediaType(),
961                         oldRow.isTrashed(), newRow.isTrashed(),
962                         oldRow.isPending(), newRow.isPending(),
963                         oldRow.isFavorite(), newRow.isFavorite(),
964                         oldRow.getSpecialFormat(), newRow.getSpecialFormat())) {
965                     mPickerSyncController.notifyMediaEvent();
966                 }
967 
968                 mDatabaseBackupAndRecovery.updateBackup(helper, oldRow, newRow);
969             });
970 
971             if (newRow.getMediaType() != oldRow.getMediaType()) {
972                 acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(),
973                         newRow.getMediaType(), isDownload);
974 
975                 helper.postBackground(() -> {
976                     // Invalidate any thumbnails when the media type changes
977                     invalidateThumbnails(fileUri);
978                 });
979             }
980         }
981 
982         @Override
983         public void onDelete(@NonNull DatabaseHelper helper, @NonNull FileRow deletedRow) {
984             if (helper.isDatabaseRecovering()) {
985                 // Do not perform any trigger operation if database is recovering
986                 return;
987             }
988 
989             handleDeletedRowForFuse(deletedRow.getPath(), deletedRow.getOwnerPackageName(),
990                     deletedRow.getId());
991             acceptWithExpansion(helper::notifyDelete, deletedRow.getVolumeName(),
992                     deletedRow.getId(), deletedRow.getMediaType(), deletedRow.isDownload());
993             // Remove cached transcoded file if any
994             mTranscodeHelper.deleteCachedTranscodeFile(deletedRow.getId());
995 
996             helper.postBackground(() -> {
997                 // Item no longer exists, so revoke all access to it
998                 Trace.beginSection("MP.revokeUriPermission");
999                 try {
1000                     acceptWithExpansion((uri) -> getContext().revokeUriPermission(uri, ~0),
1001                             deletedRow.getVolumeName(), deletedRow.getId(),
1002                             deletedRow.getMediaType(), deletedRow.isDownload());
1003                 } finally {
1004                     Trace.endSection();
1005                 }
1006 
1007                 switch (deletedRow.getMediaType()) {
1008                     case FileColumns.MEDIA_TYPE_PLAYLIST:
1009                     case FileColumns.MEDIA_TYPE_AUDIO:
1010                         if (helper.isExternal()) {
1011                             removePlaylistMembers(deletedRow.getMediaType(), deletedRow.getId());
1012                         }
1013                 }
1014 
1015                 // Invalidate any thumbnails now that media is gone
1016                 invalidateThumbnails(MediaStore.Files.getContentUri(deletedRow.getVolumeName(),
1017                         deletedRow.getId()));
1018 
1019                 // Tell our SAF provider so it can revoke too
1020                 MediaDocumentsProvider.onMediaStoreDelete(getContext(), deletedRow.getVolumeName(),
1021                         deletedRow.getMediaType(), deletedRow.getId());
1022 
1023                 if (mExternalDbFacade.onFileDeleted(deletedRow.getId(),
1024                         deletedRow.getMediaType())) {
1025                     mPickerSyncController.notifyMediaEvent();
1026                 }
1027 
1028                 mDatabaseBackupAndRecovery.deleteFromDbBackup(helper, deletedRow);
1029             });
1030         }
1031     };
1032 
1033     private final UnaryOperator<String> mIdGenerator = path -> {
1034         final long rowId = mCallingIdentity.get().getDeletedRowId(path);
1035         if (rowId != -1 && isFuseThread()) {
1036             return String.valueOf(rowId);
1037         }
1038         return null;
1039     };
1040 
1041     /** {@hide} */
1042     public static final OnLegacyMigrationListener MIGRATION_LISTENER =
1043             new OnLegacyMigrationListener() {
1044         @Override
1045         public void onStarted(ContentProviderClient client, String volumeName) {
1046             MediaStore.startLegacyMigration(ContentResolver.wrap(client), volumeName);
1047         }
1048 
1049         @Override
1050         public void onProgress(ContentProviderClient client, String volumeName,
1051                 long progress, long total) {
1052             // TODO: notify blocked threads of progress once we can change APIs
1053         }
1054 
1055         @Override
1056         public void onFinished(ContentProviderClient client, String volumeName) {
1057             MediaStore.finishLegacyMigration(ContentResolver.wrap(client), volumeName);
1058         }
1059     };
1060 
1061     /**
1062      * Apply {@link Consumer#accept} to the given item.
1063      * <p>
1064      * Since media items can be exposed through multiple collections or views,
1065      * this method expands the single item being accepted to also accept all
1066      * relevant views.
1067      */
acceptWithExpansion(@onNull Consumer<Uri> consumer, @NonNull String volumeName, long id, int mediaType, boolean isDownload)1068     private void acceptWithExpansion(@NonNull Consumer<Uri> consumer, @NonNull String volumeName,
1069             long id, int mediaType, boolean isDownload) {
1070         switch (mediaType) {
1071             case FileColumns.MEDIA_TYPE_AUDIO:
1072                 consumer.accept(MediaStore.Audio.Media.getContentUri(volumeName, id));
1073 
1074                 // Any changing audio items mean we probably need to invalidate all
1075                 // indexed views built from that media
1076                 consumer.accept(Audio.Genres.getContentUri(volumeName));
1077                 consumer.accept(Audio.Playlists.getContentUri(volumeName));
1078                 consumer.accept(Audio.Artists.getContentUri(volumeName));
1079                 consumer.accept(Audio.Albums.getContentUri(volumeName));
1080                 break;
1081 
1082             case FileColumns.MEDIA_TYPE_VIDEO:
1083                 consumer.accept(MediaStore.Video.Media.getContentUri(volumeName, id));
1084                 break;
1085 
1086             case FileColumns.MEDIA_TYPE_IMAGE:
1087                 consumer.accept(MediaStore.Images.Media.getContentUri(volumeName, id));
1088                 break;
1089 
1090             case FileColumns.MEDIA_TYPE_PLAYLIST:
1091                 consumer.accept(ContentUris.withAppendedId(
1092                         MediaStore.Audio.Playlists.getContentUri(volumeName), id));
1093                 break;
1094         }
1095 
1096         // Also notify through any generic views
1097         consumer.accept(MediaStore.Files.getContentUri(volumeName, id));
1098         if (isDownload) {
1099             consumer.accept(MediaStore.Downloads.getContentUri(volumeName, id));
1100         }
1101 
1102         // Rinse and repeat through any synthetic views
1103         switch (volumeName) {
1104             case MediaStore.VOLUME_INTERNAL:
1105             case MediaStore.VOLUME_EXTERNAL:
1106                 // Already a top-level view, no need to expand
1107                 break;
1108             default:
1109                 acceptWithExpansion(consumer, MediaStore.VOLUME_EXTERNAL,
1110                         id, mediaType, isDownload);
1111                 break;
1112         }
1113     }
1114 
1115     /**
1116      * Ensure that default folders are created on mounted storage devices.
1117      * We only do this once per volume so we don't annoy the user if deleted
1118      * manually.
1119      */
ensureDefaultFolders(@onNull MediaVolume volume, @NonNull SQLiteDatabase db)1120     private void ensureDefaultFolders(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) {
1121         if (volume.shouldSkipDefaultDirCreation()) {
1122             // Default folders should not be automatically created inside volumes managed from
1123             // outside Android.
1124             return;
1125         }
1126         final String volumeName = volume.getName();
1127         String key;
1128         if (volumeName.equals(MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
1129             // For the primary volume, we use the ID, because we may be handling
1130             // the primary volume for multiple users
1131             key = "created_default_folders_" + volume.getId();
1132         } else {
1133             // For others, like public volumes, just use the name, because the id
1134             // might not change when re-formatted
1135             key = "created_default_folders_" + volumeName;
1136         }
1137 
1138         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
1139         if (prefs.getInt(key, 0) == 0) {
1140             for (String folderName : DEFAULT_FOLDER_NAMES) {
1141                 final File folder = new File(volume.getPath(), folderName);
1142                 if (!folder.exists()) {
1143                     folder.mkdirs();
1144                     insertDirectory(db, folder.getAbsolutePath());
1145                 }
1146             }
1147 
1148             SharedPreferences.Editor editor = prefs.edit();
1149             editor.putInt(key, 1);
1150             editor.commit();
1151         }
1152     }
1153 
1154     /**
1155      * Ensure that any thumbnail collections on the given storage volume can be
1156      * used with the given {@link DatabaseHelper}. If the
1157      * {@link DatabaseHelper#getOrCreateUuid} doesn't match the UUID found on
1158      * disk, then all thumbnails will be considered stable and will be deleted.
1159      */
ensureThumbnailsValid(@onNull MediaVolume volume, @NonNull SQLiteDatabase db)1160     private void ensureThumbnailsValid(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) {
1161         if (volume.shouldSkipDefaultDirCreation()) {
1162             // Default folders and thumbnail directories should not be automatically created inside
1163             // volumes managed from outside Android, and there is no need to ensure the validity of
1164             // their thumbnails here.
1165             return;
1166         }
1167         final String uuidFromDatabase = DatabaseHelper.getOrCreateUuid(db);
1168         try {
1169             for (File dir : getThumbnailDirectories(volume)) {
1170                 if (!dir.exists()) {
1171                     dir.mkdirs();
1172                 }
1173 
1174                 final File file = new File(dir, FILE_DATABASE_UUID);
1175                 final Optional<String> uuidFromDisk = FileUtils.readString(file);
1176 
1177                 final boolean updateUuid;
1178                 if (!uuidFromDisk.isPresent()) {
1179                     // For newly inserted volumes or upgrading of existing volumes,
1180                     // assume that our current UUID is valid
1181                     updateUuid = true;
1182                 } else if (!Objects.equals(uuidFromDatabase, uuidFromDisk.get())) {
1183                     // The UUID of database disagrees with the one on disk,
1184                     // which means we can't trust any thumbnails
1185                     Log.d(TAG, "Invalidating all thumbnails under " + dir);
1186                     FileUtils.walkFileTreeContents(dir.toPath(), this::deleteAndInvalidate);
1187                     updateUuid = true;
1188                 } else {
1189                     updateUuid = false;
1190                 }
1191 
1192                 if (updateUuid) {
1193                     FileUtils.writeString(file, Optional.of(uuidFromDatabase));
1194                 }
1195             }
1196         } catch (IOException e) {
1197             Log.w(TAG, "Failed to ensure thumbnails valid for " + volume.getName(), e);
1198         }
1199     }
1200 
1201     @Override
attachInfo(Context context, ProviderInfo info)1202     public void attachInfo(Context context, ProviderInfo info) {
1203         Log.v(TAG, "Attached " + info.authority + " from " + info.applicationInfo.packageName);
1204 
1205         mUriMatcher = new LocalUriMatcher(info.authority);
1206 
1207         super.attachInfo(context, info);
1208     }
1209 
1210     @Nullable
1211     private static MediaProvider sInstance;
1212 
1213     @Nullable
getInstance()1214     static synchronized MediaProvider getInstance() {
1215         return sInstance;
1216     }
1217 
1218     @Override
onCreate()1219     public boolean onCreate() {
1220         synchronized (MediaProvider.class) {
1221             sInstance = this;
1222         }
1223 
1224         final Context context = getContext();
1225 
1226         mUserCache = new UserCache(context);
1227 
1228         // Shift call statistics back to the original caller
1229         Binder.setProxyTransactListener(mTransactListener);
1230 
1231         mStorageManager = context.getSystemService(StorageManager.class);
1232         AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
1233         mPackageManager = context.getPackageManager();
1234         mUserManager = context.getSystemService(UserManager.class);
1235         mVolumeCache = new VolumeCache(context, mUserCache);
1236 
1237         // Reasonable thumbnail size is half of the smallest screen edge width
1238         final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
1239         final int thumbSize = Math.min(metrics.widthPixels, metrics.heightPixels) / 2;
1240         mThumbSize = new Size(thumbSize, thumbSize);
1241 
1242         mConfigStore = createConfigStore();
1243         mDatabaseBackupAndRecovery = createDatabaseBackupAndRecovery();
1244 
1245         mMediaScanner = new ModernMediaScanner(context);
1246         mProjectionHelper = new ProjectionHelper(Column.class, ExportedSince.class);
1247         mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, false, false,
1248                 mProjectionHelper, Metrics::logSchemaChange, mFilesListener,
1249                 MIGRATION_LISTENER, mIdGenerator, true, mDatabaseBackupAndRecovery);
1250         mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, false,
1251                 mProjectionHelper, Metrics::logSchemaChange, mFilesListener,
1252                 MIGRATION_LISTENER, mIdGenerator, true, mDatabaseBackupAndRecovery);
1253         mExternalDbFacade = new ExternalDbFacade(getContext(), mExternalDatabase, mVolumeCache);
1254         mPickerDbFacade = new PickerDbFacade(context);
1255 
1256         mMediaGrants = new MediaGrants(mExternalDatabase);
1257 
1258         mPickerSyncController = new PickerSyncController(context, mPickerDbFacade, mConfigStore);
1259         mPickerDataLayer = new PickerDataLayer(context, mPickerDbFacade, mPickerSyncController);
1260         mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade, mProjectionHelper);
1261 
1262         if (SdkLevel.isAtLeastS()) {
1263             mTranscodeHelper = new TranscodeHelperImpl(context, this, mConfigStore);
1264         } else {
1265             mTranscodeHelper = new TranscodeHelperNoOp();
1266         }
1267 
1268         // Create dir for redacted and picker URI paths.
1269         buildPrimaryVolumeFile(uidToUserId(MY_UID), getRedactedRelativePath()).mkdirs();
1270 
1271         final IntentFilter packageFilter = new IntentFilter();
1272         packageFilter.setPriority(10);
1273         packageFilter.addDataScheme("package");
1274         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
1275         packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
1276         context.registerReceiver(mPackageReceiver, packageFilter);
1277 
1278         // Creating intent broadcast receiver for user actions like Intent.ACTION_USER_REMOVED,
1279         // where we would need to remove files stored by removed user.
1280         final IntentFilter userIntentFilter = new IntentFilter();
1281         userIntentFilter.addAction(Intent.ACTION_USER_REMOVED);
1282         context.registerReceiver(mUserIntentReceiver, userIntentFilter);
1283 
1284         // Watch for invalidation of cached volumes
1285         mStorageManager.registerStorageVolumeCallback(context.getMainExecutor(),
1286                 new StorageVolumeCallback() {
1287                     @Override
1288                     public void onStateChanged(@NonNull StorageVolume volume) {
1289                         updateVolumes();
1290                     }
1291                 });
1292 
1293         if (SdkLevel.isAtLeastT()) {
1294             try {
1295                 mStorageManager.setCloudMediaProvider(mPickerSyncController.getCloudProvider());
1296             } catch (SecurityException e) {
1297                 // This can happen in unit tests
1298                 Log.w(TAG, "Failed to update the system_server with the latest cloud provider", e);
1299             }
1300         }
1301 
1302         updateVolumes();
1303         attachVolume(MediaVolume.fromInternal(), /* validate */ false);
1304         for (MediaVolume volume : mVolumeCache.getExternalVolumes()) {
1305             attachVolume(volume, /* validate */ false);
1306         }
1307 
1308         // Watch for performance-sensitive activity
1309         appOpsManager.startWatchingActive(new String[] {
1310                 AppOpsManager.OPSTR_CAMERA
1311         }, context.getMainExecutor(), mActiveListener);
1312 
1313         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE,
1314                 null /* all packages */, mModeListener);
1315         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_AUDIO,
1316                 null /* all packages */, mModeListener);
1317         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_IMAGES,
1318                 null /* all packages */, mModeListener);
1319         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_VIDEO,
1320                 null /* all packages */, mModeListener);
1321         if (SdkLevel.isAtLeastU()) {
1322             appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_VISUAL_USER_SELECTED,
1323                     null /* all packages */, mModeListener);
1324         }
1325         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE,
1326                 null /* all packages */, mModeListener);
1327         appOpsManager.startWatchingMode(permissionToOp(ACCESS_MEDIA_LOCATION),
1328                 null /* all packages */, mModeListener);
1329         // Legacy apps
1330         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_LEGACY_STORAGE,
1331                 null /* all packages */, mModeListener);
1332         // File managers
1333         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE,
1334                 null /* all packages */, mModeListener);
1335         // Default gallery changes
1336         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES,
1337                 null /* all packages */, mModeListener);
1338         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO,
1339                 null /* all packages */, mModeListener);
1340         try {
1341             // Here we are forced to depend on the non-public API of AppOpsManager. If
1342             // OPSTR_NO_ISOLATED_STORAGE app op is not defined in AppOpsManager, then this call will
1343             // throw an IllegalArgumentException during MediaProvider startup. In combination with
1344             // MediaProvider's CTS tests it should give us guarantees that OPSTR_NO_ISOLATED_STORAGE
1345             // is defined.
1346             appOpsManager.startWatchingMode(AppOpsManager.OPSTR_NO_ISOLATED_STORAGE,
1347                     null /* all packages */, mModeListener);
1348         } catch (IllegalArgumentException e) {
1349             Log.w(TAG, "Failed to start watching " + AppOpsManager.OPSTR_NO_ISOLATED_STORAGE, e);
1350         }
1351 
1352         ProviderInfo provider = mPackageManager.resolveContentProvider(
1353                 getDownloadsProviderAuthority(), PackageManager.MATCH_DIRECT_BOOT_AWARE
1354                 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
1355         if (provider != null) {
1356             mDownloadsAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
1357         }
1358 
1359         provider = mPackageManager.resolveContentProvider(getExternalStorageProviderAuthority(),
1360                 PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
1361         if (provider != null) {
1362             mExternalStorageAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
1363         }
1364 
1365         storageNativeBootPropertyChangeListener();
1366         mConfigStore.addOnChangeListener(
1367                 BackgroundThread.getExecutor(), this::storageNativeBootPropertyChangeListener);
1368 
1369         // media_grants are cleared on device reboot, and onCreate is a good signal for this.
1370         ForegroundThread.getExecutor().execute(() -> {
1371             mMediaGrants.removeAllMediaGrants();
1372         });
1373 
1374         PulledMetrics.initialize(context);
1375         return true;
1376     }
1377 
1378     @VisibleForTesting
storageNativeBootPropertyChangeListener()1379     protected void storageNativeBootPropertyChangeListener() {
1380         boolean isGetContentTakeoverEnabled;
1381         if (SdkLevel.isAtLeastT()) {
1382             isGetContentTakeoverEnabled = true;
1383         } else {
1384             isGetContentTakeoverEnabled = mConfigStore.isGetContentTakeOverEnabled();
1385         }
1386         setComponentEnabledSetting("PhotoPickerGetContentActivity", isGetContentTakeoverEnabled);
1387 
1388         setComponentEnabledSetting("PhotoPickerUserSelectActivity",
1389                 mConfigStore.isUserSelectForAppEnabled());
1390 
1391         mDatabaseBackupAndRecovery.onConfigPropertyChangeListener();
1392     }
1393 
getDatabaseBackupAndRecovery()1394     public DatabaseBackupAndRecovery getDatabaseBackupAndRecovery() {
1395         return mDatabaseBackupAndRecovery;
1396     }
1397 
setComponentEnabledSetting(@onNull String activityName, boolean isEnabled)1398     private void setComponentEnabledSetting(@NonNull String activityName, boolean isEnabled) {
1399         final String activityFullName =
1400                 PhotoPickerActivity.class.getPackage().getName() + "." + activityName;
1401         final ComponentName componentName = new ComponentName(getContext().getPackageName(),
1402                 activityFullName);
1403 
1404         final int expectedState = isEnabled
1405                 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
1406                 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
1407 
1408         Log.i(TAG, "Changed " + activityName + " component state to "
1409                 + componentStateToString(expectedState));
1410 
1411         getContext().getPackageManager().setComponentEnabledSetting(componentName, expectedState,
1412                 PackageManager.DONT_KILL_APP);
1413     }
1414 
getDatabaseHelper(String dbName)1415     Optional<DatabaseHelper> getDatabaseHelper(String dbName) {
1416         if (dbName.equalsIgnoreCase(INTERNAL_DATABASE_NAME)) {
1417             return Optional.of(mInternalDatabase);
1418         } else if (dbName.equalsIgnoreCase(EXTERNAL_DATABASE_NAME)) {
1419             return Optional.of(mExternalDatabase);
1420         }
1421 
1422         return Optional.empty();
1423     }
1424 
1425     @Override
onCallingPackageChanged()1426     public void onCallingPackageChanged() {
1427         // Identity of the current thread has changed, so invalidate caches
1428         mCallingIdentity.remove();
1429     }
1430 
clearLocalCallingIdentity()1431     public LocalCallingIdentity clearLocalCallingIdentity() {
1432         // We retain the user part of the calling identity, since we are executing
1433         // the call on behalf of that user, and we need to maintain the user context
1434         // to correctly resolve things like volumes
1435         UserHandle user = mCallingIdentity.get().getUser();
1436         return clearLocalCallingIdentity(LocalCallingIdentity.fromSelfAsUser(getContext(), user));
1437     }
1438 
clearLocalCallingIdentity(LocalCallingIdentity replacement)1439     public LocalCallingIdentity clearLocalCallingIdentity(LocalCallingIdentity replacement) {
1440         final LocalCallingIdentity token = mCallingIdentity.get();
1441         mCallingIdentity.set(replacement);
1442         return token;
1443     }
1444 
restoreLocalCallingIdentity(LocalCallingIdentity token)1445     public void restoreLocalCallingIdentity(LocalCallingIdentity token) {
1446         mCallingIdentity.set(token);
1447     }
1448 
isPackageKnown(@onNull String packageName, int userId)1449     private boolean isPackageKnown(@NonNull String packageName, int userId) {
1450         final Context context = mUserCache.getContextForUser(UserHandle.of(userId));
1451         final PackageManager pm = context.getPackageManager();
1452 
1453         // First, is the app actually installed?
1454         try {
1455             pm.getPackageInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES);
1456             return true;
1457         } catch (NameNotFoundException ignored) {
1458         }
1459 
1460         // Second, is the app pending, probably from a backup/restore operation?
1461         for (SessionInfo si : pm.getPackageInstaller().getAllSessions()) {
1462             if (Objects.equals(packageName, si.getAppPackageName())) {
1463                 return true;
1464             }
1465         }
1466 
1467         // I've never met this package in my life
1468         return false;
1469     }
1470 
onIdleMaintenance(@onNull CancellationSignal signal)1471     public void onIdleMaintenance(@NonNull CancellationSignal signal) {
1472         final long startTime = SystemClock.elapsedRealtime();
1473         // Trim any stale log files before we emit new events below
1474         Logging.trimPersistent();
1475 
1476         // Scan all volumes to resolve any staleness
1477         for (MediaVolume volume : mVolumeCache.getExternalVolumes()) {
1478             // Possibly bail before digging into each volume
1479             signal.throwIfCanceled();
1480 
1481             try {
1482                 MediaService.onScanVolume(getContext(), volume, REASON_IDLE);
1483             } catch (IOException e) {
1484                 Log.w(TAG, e);
1485             }
1486 
1487             // Ensure that our thumbnails are valid
1488             mExternalDatabase.runWithTransaction((db) -> {
1489                 ensureThumbnailsValid(volume, db);
1490                 return null;
1491             });
1492         }
1493 
1494         // Delete any stale thumbnails
1495         final int staleThumbnails = mExternalDatabase.runWithTransaction((db) -> {
1496             return pruneThumbnails(db, signal);
1497         });
1498         Log.d(TAG, "Pruned " + staleThumbnails + " unknown thumbnails");
1499 
1500         // Finished orphaning any content whose package no longer exists
1501         pruneStalePackages(signal);
1502 
1503         // Delete the expired items or extend them on mounted volumes
1504         final int[] result = deleteOrExtendExpiredItems(signal);
1505         final int deletedExpiredMedia = result[0];
1506         Log.d(TAG, "Deleted " + deletedExpiredMedia + " expired items");
1507         Log.d(TAG, "Extended " + result[1] + " expired items");
1508 
1509         // Forget any stale volumes
1510         deleteStaleVolumes(signal);
1511 
1512         final long itemCount = mExternalDatabase.runWithTransaction(DatabaseHelper::getItemCount);
1513 
1514         // Cleaning media files for users that have been removed
1515         cleanMediaFilesForRemovedUser(signal);
1516 
1517         // Calculate standard_mime_type_extension column for files which have SPECIAL_FORMAT column
1518         // value as NULL, and update the same in the picker db
1519         detectSpecialFormat(signal);
1520 
1521         final long durationMillis = (SystemClock.elapsedRealtime() - startTime);
1522         Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount,
1523                 durationMillis, staleThumbnails, deletedExpiredMedia);
1524     }
1525 
1526     /**
1527      * This function find and clean the files related to user who have been removed
1528      */
cleanMediaFilesForRemovedUser(CancellationSignal signal)1529     private void cleanMediaFilesForRemovedUser(CancellationSignal signal) {
1530         //Finding userIds that are available in database
1531         final List<String> userIds = mExternalDatabase.runWithTransaction((db) -> {
1532             final List<String> userIdsPresent = new ArrayList<>();
1533             try (Cursor c = db.query(true, "files", new String[] { "_user_id" },
1534                     null, null, null, null, null,
1535                     null, signal)) {
1536                 while (c.moveToNext()) {
1537                     final String userId = c.getString(0);
1538                     userIdsPresent.add(userId);
1539                 }
1540             }
1541             return userIdsPresent;
1542         });
1543 
1544         // removing calling userId
1545         userIds.remove(String.valueOf(sUserId));
1546         // removing all the valid/existing user, remaining userIds would be users who would have
1547         // been removed
1548         userIds.removeAll(mUserManager.getEnabledProfiles().stream()
1549                 .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect(
1550                         Collectors.toList()));
1551 
1552         // Cleaning media files of users who have been removed
1553         mExternalDatabase.runWithTransaction((db) -> {
1554             userIds.stream().forEach(userId ->{
1555                 Log.d(TAG, "Removing media files associated with user : " + userId);
1556                 db.execSQL("delete from files where _user_id=?",
1557                         new String[]{String.valueOf(userId)});
1558             });
1559             return null ;
1560         });
1561     }
1562 
pruneStalePackages(CancellationSignal signal)1563     private void pruneStalePackages(CancellationSignal signal) {
1564         final int stalePackages = mExternalDatabase.runWithTransaction((db) -> {
1565             final ArraySet<Pair<String, Integer>> unknownPackages = new ArraySet<>();
1566             try (Cursor c = db.query(true, "files",
1567                     new String[] { "owner_package_name", "_user_id" },
1568                     null, null, null, null, null, null, signal)) {
1569                 while (c.moveToNext()) {
1570                     final String packageName = c.getString(0);
1571                     if (TextUtils.isEmpty(packageName)) continue;
1572 
1573                     final int userId = c.getInt(1);
1574 
1575                     if (!isPackageKnown(packageName, userId)) {
1576                         unknownPackages.add(Pair.create(packageName, userId));
1577                     }
1578                 }
1579             }
1580             for (Pair<String, Integer> pair : unknownPackages) {
1581                 onPackageOrphaned(db, pair.first, pair.second);
1582             }
1583             return unknownPackages.size();
1584         });
1585         Log.d(TAG, "Pruned " + stalePackages + " unknown packages");
1586     }
1587 
deleteStaleVolumes(CancellationSignal signal)1588     private void deleteStaleVolumes(CancellationSignal signal) {
1589         mExternalDatabase.runWithTransaction((db) -> {
1590             final Set<String> recentVolumeNames = MediaStore
1591                     .getRecentExternalVolumeNames(getContext());
1592             final Set<String> knownVolumeNames = new ArraySet<>();
1593             try (Cursor c = db.query(true, "files", new String[] { MediaColumns.VOLUME_NAME },
1594                     null, null, null, null, null, null, signal)) {
1595                 while (c.moveToNext()) {
1596                     knownVolumeNames.add(c.getString(0));
1597                 }
1598             }
1599             final Set<String> staleVolumeNames = new ArraySet<>();
1600             staleVolumeNames.addAll(knownVolumeNames);
1601             staleVolumeNames.removeAll(recentVolumeNames);
1602             for (String staleVolumeName : staleVolumeNames) {
1603                 final int num = db.delete("files", FileColumns.VOLUME_NAME + "=?",
1604                         new String[] { staleVolumeName });
1605                 Log.d(TAG, "Forgot " + num + " stale items from " + staleVolumeName);
1606             }
1607             return null;
1608         });
1609 
1610         synchronized (mDirectoryCache) {
1611             mDirectoryCache.clear();
1612         }
1613     }
1614 
1615     @VisibleForTesting
setUriResolver(PickerUriResolver resolver)1616     public void setUriResolver(PickerUriResolver resolver) {
1617         Log.w(TAG, "Changing the PickerUriResolver!!! Should only be called during test");
1618         mPickerUriResolver = resolver;
1619     }
1620 
1621     @VisibleForTesting
detectSpecialFormat(@onNull CancellationSignal signal)1622     void detectSpecialFormat(@NonNull CancellationSignal signal) {
1623         // Picker sync and special format update can execute concurrently and run into a deadlock.
1624         // Acquiring a lock before execution of each flow to avoid this.
1625         PickerSyncController.sIdleMaintenanceSyncLock.lock();
1626         try {
1627             mExternalDatabase.runWithTransaction((db) -> {
1628                 updateSpecialFormatColumn(db, signal);
1629                 return null;
1630             });
1631         } finally {
1632             PickerSyncController.sIdleMaintenanceSyncLock.unlock();
1633         }
1634     }
1635 
updateSpecialFormatColumn(SQLiteDatabase db, @NonNull CancellationSignal signal)1636     private void updateSpecialFormatColumn(SQLiteDatabase db, @NonNull CancellationSignal signal) {
1637         // This is to ensure we only do a bounded iteration over the rows as updates can fail, and
1638         // we don't want to keep running the query/update indefinitely.
1639         final int totalRowsToUpdate = getPendingSpecialFormatRowsCount(db, signal);
1640         for (int i = 0; i < totalRowsToUpdate; i += IDLE_MAINTENANCE_ROWS_LIMIT) {
1641             try (PickerDbFacade.UpdateMediaOperation operation =
1642                          mPickerDbFacade.beginUpdateMediaOperation(
1643                                  PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)) {
1644                 updateSpecialFormatForLimitedRows(db, signal, operation);
1645                 operation.setSuccess();
1646             }
1647         }
1648     }
1649 
getPendingSpecialFormatRowsCount(SQLiteDatabase db, @NonNull CancellationSignal signal)1650     private int getPendingSpecialFormatRowsCount(SQLiteDatabase db,
1651             @NonNull CancellationSignal signal) {
1652         try (Cursor c = queryForPendingSpecialFormatColumns(db, /* limit */ null, signal)) {
1653             if (c == null) {
1654                 return 0;
1655             }
1656             return c.getCount();
1657         }
1658     }
1659 
updateSpecialFormatForLimitedRows(SQLiteDatabase externalDb, @NonNull CancellationSignal signal, PickerDbFacade.UpdateMediaOperation operation)1660     private void updateSpecialFormatForLimitedRows(SQLiteDatabase externalDb,
1661             @NonNull CancellationSignal signal, PickerDbFacade.UpdateMediaOperation operation) {
1662         // Accumulate all the new SPECIAL_FORMAT updates with their ids
1663         ArrayMap<Long, Integer> newSpecialFormatValues = new ArrayMap<>();
1664         final String limit = String.valueOf(IDLE_MAINTENANCE_ROWS_LIMIT);
1665         try (Cursor c = queryForPendingSpecialFormatColumns(externalDb, limit, signal)) {
1666             while (c.moveToNext() && !signal.isCanceled()) {
1667                 final long id = c.getLong(0);
1668                 final String path = c.getString(1);
1669                 newSpecialFormatValues.put(id, getSpecialFormatValue(path));
1670             }
1671         }
1672 
1673         // Now, update all the new SPECIAL_FORMAT values in both external db and picker db.
1674         final ContentValues pickerDbValues = new ContentValues();
1675         final ContentValues externalDbValues = new ContentValues();
1676         int count = 0;
1677         for (long id : newSpecialFormatValues.keySet()) {
1678             if (signal.isCanceled()) {
1679                 return;
1680             }
1681 
1682             int specialFormat = newSpecialFormatValues.get(id);
1683 
1684             pickerDbValues.clear();
1685             pickerDbValues.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION, specialFormat);
1686             boolean pickerDbWriteSuccess = operation.execute(String.valueOf(id), pickerDbValues);
1687 
1688             externalDbValues.clear();
1689             externalDbValues.put(_SPECIAL_FORMAT, specialFormat);
1690             final String externalDbSelection = MediaColumns._ID + "=?";
1691             final String[] externalDbSelectionArgs = new String[]{String.valueOf(id)};
1692             boolean externalDbWriteSuccess =
1693                     externalDb.update("files", externalDbValues, externalDbSelection,
1694                             externalDbSelectionArgs)
1695                             == 1;
1696 
1697             if (pickerDbWriteSuccess && externalDbWriteSuccess) {
1698                 count++;
1699             }
1700         }
1701         Log.d(TAG, "Updated standard_mime_type_extension for " + count + " items");
1702     }
1703 
getSpecialFormatValue(String path)1704     private int getSpecialFormatValue(String path) {
1705         final File file = new File(path);
1706         if (!file.exists()) {
1707             // We always update special format to none if the file is not found or there is an
1708             // error, this is so that we do not repeat over the same column again and again.
1709             return _SPECIAL_FORMAT_NONE;
1710         }
1711 
1712         try {
1713             return SpecialFormatDetector.detect(file);
1714         } catch (Exception e) {
1715             // we tried our best, no need to run special detection again and again if it
1716             // throws exception once, it is likely to do so everytime.
1717             Log.d(TAG, "Failed to detect special format for file: " + file, e);
1718             return _SPECIAL_FORMAT_NONE;
1719         }
1720     }
1721 
queryForPendingSpecialFormatColumns(SQLiteDatabase db, String limit, @NonNull CancellationSignal signal)1722     private Cursor queryForPendingSpecialFormatColumns(SQLiteDatabase db, String limit,
1723             @NonNull CancellationSignal signal) {
1724         // Run special detection for images only
1725         final String selection = _SPECIAL_FORMAT + " IS NULL AND "
1726                 + MEDIA_TYPE + "=" + MEDIA_TYPE_IMAGE;
1727         final String[] projection = new String[] { MediaColumns._ID, MediaColumns.DATA };
1728         return db.query(/* distinct */ true, "files", projection, selection, null, null, null,
1729                 null, limit, signal);
1730     }
1731 
1732     /**
1733      * Delete any expired content on mounted volumes. The expired content on unmounted
1734      * volumes will be deleted when we forget any stale volumes; we're cautious about
1735      * wildly changing clocks, so only delete items within the last week.
1736      * If the items are expired more than one week, extend the expired time of them
1737      * another one week to avoid data loss with incorrect time zone data. We will
1738      * delete it when it is expired next time.
1739      *
1740      * @param signal the cancellation signal
1741      * @return the integer array includes total deleted count and total extended count
1742      */
1743     @NonNull
deleteOrExtendExpiredItems(@onNull CancellationSignal signal)1744     private int[] deleteOrExtendExpiredItems(@NonNull CancellationSignal signal) {
1745         final long expiredOneWeek =
1746                 ((System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS) / 1000);
1747         final long now = (System.currentTimeMillis() / 1000);
1748         final long expiredTime = now + (FileUtils.DEFAULT_DURATION_EXTENDED / 1000);
1749         return mExternalDatabase.runWithTransaction((db) -> {
1750             String selection = FileColumns.DATE_EXPIRES + " < " + now;
1751             selection += " AND volume_name in " + bindList(MediaStore.getExternalVolumeNames(
1752                     getContext()).toArray());
1753             String[] projection = new String[]{"volume_name", "_id",
1754                     FileColumns.DATE_EXPIRES, FileColumns.DATA};
1755             try (Cursor c = db.query(true, "files", projection, selection, null, null, null, null,
1756                     null, signal)) {
1757                 int totalDeleteCount = 0;
1758                 int totalExtendedCount = 0;
1759                 int index = 0;
1760                 while (c.moveToNext()) {
1761                     final String volumeName = c.getString(0);
1762                     final long id = c.getLong(1);
1763                     final long dateExpires = c.getLong(2);
1764                     // we only delete the items that expire in one week
1765                     if (dateExpires > expiredOneWeek) {
1766                         totalDeleteCount += delete(Files.getContentUri(volumeName, id), null, null);
1767                     } else {
1768                         final String oriPath = c.getString(3);
1769 
1770                         final boolean success = extendExpiredItem(db, oriPath, id, expiredTime,
1771                                 expiredTime + index);
1772                         if (success) {
1773                             totalExtendedCount++;
1774                         }
1775                         index++;
1776                     }
1777                 }
1778                 return new int[]{totalDeleteCount, totalExtendedCount};
1779             }
1780         });
1781     }
1782 
1783     /**
1784      * Extend the expired items by renaming the file to new path with new timestamp and updating the
1785      * database for {@link FileColumns#DATA} and {@link FileColumns#DATE_EXPIRES}. If there is
1786      * UNIQUE constraint error for FileColumns.DATA, use adjustedExpiredTime and generate the new
1787      * path by adjustedExpiredTime.
1788      */
extendExpiredItem(@onNull SQLiteDatabase db, @NonNull String originalPath, long id, long newExpiredTime, long adjustedExpiredTime)1789     private boolean extendExpiredItem(@NonNull SQLiteDatabase db, @NonNull String originalPath,
1790             long id, long newExpiredTime, long adjustedExpiredTime) {
1791         String newPath = FileUtils.getAbsoluteExtendedPath(originalPath, newExpiredTime);
1792         if (newPath == null) {
1793             Log.e(TAG, "Couldn't compute path for " + originalPath + " and expired time "
1794                     + newExpiredTime);
1795             return false;
1796         }
1797 
1798         try {
1799             if (updateDatabaseForExpiredItem(db, newPath, id, newExpiredTime)) {
1800                 return renameInLowerFsAndInvalidateFuseDentry(originalPath, newPath);
1801             }
1802             return false;
1803         } catch (SQLiteConstraintException e) {
1804             final String errorMessage =
1805                     "Update database _data from " + originalPath + " to " + newPath + " failed.";
1806             Log.d(TAG, errorMessage, e);
1807         }
1808 
1809         // When we update the database for newPath with newExpiredTime, if the new path already
1810         // exists in the database, it may raise SQLiteConstraintException.
1811         // If there are two expired items that have the same display name in the same directory,
1812         // but they have different expired time. E.g. .trashed-123-A.jpg and .trashed-456-A.jpg.
1813         // After we rename .trashed-123-A.jpg to .trashed-newExpiredTime-A.jpg, then we rename
1814         // .trashed-456-A.jpg to .trashed-newExpiredTime-A.jpg, it raises the exception. For
1815         // this case, we will retry it with the adjustedExpiredTime again.
1816         newPath = FileUtils.getAbsoluteExtendedPath(originalPath, adjustedExpiredTime);
1817         Log.i(TAG, "Retrying to extend expired item with the new path = " + newPath);
1818         try {
1819             if (updateDatabaseForExpiredItem(db, newPath, id, adjustedExpiredTime)) {
1820                 return renameInLowerFsAndInvalidateFuseDentry(originalPath, newPath);
1821             }
1822         } catch (SQLiteConstraintException e) {
1823             // If we want to rename one expired item E.g. .trashed-123-A.jpg., and there is another
1824             // non-expired trashed/pending item has the same name. E.g.
1825             // .trashed-adjustedExpiredTime-A.jpg. When we rename .trashed-123-A.jpg to
1826             // .trashed-adjustedExpiredTime-A.jpg, it raises the SQLiteConstraintException.
1827             // The smallest unit of the expired time we use is second. It is a very rare case.
1828             // When this case is happened, we can handle it in next idle maintenance.
1829             final String errorMessage =
1830                     "Update database _data from " + originalPath + " to " + newPath + " failed.";
1831             Log.d(TAG, errorMessage, e);
1832         }
1833 
1834         return false;
1835     }
1836 
updateDatabaseForExpiredItem(@onNull SQLiteDatabase db, @NonNull String path, long id, long expiredTime)1837     private boolean updateDatabaseForExpiredItem(@NonNull SQLiteDatabase db,
1838             @NonNull String path, long id, long expiredTime) {
1839         final String table = "files";
1840         final String whereClause = MediaColumns._ID + "=?";
1841         final String[] whereArgs = new String[]{String.valueOf(id)};
1842         final ContentValues values = new ContentValues();
1843         values.put(FileColumns.DATA, path);
1844         values.put(FileColumns.DATE_EXPIRES, expiredTime);
1845         final int count = db.update(table, values, whereClause, whereArgs);
1846         return count == 1;
1847     }
1848 
renameInLowerFsAndInvalidateFuseDentry(@onNull String originalPath, @NonNull String newPath)1849     private boolean renameInLowerFsAndInvalidateFuseDentry(@NonNull String originalPath,
1850             @NonNull String newPath) {
1851         try {
1852             Os.rename(originalPath, newPath);
1853             invalidateFuseDentry(originalPath);
1854             invalidateFuseDentry(newPath);
1855             return true;
1856         } catch (ErrnoException e) {
1857             final String errorMessage = "Rename " + originalPath + " to " + newPath
1858                     + " in lower file system for extending item failed.";
1859             Log.e(TAG, errorMessage, e);
1860         }
1861         return false;
1862     }
1863 
onIdleMaintenanceStopped()1864     public void onIdleMaintenanceStopped() {
1865         mMediaScanner.onIdleScanStopped();
1866     }
1867 
1868     /**
1869      * Orphan any content of the given package. This will delete Android/media orphaned files from
1870      * the database.
1871      */
onPackageOrphaned(String packageName, int uid)1872     public void onPackageOrphaned(String packageName, int uid) {
1873         mExternalDatabase.runWithTransaction((db) -> {
1874             final int userId = uid / PER_USER_RANGE;
1875             onPackageOrphaned(db, packageName, userId);
1876             return null;
1877         });
1878     }
1879 
1880     /**
1881      * Orphan any content of the given package from the given database. This will delete
1882      * Android/media files from the database if the underlying file no longer exists.
1883      */
onPackageOrphaned(@onNull SQLiteDatabase db, @NonNull String packageName, int userId)1884     public void onPackageOrphaned(@NonNull SQLiteDatabase db,
1885             @NonNull String packageName, int userId) {
1886         // Delete Android/media entries.
1887         deleteAndroidMediaEntries(db, packageName, userId);
1888         // Orphan rest of entries.
1889         orphanEntries(db, packageName, userId);
1890         mDatabaseBackupAndRecovery.removeOwnerIdToPackageRelation(packageName, userId);
1891         // TODO(b/260685885): Add e2e tests to ensure these are cleared when a package is removed.
1892         mMediaGrants.removeAllMediaGrantsForPackage(packageName, /* reason */ "Package orphaned",
1893                 userId);
1894     }
1895 
deleteAndroidMediaEntries(SQLiteDatabase db, String packageName, int userId)1896     private void deleteAndroidMediaEntries(SQLiteDatabase db, String packageName, int userId) {
1897         String relativePath = "Android/media/" + DatabaseUtils.escapeForLike(packageName) + "/%";
1898         try (Cursor cursor = db.query(
1899                 "files",
1900                 new String[] { MediaColumns._ID, MediaColumns.DATA },
1901                 "relative_path LIKE ? ESCAPE '\\' AND owner_package_name=? AND _user_id=?",
1902                 new String[] { relativePath, packageName, "" + userId },
1903                 /* groupBy= */ null,
1904                 /* having= */ null,
1905                 /* orderBy= */null,
1906                 /* limit= */ null)) {
1907             int countDeleted = 0;
1908             if (cursor != null) {
1909                 while (cursor.moveToNext()) {
1910                     File file = new File(cursor.getString(1));
1911                     // We check for existence to be sure we don't delete files that still exist.
1912                     // This can happen even if the pair (package, userid) is unknown,
1913                     // since some framework implementations may rely on special userids.
1914                     if (!file.exists()) {
1915                         countDeleted +=
1916                                 db.delete("files", "_id=?", new String[]{cursor.getString(0)});
1917                     }
1918                 }
1919             }
1920             Log.d(TAG, "Deleted " + countDeleted + " Android/media items belonging to "
1921                     + packageName + " on " + db.getPath());
1922         }
1923     }
1924 
orphanEntries( @onNull SQLiteDatabase db, @NonNull String packageName, int userId)1925     private void orphanEntries(
1926             @NonNull SQLiteDatabase db, @NonNull String packageName, int userId) {
1927         final ContentValues values = new ContentValues();
1928         values.putNull(FileColumns.OWNER_PACKAGE_NAME);
1929 
1930         final int countOrphaned = db.update("files", values,
1931                 "owner_package_name=? AND _user_id=?", new String[] { packageName, "" + userId });
1932         if (countOrphaned > 0) {
1933             Log.d(TAG, "Orphaned " + countOrphaned + " items belonging to "
1934                     + packageName + " on " + db.getPath());
1935         }
1936     }
1937 
scanDirectory(@onNull File dir, @ScanReason int reason)1938     public void scanDirectory(@NonNull File dir, @ScanReason int reason) {
1939         mMediaScanner.scanDirectory(dir, reason);
1940     }
1941 
scanFile(@onNull File file, @ScanReason int reason)1942     public Uri scanFile(@NonNull File file, @ScanReason int reason) {
1943         return mMediaScanner.scanFile(file, reason);
1944     }
1945 
scanFileAsMediaProvider(File file, int reason)1946     private Uri scanFileAsMediaProvider(File file, int reason) {
1947         final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
1948         try {
1949             return scanFile(file, REASON_DEMAND);
1950         } finally {
1951             restoreLocalCallingIdentity(tokenInner);
1952         }
1953     }
1954 
1955     /**
1956      * Called when a new file is created through FUSE
1957      *
1958      * @param path path of the file that was created
1959      *
1960      * Called from JNI in jni/MediaProviderWrapper.cpp
1961      */
1962     @Keep
onFileCreatedForFuse(String path)1963     public void onFileCreatedForFuse(String path) {
1964         // Make sure we update the quota type of the file
1965         BackgroundThread.getExecutor().execute(() -> {
1966             File file = new File(path);
1967             int mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
1968             updateQuotaTypeForFileInternal(file, mediaType);
1969         });
1970     }
1971 
isAppCloneUserPair(int userId1, int userId2)1972     private boolean isAppCloneUserPair(int userId1, int userId2) {
1973         UserHandle user1 = UserHandle.of(userId1);
1974         UserHandle user2 = UserHandle.of(userId2);
1975         if (SdkLevel.isAtLeastS()) {
1976             if (mUserCache.userSharesMediaWithParent(user1)
1977                     || mUserCache.userSharesMediaWithParent(user2)) {
1978                 return true;
1979             }
1980             if (Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.S) {
1981                 // If we're on S or higher, and we shipped with S or higher, only allow the new
1982                 // app cloning functionality
1983                 return false;
1984             }
1985             // else, fall back to deprecated solution below on updating devices
1986         }
1987         try {
1988             Method isAppCloneUserPair = StorageManager.class.getMethod("isAppCloneUserPair",
1989                 int.class, int.class);
1990             return (Boolean) isAppCloneUserPair.invoke(mStorageManager, userId1, userId2);
1991         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
1992             Log.w(TAG, "isAppCloneUserPair failed. Users: " + userId1 + " and " + userId2);
1993             return false;
1994         }
1995     }
1996 
1997     /**
1998      * Determines whether the passed in userId forms an app clone user pair with user 0.
1999      *
2000      * @param userId user ID to check
2001      *
2002      * Called from JNI in jni/MediaProviderWrapper.cpp
2003      */
2004     @Keep
isAppCloneUserForFuse(int userId)2005     public boolean isAppCloneUserForFuse(int userId) {
2006         if (!isCrossUserEnabled()) {
2007             Log.d(TAG, "CrossUser not enabled.");
2008             return false;
2009         }
2010         boolean result = isAppCloneUserPair(0, userId);
2011 
2012         Log.w(TAG, "isAppCloneUserPair for user " + userId + ": " + result);
2013 
2014         return result;
2015     }
2016 
2017     /**
2018      * Determines if to allow FUSE_LOOKUP for uid. Might allow uids that don't belong to the
2019      * MediaProvider user, depending on OEM configuration.
2020      *
2021      * @param uid linux uid to check
2022      *
2023      * Called from JNI in jni/MediaProviderWrapper.cpp
2024      */
2025     @Keep
shouldAllowLookupForFuse(int uid, int pathUserId)2026     public boolean shouldAllowLookupForFuse(int uid, int pathUserId) {
2027         int callingUserId = uidToUserId(uid);
2028         if (!isCrossUserEnabled()) {
2029             Log.d(TAG, "CrossUser not enabled. Users: " + callingUserId + " and " + pathUserId);
2030             return false;
2031         }
2032 
2033         if (callingUserId != pathUserId && callingUserId != 0 && pathUserId != 0) {
2034             Log.w(TAG, "CrossUser at least one user is 0 check failed. Users: " + callingUserId
2035                     + " and " + pathUserId);
2036             return false;
2037         }
2038 
2039         if (mUserCache.isWorkProfile(callingUserId) || mUserCache.isWorkProfile(pathUserId)) {
2040             // Cross-user lookup not allowed if one user in the pair has a profile owner app
2041             Log.w(TAG, "CrossUser work profile check failed. Users: " + callingUserId + " and "
2042                     + pathUserId);
2043             return false;
2044         }
2045 
2046         boolean result = isAppCloneUserPair(pathUserId, callingUserId);
2047         if (result) {
2048             Log.i(TAG, "CrossUser allowed. Users: " + callingUserId + " and " + pathUserId);
2049         } else {
2050             Log.w(TAG, "CrossUser isAppCloneUserPair check failed. Users: " + callingUserId
2051                     + " and " + pathUserId);
2052         }
2053 
2054         return result;
2055     }
2056 
2057     /**
2058      * Called from FUSE to transform a file
2059      *
2060      * A transform can change the file contents for {@code uid} from {@code src} to {@code dst}
2061      * depending on {@code flags}. This allows the FUSE daemon serve different file contents for
2062      * the same file to different apps.
2063      *
2064      * The only supported transform for now is transcoding which re-encodes a file taken in a modern
2065      * format like HEVC to a legacy format like AVC.
2066      *
2067      * @param src file path to transform
2068      * @param dst file path to save transformed file
2069      * @param flags determines the kind of transform
2070      * @param readUid app that called us requesting transform
2071      * @param openUid app that originally made the open call
2072      * @param mediaCapabilitiesUid app for which the transform decision was made,
2073      *                             0 if decision was made with openUid
2074      *
2075      * Called from JNI in jni/MediaProviderWrapper.cpp
2076      */
2077     @Keep
transformForFuse(String src, String dst, int transforms, int transformsReason, int readUid, int openUid, int mediaCapabilitiesUid)2078     public boolean transformForFuse(String src, String dst, int transforms, int transformsReason,
2079             int readUid, int openUid, int mediaCapabilitiesUid) {
2080         if ((transforms & FLAG_TRANSFORM_TRANSCODING) != 0) {
2081             if (mTranscodeHelper.isTranscodeFileCached(src, dst)) {
2082                 Log.d(TAG, "Using transcode cache for " + src);
2083                 return true;
2084             }
2085 
2086             // In general we always mark the opener as causing transcoding.
2087             // However, if the mediaCapabilitiesUid is available then we mark the reader as causing
2088             // transcoding.  This handles the case where a malicious app might want to take
2089             // advantage of mediaCapabilitiesUid by setting it to another app's uid and reading the
2090             // media contents itself; in such cases we'd mark the reader (malicious app) for the
2091             // cost of transcoding.
2092             //
2093             //                     openUid             readUid                mediaCapabilitiesUid
2094             // -------------------------------------------------------------------------------------
2095             // using picker         SAF                 app                           app
2096             // abusive case        bad app             bad app                       victim
2097             // modern to lega-
2098             // -cy sharing         modern              legacy                        legacy
2099             //
2100             // we'd not be here in the below case.
2101             // legacy to mode-
2102             // -rn sharing         legacy              modern                        modern
2103 
2104             int transcodeUid = openUid;
2105             if (mediaCapabilitiesUid > 0) {
2106                 Log.d(TAG, "Fix up transcodeUid to " + readUid + ". openUid " + openUid
2107                         + ", mediaCapabilitiesUid " + mediaCapabilitiesUid);
2108                 transcodeUid = readUid;
2109             }
2110             return mTranscodeHelper.transcode(src, dst, transcodeUid, transformsReason);
2111         }
2112         return true;
2113     }
2114 
2115     /**
2116      * Called from FUSE to get {@link FileLookupResult} for a {@code path} and {@code uid}
2117      *
2118      * {@link FileLookupResult} contains transforms, transforms completion status and ioPath
2119      * for transform lookup query for a file and uid.
2120      *
2121      * @param path file path to get transforms for
2122      * @param uid app requesting IO form kernel
2123      * @param tid FUSE thread id handling IO request from kernel
2124      *
2125      * Called from JNI in jni/MediaProviderWrapper.cpp
2126      */
2127     @Keep
onFileLookupForFuse(String path, int uid, int tid)2128     public FileLookupResult onFileLookupForFuse(String path, int uid, int tid) {
2129         uid = getBinderUidForFuse(uid, tid);
2130         final int userId = uidToUserId(uid);
2131 
2132         if (isSyntheticPath(path, userId)) {
2133             if (isRedactedPath(path, userId)) {
2134                 return handleRedactedFileLookup(uid, path);
2135             } else if (isPickerPath(path, userId)) {
2136                 return handlePickerFileLookup(userId, uid, path);
2137             }
2138 
2139             throw new IllegalStateException("Unexpected synthetic path: " + path);
2140         }
2141 
2142         if (mTranscodeHelper.supportsTranscode(path)) {
2143             return handleTranscodedFileLookup(path, uid, tid);
2144         }
2145 
2146         return new FileLookupResult(/* transforms */ 0, uid, /* ioPath */ "");
2147     }
2148 
handleTranscodedFileLookup(String path, int uid, int tid)2149     private FileLookupResult handleTranscodedFileLookup(String path, int uid, int tid) {
2150         final int transformsReason;
2151         final PendingOpenInfo info;
2152 
2153         synchronized (mPendingOpenInfo) {
2154             info = mPendingOpenInfo.get(tid);
2155         }
2156 
2157         if (info != null && info.uid == uid) {
2158             transformsReason = info.transcodeReason;
2159         } else {
2160             transformsReason = mTranscodeHelper.shouldTranscode(path, uid, null /* bundle */);
2161         }
2162 
2163         if (transformsReason > 0) {
2164             final String ioPath = mTranscodeHelper.prepareIoPath(path, uid);
2165             final boolean transformsComplete = mTranscodeHelper.isTranscodeFileCached(path, ioPath);
2166 
2167             return new FileLookupResult(FLAG_TRANSFORM_TRANSCODING, transformsReason, uid,
2168                     transformsComplete, /* transformsSupported */ true, ioPath);
2169         }
2170 
2171         return new FileLookupResult(/* transforms */ 0, transformsReason, uid,
2172                 /* transformsComplete */ true, /* transformsSupported */ true, "");
2173     }
2174 
handleRedactedFileLookup(int uid, @NonNull String path)2175     private FileLookupResult handleRedactedFileLookup(int uid, @NonNull String path) {
2176         final LocalCallingIdentity token = clearLocalCallingIdentity();
2177         final String fileName = extractFileName(path);
2178 
2179         final DatabaseHelper helper;
2180         try {
2181             helper = getDatabaseForUri(FileUtils.getContentUriForPath(path));
2182         } catch (VolumeNotFoundException e) {
2183             throw new IllegalStateException("Volume not found for file: " + path);
2184         }
2185 
2186         try (final Cursor c = helper.runWithoutTransaction(
2187                 (db) -> db.query("files", new String[]{MediaColumns.DATA},
2188                         FileColumns.REDACTED_URI_ID + "=?", new String[]{fileName}, null, null,
2189                         null))) {
2190             if (c.moveToFirst()) {
2191                 return new FileLookupResult(FLAG_TRANSFORM_REDACTION, uid, c.getString(0));
2192             }
2193 
2194             throw new IllegalStateException("Failed to fetch synthetic redacted path: " + path);
2195         } finally {
2196             restoreLocalCallingIdentity(token);
2197         }
2198     }
2199 
2200     /** TODO(b/242153950) :Add negative tests for permission check of file lookup of synthetic
2201      * paths. */
handlePickerFileLookup(int userId, int uid, @NonNull String path)2202     private FileLookupResult handlePickerFileLookup(int userId, int uid, @NonNull String path) {
2203         final File file = new File(path);
2204         final List<String> syntheticRelativePathSegments =
2205                 extractSyntheticRelativePathSegements(path, userId);
2206         final int segmentCount = syntheticRelativePathSegments.size();
2207 
2208         if (segmentCount < 1 || segmentCount > 5) {
2209             throw new IllegalStateException("Unexpected synthetic picker path: " + file);
2210         }
2211 
2212         final String lastSegment = syntheticRelativePathSegments.get(segmentCount - 1);
2213 
2214         boolean result = false;
2215         switch (segmentCount) {
2216             case 1:
2217                 // .../picker
2218                 if (lastSegment.equals("picker")) {
2219                     result = file.exists() || file.mkdir();
2220                 }
2221                 break;
2222             case 2:
2223                 // .../picker/<user-id>
2224                 try {
2225                     Integer.parseInt(lastSegment);
2226                     result = file.exists() || file.mkdir();
2227                 } catch (NumberFormatException e) {
2228                     Log.w(TAG, "Invalid user id for picker file lookup: " + lastSegment
2229                             + ". File: " + file);
2230                 }
2231                 break;
2232             case 3:
2233                 // .../picker/<user-id>/<authority>
2234                 result = preparePickerAuthorityPathSegment(file, lastSegment, uid);
2235                 break;
2236             case 4:
2237                 // .../picker/<user-id>/<authority>/media
2238                 if (lastSegment.equals("media")) {
2239                     result = file.exists() || file.mkdir();
2240                 }
2241                 break;
2242             case 5:
2243                 // .../picker/<user-id>/<authority>/media/<media-id.extension>
2244                 final String fileUserId = syntheticRelativePathSegments.get(1);
2245                 final String authority = syntheticRelativePathSegments.get(2);
2246                 result = preparePickerMediaIdPathSegment(file, authority, lastSegment, fileUserId,
2247                         uid);
2248                 break;
2249         }
2250 
2251         if (result) {
2252             return new FileLookupResult(FLAG_TRANSFORM_PICKER, uid, path);
2253         }
2254         throw new IllegalStateException("Failed to prepare synthetic picker path: " + file);
2255     }
2256 
handlePickerFileOpen(String path, int uid)2257     private FileOpenResult handlePickerFileOpen(String path, int uid) {
2258         final String[] segments = path.split("/");
2259         if (segments.length != 11) {
2260             Log.e(TAG, "Picker file open failed. Unexpected segments: " + path);
2261             return new FileOpenResult(OsConstants.ENOENT /* status */, uid, /* transformsUid */ 0,
2262                     new long[0]);
2263         }
2264 
2265         // ['', 'storage', 'emulated', '0', 'transforms', 'synthetic', 'picker', '<user-id>',
2266         // '<host>', 'media', '<fileName>']
2267         final String userId = segments[7];
2268         final String fileName = segments[10];
2269         final String host = segments[8];
2270         final String authority = userId + "@" + host;
2271         final int lastDotIndex = fileName.lastIndexOf('.');
2272 
2273         if (lastDotIndex == -1) {
2274             Log.e(TAG, "Picker file open failed. No file extension: " + path);
2275             return FileOpenResult.createError(OsConstants.ENOENT, uid);
2276         }
2277 
2278         final String mediaId = fileName.substring(0, lastDotIndex);
2279         final Uri uri = getMediaUri(authority).buildUpon().appendPath(mediaId).build();
2280 
2281         IBinder binder = getContext().getContentResolver()
2282                 .call(uri, METHOD_GET_ASYNC_CONTENT_PROVIDER, null, null)
2283                 .getBinder(EXTRA_ASYNC_CONTENT_PROVIDER);
2284         if (binder == null) {
2285             Log.e(TAG, "Picker file open failed. No cloud media provider found.");
2286             return FileOpenResult.createError(OsConstants.ENOENT, uid);
2287         }
2288         IAsyncContentProvider iAsyncContentProvider = IAsyncContentProvider.Stub.asInterface(
2289                 binder);
2290         AsyncContentProvider asyncContentProvider = new AsyncContentProvider(iAsyncContentProvider);
2291         final ParcelFileDescriptor pfd;
2292         try {
2293             pfd = asyncContentProvider.openMedia(uri, "r");
2294         } catch (FileNotFoundException | ExecutionException | InterruptedException
2295                 | TimeoutException | RemoteException e) {
2296             Log.e(TAG, "Picker file open failed. Failed to open URI: " + uri, e);
2297             return FileOpenResult.createError(OsConstants.ENOENT, uid);
2298         }
2299 
2300         try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {
2301             final String mimeType = MimeUtils.resolveMimeType(new File(path));
2302             final long[] redactionRanges = getRedactionRanges(fis, mimeType).redactionRanges;
2303             return new FileOpenResult(0 /* status */, uid, /* transformsUid */ 0,
2304                     /* nativeFd */ pfd.detachFd(), redactionRanges);
2305         } catch (IOException e) {
2306             Log.e(TAG, "Picker file open failed. No file extension: " + path, e);
2307             return FileOpenResult.createError(OsConstants.ENOENT, uid);
2308         }
2309     }
2310 
preparePickerAuthorityPathSegment(File file, String authority, int uid)2311     private boolean preparePickerAuthorityPathSegment(File file, String authority, int uid) {
2312         if (mPickerSyncController.isProviderEnabled(authority)) {
2313             return file.exists() || file.mkdir();
2314         }
2315 
2316         return false;
2317     }
2318 
preparePickerMediaIdPathSegment(File file, String authority, String fileName, String userId, int uid)2319     private boolean preparePickerMediaIdPathSegment(File file, String authority, String fileName,
2320             String userId, int uid) {
2321         final String mediaId = extractFileName(fileName);
2322         final String[] projection = new String[] { MediaStore.PickerMediaColumns.SIZE };
2323 
2324         final Uri uri = Uri.parse("content://media/picker/" + userId + "/" + authority + "/media/"
2325                 + mediaId);
2326         try (Cursor cursor = mPickerUriResolver.query(uri, projection, /* callingPid */0, uid,
2327                 mCallingIdentity.get().getPackageName())) {
2328             if (cursor != null && cursor.moveToFirst()) {
2329                 final int sizeBytesIdx = cursor.getColumnIndex(MediaStore.PickerMediaColumns.SIZE);
2330 
2331                 if (sizeBytesIdx != -1) {
2332                     return createSparseFile(file, cursor.getLong(sizeBytesIdx));
2333                 }
2334             }
2335         }
2336 
2337         return false;
2338     }
2339 
getBinderUidForFuse(int uid, int tid)2340     public int getBinderUidForFuse(int uid, int tid) {
2341         if (uid != MY_UID) {
2342             return uid;
2343         }
2344 
2345         synchronized (mPendingOpenInfo) {
2346             PendingOpenInfo info = mPendingOpenInfo.get(tid);
2347             if (info == null) {
2348                 return uid;
2349             }
2350             return info.uid;
2351         }
2352     }
2353 
uidToUserId(int uid)2354     private static int uidToUserId(int uid) {
2355         return uid / PER_USER_RANGE;
2356     }
2357 
2358     /**
2359      * Returns true if the app denoted by the given {@code uid} and {@code packageName} is allowed
2360      * to clear other apps' cache directories.
2361      */
hasPermissionToClearCaches(Context context, ApplicationInfo ai)2362     static boolean hasPermissionToClearCaches(Context context, ApplicationInfo ai) {
2363         PermissionUtils.setOpDescription("clear app cache");
2364         try {
2365             return PermissionUtils.checkPermissionManager(context, /* pid */ -1, ai.uid,
2366                     ai.packageName, /* attributionTag */ null);
2367         } finally {
2368             PermissionUtils.clearOpDescription();
2369         }
2370     }
2371 
2372     @VisibleForTesting
computeAudioLocalizedValues(ContentValues values)2373     void computeAudioLocalizedValues(ContentValues values) {
2374         try {
2375             final String title = values.getAsString(AudioColumns.TITLE);
2376             final String titleRes = values.getAsString(AudioColumns.TITLE_RESOURCE_URI);
2377 
2378             if (!TextUtils.isEmpty(titleRes)) {
2379                 final String localized = getLocalizedTitle(titleRes);
2380                 if (!TextUtils.isEmpty(localized)) {
2381                     values.put(AudioColumns.TITLE, localized);
2382                 }
2383             } else {
2384                 final String localized = getLocalizedTitle(title);
2385                 if (!TextUtils.isEmpty(localized)) {
2386                     values.put(AudioColumns.TITLE, localized);
2387                     values.put(AudioColumns.TITLE_RESOURCE_URI, title);
2388                 }
2389             }
2390         } catch (Exception e) {
2391             Log.w(TAG, "Failed to localize title", e);
2392         }
2393     }
2394 
2395     @VisibleForTesting
computeAudioKeyValues(ContentValues values)2396     static void computeAudioKeyValues(ContentValues values) {
2397         computeAudioKeyValue(values, AudioColumns.TITLE, AudioColumns.TITLE_KEY, /* focusId */
2398                 null, /* hashValue */ 0);
2399         computeAudioKeyValue(values, AudioColumns.ARTIST, AudioColumns.ARTIST_KEY,
2400                 AudioColumns.ARTIST_ID, /* hashValue */ 0);
2401         computeAudioKeyValue(values, AudioColumns.GENRE, AudioColumns.GENRE_KEY,
2402                 AudioColumns.GENRE_ID, /* hashValue */ 0);
2403         computeAudioAlbumKeyValue(values);
2404     }
2405 
2406     /**
2407      * To distinguish same-named albums, we append a hash. The hash is
2408      * based on the "album artist" tag if present, otherwise on the path of
2409      * the parent directory of the audio file.
2410      */
computeAudioAlbumKeyValue(ContentValues values)2411     private static void computeAudioAlbumKeyValue(ContentValues values) {
2412         int hashCode = 0;
2413 
2414         final String albumArtist = values.getAsString(MediaColumns.ALBUM_ARTIST);
2415         if (!TextUtils.isEmpty(albumArtist)) {
2416             hashCode = albumArtist.hashCode();
2417         } else {
2418             final String path = values.getAsString(MediaColumns.DATA);
2419             if (!TextUtils.isEmpty(path)) {
2420                 hashCode = path.substring(0, path.lastIndexOf('/')).hashCode();
2421             }
2422         }
2423 
2424         computeAudioKeyValue(values, AudioColumns.ALBUM, AudioColumns.ALBUM_KEY,
2425                 AudioColumns.ALBUM_ID, hashCode);
2426     }
2427 
computeAudioKeyValue(@onNull ContentValues values, @NonNull String focus, @Nullable String focusKey, @Nullable String focusId, int hashValue)2428     private static void computeAudioKeyValue(@NonNull ContentValues values, @NonNull String focus,
2429             @Nullable String focusKey, @Nullable String focusId, int hashValue) {
2430         if (focusKey != null) values.remove(focusKey);
2431         if (focusId != null) values.remove(focusId);
2432 
2433         final String value = values.getAsString(focus);
2434         if (TextUtils.isEmpty(value)) return;
2435 
2436         final String key = Audio.keyFor(value);
2437         if (key == null) return;
2438 
2439         if (focusKey != null) {
2440             values.put(focusKey, key);
2441         }
2442         if (focusId != null) {
2443             // Many apps break if we generate negative IDs, so trim off the
2444             // highest bit to ensure we're always unsigned
2445             final long id = Hashing.farmHashFingerprint64().hashString(key + hashValue,
2446                     StandardCharsets.UTF_8).asLong() & ~(1L << 63);
2447             values.put(focusId, id);
2448         }
2449     }
2450 
2451     @Override
canonicalize(Uri uri)2452     public Uri canonicalize(Uri uri) {
2453         final boolean allowHidden = isCallingPackageAllowedHidden();
2454         final int match = matchUri(uri, allowHidden);
2455 
2456         // Skip when we have nothing to canonicalize
2457         if ("1".equals(uri.getQueryParameter(CANONICAL))) {
2458             return uri;
2459         }
2460 
2461         try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
2462             switch (match) {
2463                 case AUDIO_MEDIA_ID: {
2464                     final String title = getDefaultTitleFromCursor(c);
2465                     if (!TextUtils.isEmpty(title)) {
2466                         final Uri.Builder builder = uri.buildUpon();
2467                         builder.appendQueryParameter(AudioColumns.TITLE, title);
2468                         builder.appendQueryParameter(CANONICAL, "1");
2469                         return builder.build();
2470                     }
2471                     break;
2472                 }
2473                 case VIDEO_MEDIA_ID:
2474                 case IMAGES_MEDIA_ID: {
2475                     final String documentId = c
2476                             .getString(c.getColumnIndexOrThrow(MediaColumns.DOCUMENT_ID));
2477                     if (!TextUtils.isEmpty(documentId)) {
2478                         final Uri.Builder builder = uri.buildUpon();
2479                         builder.appendQueryParameter(MediaColumns.DOCUMENT_ID, documentId);
2480                         builder.appendQueryParameter(CANONICAL, "1");
2481                         return builder.build();
2482                     }
2483                     break;
2484                 }
2485             }
2486         } catch (FileNotFoundException e) {
2487             Log.w(TAG, e.getMessage());
2488         }
2489         return null;
2490     }
2491 
2492     @Override
uncanonicalize(Uri uri)2493     public Uri uncanonicalize(Uri uri) {
2494         final boolean allowHidden = isCallingPackageAllowedHidden();
2495         final int match = matchUri(uri, allowHidden);
2496 
2497         // Skip when we have nothing to uncanonicalize
2498         if (!"1".equals(uri.getQueryParameter(CANONICAL))) {
2499             return uri;
2500         }
2501 
2502         // Extract values and then clear to avoid recursive lookups
2503         final String title = uri.getQueryParameter(AudioColumns.TITLE);
2504         final String documentId = uri.getQueryParameter(MediaColumns.DOCUMENT_ID);
2505         uri = uri.buildUpon().clearQuery().build();
2506 
2507         switch (match) {
2508             case AUDIO_MEDIA_ID: {
2509                 // First check for an exact match
2510                 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
2511                     if (Objects.equals(title, getDefaultTitleFromCursor(c))) {
2512                         return uri;
2513                     }
2514                 } catch (FileNotFoundException e) {
2515                     Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e);
2516                 }
2517 
2518                 // Otherwise fallback to searching
2519                 final Uri baseUri = ContentUris.removeId(uri);
2520                 try (Cursor c = queryForSingleItem(baseUri,
2521                         new String[] { BaseColumns._ID },
2522                         AudioColumns.TITLE + "=?", new String[] { title }, null)) {
2523                     return ContentUris.withAppendedId(baseUri, c.getLong(0));
2524                 } catch (FileNotFoundException e) {
2525                     Log.w(TAG, "Failed to resolve " + uri + ": " + e);
2526                     return null;
2527                 }
2528             }
2529             case VIDEO_MEDIA_ID:
2530             case IMAGES_MEDIA_ID: {
2531                 // First check for an exact match
2532                 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
2533                     if (Objects.equals(title, getDefaultTitleFromCursor(c))) {
2534                         return uri;
2535                     }
2536                 } catch (FileNotFoundException e) {
2537                     Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e);
2538                 }
2539 
2540                 // Otherwise fallback to searching
2541                 final Uri baseUri = ContentUris.removeId(uri);
2542                 try (Cursor c = queryForSingleItem(baseUri,
2543                         new String[] { BaseColumns._ID },
2544                         MediaColumns.DOCUMENT_ID + "=?", new String[] { documentId }, null)) {
2545                     return ContentUris.withAppendedId(baseUri, c.getLong(0));
2546                 } catch (FileNotFoundException e) {
2547                     Log.w(TAG, "Failed to resolve " + uri + ": " + e);
2548                     return null;
2549                 }
2550             }
2551         }
2552 
2553         return uri;
2554     }
2555 
safeUncanonicalize(Uri uri)2556     private Uri safeUncanonicalize(Uri uri) {
2557         Uri newUri = uncanonicalize(uri);
2558         if (newUri != null) {
2559             return newUri;
2560         }
2561         return uri;
2562     }
2563 
2564     /**
2565      * @return where clause to exclude database rows where
2566      * <ul>
2567      * <li> {@code column} is set or
2568      * <li> {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE and not owned by
2569      * calling package.
2570      * <li> {@code column} is {@link MediaColumns#IS_PENDING}, is unset and is waiting for
2571      * metadata update from a deferred scan.
2572      * </ul>
2573      */
getWhereClauseForMatchExclude(@onNull String column)2574     private String getWhereClauseForMatchExclude(@NonNull String column) {
2575         if (column.equalsIgnoreCase(MediaColumns.IS_PENDING)) {
2576             // Don't include rows that are pending for metadata
2577             final String pendingForMetadata = FileColumns._MODIFIER + "="
2578                     + FileColumns._MODIFIER_CR_PENDING_METADATA;
2579             final String notPending = String.format("(%s=0 AND NOT %s)", column,
2580                     pendingForMetadata);
2581 
2582             // Include owned pending files from Fuse
2583             final String pendingFromFuse = String.format("(%s=1 AND %s AND %s)", column,
2584                     MATCH_PENDING_FROM_FUSE, getWhereForOwnerPackageMatch(mCallingIdentity.get()));
2585 
2586             return "(" + notPending + " OR " + pendingFromFuse + ")";
2587         }
2588         return column + "=0";
2589     }
2590 
2591     /**
2592      * @return where clause to include database rows where
2593      * <ul>
2594      * <li> {@code column} is not set or
2595      * <li> {@code column} is set and calling package has write permission to corresponding db row
2596      *      or {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE.
2597      * </ul>
2598      * The method is used to match db rows corresponding to writable pending and trashed files.
2599      */
2600     @Nullable
getWhereClauseForMatchableVisibleFromFilePath(@onNull Uri uri, @NonNull String column)2601     private String getWhereClauseForMatchableVisibleFromFilePath(@NonNull Uri uri,
2602             @NonNull String column) {
2603         if (checkCallingPermissionGlobal(uri, /*forWrite*/ true)) {
2604             // No special filtering needed
2605             return null;
2606         }
2607 
2608         int uriType = matchUri(uri, isCallingPackageAllowedHidden());
2609         if (hasAccessToCollection(mCallingIdentity.get(), uriType, /* forWrite */ true)) {
2610             // has direct write access to whole collection, no special filtering needed.
2611             return null;
2612         }
2613 
2614         final String writeAccessCheckSql = getWhereForConstrainedAccess(mCallingIdentity.get(),
2615                 uriType, /* forWrite */ true, Bundle.EMPTY);
2616 
2617         final String matchWritableRowsClause = String.format("%s=0 OR (%s=1 AND (%s OR %s))",
2618                 column, column, MATCH_PENDING_FROM_FUSE, writeAccessCheckSql);
2619 
2620         return matchWritableRowsClause;
2621     }
2622 
2623     /**
2624      * Gets list of files in {@code path} from media provider database.
2625      *
2626      * @param path path of the directory.
2627      * @param uid UID of the calling process.
2628      * @return a list of file names in the given directory path.
2629      * An empty list is returned if no files are visible to the calling app or the given directory
2630      * does not have any files.
2631      * A list with ["/"] is returned if the path is not indexed by MediaProvider database or
2632      * calling package is a legacy app and has appropriate storage permissions for the given path.
2633      * In both scenarios file names should be obtained from lower file system.
2634      * A list with empty string[""] is returned if the calling package doesn't have access to the
2635      * given path.
2636      *
2637      * <p>Directory names are always obtained from lower file system.
2638      *
2639      * Called from JNI in jni/MediaProviderWrapper.cpp
2640      */
2641     @Keep
getFilesInDirectoryForFuse(String path, int uid)2642     public String[] getFilesInDirectoryForFuse(String path, int uid) {
2643         final LocalCallingIdentity token =
2644                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
2645         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
2646 
2647         try {
2648             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
2649                 return new String[] {""};
2650             }
2651 
2652             if (shouldBypassFuseRestrictions(/*forWrite*/ false, path)) {
2653                 return new String[] {"/"};
2654             }
2655 
2656             // Do not allow apps to list Android/data or Android/obb dirs.
2657             // On primary volumes, apps that get special access to these directories get it via
2658             // mount views of lowerfs. On secondary volumes, such apps would return early from
2659             // shouldBypassFuseRestrictions above.
2660             if (isDataOrObbPath(path)) {
2661                 return new String[] {""};
2662             }
2663 
2664             // Legacy apps that made is this far don't have the right storage permission and hence
2665             // are not allowed to access anything other than their external app directory
2666             if (isCallingPackageRequestingLegacy()) {
2667                 return new String[] {""};
2668             }
2669 
2670             // Get relative path for the contents of given directory.
2671             String relativePath = extractRelativePathWithDisplayName(path);
2672             if (relativePath == null) {
2673                 // Path is /storage/emulated/, if relativePath is null, MediaProvider doesn't
2674                 // have any details about the given directory. Use lower file system to obtain
2675                 // files and directories in the given directory.
2676                 return new String[] {"/"};
2677             }
2678             // Getting UserId from the directory path, as clone user shares the MediaProvider
2679             // of user 0.
2680             int userIdFromPath = FileUtils.extractUserId(path);
2681             // In some cases, like querying public volumes, userId is not available in path. We
2682             // take userId from the user running MediaProvider process (sUserId).
2683             if (userIdFromPath == -1) {
2684                 userIdFromPath = sUserId;
2685             }
2686             // For all other paths, get file names from media provider database.
2687             // Return media and non-media files visible to the calling package.
2688             ArrayList<String> fileNamesList = new ArrayList<>();
2689 
2690             // Only FileColumns.DATA contains actual name of the file.
2691             String[] projection = {MediaColumns.DATA};
2692 
2693             Bundle queryArgs = new Bundle();
2694             queryArgs.putString(QUERY_ARG_SQL_SELECTION, MediaColumns.RELATIVE_PATH +
2695                     " =? and " + FileColumns._USER_ID + " =? and mime_type not like 'null'");
2696             queryArgs.putStringArray(QUERY_ARG_SQL_SELECTION_ARGS, new String[] {relativePath,
2697                     String.valueOf(userIdFromPath)});
2698             // Get database entries for files from MediaProvider database with
2699             // MediaColumns.RELATIVE_PATH as the given path.
2700             try (final Cursor cursor = query(FileUtils.getContentUriForPath(path), projection,
2701                     queryArgs, null)) {
2702                 while(cursor.moveToNext()) {
2703                     fileNamesList.add(extractDisplayName(cursor.getString(0)));
2704                 }
2705             }
2706             return fileNamesList.toArray(new String[fileNamesList.size()]);
2707         } finally {
2708             restoreLocalCallingIdentity(token);
2709         }
2710     }
2711 
2712     /**
2713      * Scan files during directory renames for the following reasons:
2714      * <ul>
2715      * <li>Because we don't update db rows for directories, we scan the oldPath to discard stale
2716      * directory db rows. This prevents conflicts during subsequent db operations with oldPath.
2717      * <li>We need to scan newPath as well, because the new directory may have become hidden
2718      * or unhidden, in which case we need to update the media types of the contained files
2719      * </ul>
2720      */
scanRenamedDirectoryForFuse(@onNull String oldPath, @NonNull String newPath)2721     private void scanRenamedDirectoryForFuse(@NonNull String oldPath, @NonNull String newPath) {
2722         scanFileAsMediaProvider(new File(oldPath), REASON_DEMAND);
2723         scanFileAsMediaProvider(new File(newPath), REASON_DEMAND);
2724     }
2725 
2726     /**
2727      * Checks if given {@code mimeType} is supported in {@code path}.
2728      */
isMimeTypeSupportedInPath(String path, String mimeType)2729     private boolean isMimeTypeSupportedInPath(String path, String mimeType) {
2730         final String supportedPrimaryMimeType;
2731         final int match = matchUri(getContentUriForFile(path, mimeType), true);
2732         switch (match) {
2733             case AUDIO_MEDIA:
2734                 supportedPrimaryMimeType = "audio";
2735                 break;
2736             case VIDEO_MEDIA:
2737                 supportedPrimaryMimeType = "video";
2738                 break;
2739             case IMAGES_MEDIA:
2740                 supportedPrimaryMimeType = "image";
2741                 break;
2742             default:
2743                 supportedPrimaryMimeType = ClipDescription.MIMETYPE_UNKNOWN;
2744         }
2745         return (supportedPrimaryMimeType.equalsIgnoreCase(ClipDescription.MIMETYPE_UNKNOWN) ||
2746                 StringUtils.startsWithIgnoreCase(mimeType, supportedPrimaryMimeType));
2747     }
2748 
2749     /**
2750      * Removes owner package for the renamed path if the calling package doesn't own the db row
2751      *
2752      * When oldPath is renamed to newPath, if newPath exists in the database, and caller is not the
2753      * owner of the file, owner package is set to 'null'. This prevents previous owner of newPath
2754      * from accessing renamed file.
2755      * @return {@code true} if
2756      * <ul>
2757      * <li> there is no corresponding database row for given {@code path}
2758      * <li> shared calling package is the owner of the database row
2759      * <li> owner package name is already set to 'null'
2760      * <li> updating owner package name to 'null' was successful.
2761      * </ul>
2762      * Returns {@code false} otherwise.
2763      */
maybeRemoveOwnerPackageForFuseRename(@onNull DatabaseHelper helper, @NonNull String path)2764     private boolean maybeRemoveOwnerPackageForFuseRename(@NonNull DatabaseHelper helper,
2765             @NonNull String path) {
2766         final Uri uri = FileUtils.getContentUriForPath(path);
2767         final int match = matchUri(uri, isCallingPackageAllowedHidden());
2768         final String ownerPackageName;
2769         final String selection = MediaColumns.DATA + " =? AND "
2770                 + MediaColumns.OWNER_PACKAGE_NAME + " != 'null'";
2771         final String[] selectionArgs = new String[] {path};
2772 
2773         final SQLiteQueryBuilder qbForQuery =
2774                 getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, null);
2775         try (Cursor c = qbForQuery.query(helper, new String[] {FileColumns.OWNER_PACKAGE_NAME},
2776                 selection, selectionArgs, null, null, null, null, null)) {
2777             if (!c.moveToFirst()) {
2778                 // We don't need to remove owner_package from db row if path doesn't exist in
2779                 // database or owner_package is already set to 'null'
2780                 return true;
2781             }
2782             ownerPackageName = c.getString(0);
2783             if (isCallingIdentitySharedPackageName(ownerPackageName)) {
2784                 // We don't need to remove owner_package from db row if calling package is the owner
2785                 // of the database row
2786                 return true;
2787             }
2788         }
2789 
2790         final SQLiteQueryBuilder qbForUpdate =
2791                 getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null);
2792         ContentValues values = new ContentValues();
2793         values.put(FileColumns.OWNER_PACKAGE_NAME, "null");
2794         return qbForUpdate.update(helper, values, selection, selectionArgs) == 1;
2795     }
2796 
updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values)2797     private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
2798             @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) {
2799         return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY);
2800     }
2801 
updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, @NonNull Bundle qbExtras)2802     private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
2803             @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values,
2804             @NonNull Bundle qbExtras) {
2805         return updateDatabaseForFuseRename(helper, oldPath, newPath, values, qbExtras,
2806                 FileUtils.getContentUriForPath(oldPath));
2807     }
2808 
2809     /**
2810      * Updates database entry for given {@code path} with {@code values}
2811      */
updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, @NonNull Bundle qbExtras, Uri uriOldPath)2812     private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
2813             @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values,
2814             @NonNull Bundle qbExtras, Uri uriOldPath) {
2815         boolean allowHidden = isCallingPackageAllowedHidden();
2816         final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE,
2817                 matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null);
2818 
2819         // uriOldPath may use Files uri which doesn't allow modifying AudioColumns. Include
2820         // AudioColumns projection map if we are modifying any audio columns while renaming
2821         // database rows.
2822         if (values.containsKey(AudioColumns.IS_RINGTONE)) {
2823             qbForUpdate.setProjectionMap(getProjectionMap(AudioColumns.class, FileColumns.class));
2824         }
2825 
2826         if (values.containsKey(FileColumns._MODIFIER)) {
2827             qbForUpdate.allowColumn(FileColumns._MODIFIER);
2828         }
2829 
2830         final String selection = MediaColumns.DATA + " =? ";
2831         int count = 0;
2832         boolean retryUpdateWithReplace = false;
2833 
2834         try {
2835             // TODO(b/146777893): System gallery apps can rename a media directory containing
2836             // non-media files. This update doesn't support updating non-media files that are not
2837             // owned by system gallery app.
2838             count = qbForUpdate.update(helper, values, selection, new String[]{oldPath});
2839         } catch (SQLiteConstraintException e) {
2840             Log.w(TAG, "Database update failed while renaming " + oldPath, e);
2841             retryUpdateWithReplace = true;
2842         }
2843 
2844         if (retryUpdateWithReplace) {
2845             if (deleteForFuseRename(helper, oldPath, newPath, qbExtras, selection, allowHidden)) {
2846                 Log.i(TAG, "Retrying database update after deleting conflicting entry");
2847                 count = qbForUpdate.update(helper, values, selection, new String[]{oldPath});
2848             } else {
2849                 return false;
2850             }
2851         }
2852         return count == 1;
2853     }
2854 
deleteForFuseRename(DatabaseHelper helper, String oldPath, String newPath, Bundle qbExtras, String selection, boolean allowHidden)2855     private boolean deleteForFuseRename(DatabaseHelper helper, String oldPath,
2856             String newPath, Bundle qbExtras, String selection, boolean allowHidden) {
2857         // We are replacing file in newPath with file in oldPath. If calling package has
2858         // write permission for newPath, delete existing database entry and retry update.
2859         final Uri uriNewPath = FileUtils.getContentUriForPath(oldPath);
2860         final SQLiteQueryBuilder qbForDelete = getQueryBuilder(TYPE_DELETE,
2861                 matchUri(uriNewPath, allowHidden), uriNewPath, qbExtras, null);
2862         if (qbForDelete.delete(helper, selection, new String[] {newPath}) == 1) {
2863             return true;
2864         }
2865         // Check if delete can be done using other URI grants
2866         final String[] projection = new String[] {
2867                 FileColumns.MEDIA_TYPE,
2868                 FileColumns.DATA,
2869                 FileColumns._ID,
2870                 FileColumns.IS_DOWNLOAD,
2871                 FileColumns.MIME_TYPE,
2872         };
2873         return
2874             deleteWithOtherUriGrants(
2875                     FileUtils.getContentUriForPath(newPath),
2876                     helper, projection, selection, new String[] {newPath}, qbExtras) == 1;
2877     }
2878 
2879     /**
2880      * Gets {@link ContentValues} for updating database entry to {@code path}.
2881      */
getContentValuesForFuseRename(String path, String newMimeType, boolean wasHidden, boolean isHidden, boolean isSameMimeType)2882     private ContentValues getContentValuesForFuseRename(String path, String newMimeType,
2883             boolean wasHidden, boolean isHidden, boolean isSameMimeType) {
2884         ContentValues values = new ContentValues();
2885         values.put(MediaColumns.MIME_TYPE, newMimeType);
2886         values.put(MediaColumns.DATA, path);
2887 
2888         if (isHidden) {
2889             values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
2890         } else {
2891             int mediaType = MimeUtils.resolveMediaType(newMimeType);
2892             values.put(FileColumns.MEDIA_TYPE, mediaType);
2893         }
2894 
2895         if ((!isHidden && wasHidden) || !isSameMimeType) {
2896             // Set the modifier as MODIFIER_FUSE so that apps can scan the file to update the
2897             // metadata. Otherwise, scan will skip scanning this file because rename() doesn't
2898             // change lastModifiedTime and scan assumes there is no change in the file.
2899             values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE);
2900         }
2901 
2902         if (MimeUtils.isAudioMimeType(newMimeType) && !values.containsKey(FileColumns._MODIFIER)) {
2903             computeAudioLocalizedValues(values);
2904             computeAudioKeyValues(values);
2905             FileUtils.computeAudioTypeValuesFromData(path, values::put);
2906         }
2907 
2908         FileUtils.computeValuesFromData(values, isFuseThread());
2909         return values;
2910     }
2911 
getIncludedDefaultDirectories()2912     private ArrayList<String> getIncludedDefaultDirectories() {
2913         final ArrayList<String> includedDefaultDirs = new ArrayList<>();
2914         if (mCallingIdentity.get().checkCallingPermissionVideo(/* forWrite */ true)) {
2915             includedDefaultDirs.add(Environment.DIRECTORY_DCIM);
2916             includedDefaultDirs.add(Environment.DIRECTORY_PICTURES);
2917             includedDefaultDirs.add(Environment.DIRECTORY_MOVIES);
2918         } else if (mCallingIdentity.get().checkCallingPermissionImages(/* forWrite */ true)) {
2919             includedDefaultDirs.add(Environment.DIRECTORY_DCIM);
2920             includedDefaultDirs.add(Environment.DIRECTORY_PICTURES);
2921         }
2922         return includedDefaultDirs;
2923     }
2924 
2925     /**
2926      * Gets all files in the given {@code path} and subdirectories of the given {@code path}.
2927      */
getAllFilesForRenameDirectory(String oldPath)2928     private ArrayList<String> getAllFilesForRenameDirectory(String oldPath) {
2929         final String selection = FileColumns.DATA + " LIKE ? ESCAPE '\\'"
2930                 + " and mime_type not like 'null'";
2931         final String[] selectionArgs = new String[] {DatabaseUtils.escapeForLike(oldPath) + "/%"};
2932         ArrayList<String> fileList = new ArrayList<>();
2933 
2934         final LocalCallingIdentity token = clearLocalCallingIdentity();
2935         try (final Cursor c = query(FileUtils.getContentUriForPath(oldPath),
2936                 new String[] {MediaColumns.DATA}, selection, selectionArgs, null)) {
2937             while (c.moveToNext()) {
2938                 String filePath = c.getString(0);
2939                 filePath = filePath.replaceFirst(Pattern.quote(oldPath + "/"), "");
2940                 fileList.add(filePath);
2941             }
2942         } finally {
2943             restoreLocalCallingIdentity(token);
2944         }
2945         return fileList;
2946     }
2947 
2948     /**
2949      * Gets files in the given {@code path} and subdirectories of the given {@code path} for which
2950      * calling package has write permissions.
2951      *
2952      * This method throws {@code IllegalArgumentException} if the directory has one or more
2953      * files for which calling package doesn't have write permission or if file type is not
2954      * supported in {@code newPath}
2955      */
getWritableFilesForRenameDirectory(String oldPath, String newPath)2956     private ArrayList<String> getWritableFilesForRenameDirectory(String oldPath, String newPath)
2957             throws IllegalArgumentException {
2958         // Try a simple check to see if the caller has full access to the given collections first
2959         // before falling back to performing a query to probe for access.
2960         final String oldRelativePath = extractRelativePathWithDisplayName(oldPath);
2961         final String newRelativePath = extractRelativePathWithDisplayName(newPath);
2962         boolean hasFullAccessToOldPath = false;
2963         boolean hasFullAccessToNewPath = false;
2964         for (String defaultDir : getIncludedDefaultDirectories()) {
2965             if (oldRelativePath.startsWith(defaultDir)) hasFullAccessToOldPath = true;
2966             if (newRelativePath.startsWith(defaultDir)) hasFullAccessToNewPath = true;
2967         }
2968         if (hasFullAccessToNewPath && hasFullAccessToOldPath) {
2969             return getAllFilesForRenameDirectory(oldPath);
2970         }
2971 
2972         final int countAllFilesInDirectory;
2973         final String selection = FileColumns.DATA + " LIKE ? ESCAPE '\\'"
2974                 + " and mime_type not like 'null'";
2975         final String[] selectionArgs = new String[] {DatabaseUtils.escapeForLike(oldPath) + "/%"};
2976 
2977         final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath);
2978 
2979         final LocalCallingIdentity token = clearLocalCallingIdentity();
2980         try (final Cursor c = query(uriOldPath, new String[] {MediaColumns._ID}, selection,
2981                 selectionArgs, null)) {
2982             // get actual number of files in the given directory.
2983             countAllFilesInDirectory = c.getCount();
2984         } finally {
2985             restoreLocalCallingIdentity(token);
2986         }
2987 
2988         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE,
2989                 matchUri(uriOldPath, isCallingPackageAllowedHidden()), uriOldPath, Bundle.EMPTY,
2990                 null);
2991         final DatabaseHelper helper;
2992         try {
2993             helper = getDatabaseForUri(uriOldPath);
2994         } catch (VolumeNotFoundException e) {
2995             throw new IllegalStateException("Volume not found while querying files for renaming "
2996                     + oldPath);
2997         }
2998 
2999         ArrayList<String> fileList = new ArrayList<>();
3000         final String[] projection = {MediaColumns.DATA, MediaColumns.MIME_TYPE};
3001         try (Cursor c = qb.query(helper, projection, selection, selectionArgs, null, null, null,
3002                 null, null)) {
3003             // Check if the calling package has write permission to all files in the given
3004             // directory. If calling package has write permission to all files in the directory, the
3005             // query with update uri should return same number of files as previous query.
3006             if (c.getCount() != countAllFilesInDirectory) {
3007                 throw new IllegalArgumentException("Calling package doesn't have write permission "
3008                         + " to rename one or more files in " + oldPath);
3009             }
3010             while(c.moveToNext()) {
3011                 String filePath = c.getString(0);
3012                 filePath = filePath.replaceFirst(Pattern.quote(oldPath + "/"), "");
3013 
3014                 final String mimeType = c.getString(1);
3015                 if (!isMimeTypeSupportedInPath(newPath + "/" + filePath, mimeType)) {
3016                     throw new IllegalArgumentException("Can't rename " + oldPath + "/" + filePath
3017                             + ". Mime type " + mimeType + " not supported in " + newPath);
3018                 }
3019                 fileList.add(filePath);
3020             }
3021         }
3022         return fileList;
3023     }
3024 
renameInLowerFs(String oldPath, String newPath)3025     private int renameInLowerFs(String oldPath, String newPath) {
3026         try {
3027             Os.rename(oldPath, newPath);
3028             return 0;
3029         } catch (ErrnoException e) {
3030             final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed.";
3031             Log.e(TAG, errorMessage, e);
3032             return e.errno;
3033         }
3034     }
3035 
3036     /**
3037      * Rename directory from {@code oldPath} to {@code newPath}.
3038      *
3039      * Renaming a directory is only allowed if calling package has write permission to all files in
3040      * the given directory tree and all file types in the given directory tree are supported by the
3041      * top level directory of new path. Renaming a directory is split into three steps:
3042      * 1. Check calling package's permissions for all files in the given directory tree. Also check
3043      *    file type support for all files in the {@code newPath}.
3044      * 2. Try updating database for all files in the directory.
3045      * 3. Rename the directory in lower file system. If rename in the lower file system is
3046      *    successful, commit database update.
3047      *
3048      * @param oldPath path of the directory to be renamed.
3049      * @param newPath new path of directory to be renamed.
3050      * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed.
3051      * <ul>
3052      * <li>{@link OsConstants#EPERM} Renaming a directory with file types not supported by
3053      * {@code newPath} or renaming a directory with files for which calling package doesn't have
3054      * write permission.
3055      * This method can also return errno returned from {@code Os.rename} function.
3056      */
renameDirectoryCheckedForFuse(String oldPath, String newPath)3057     private int renameDirectoryCheckedForFuse(String oldPath, String newPath) {
3058         final ArrayList<String> fileList;
3059         try {
3060             fileList = getWritableFilesForRenameDirectory(oldPath, newPath);
3061         } catch (IllegalArgumentException e) {
3062             final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. ";
3063             Log.e(TAG, errorMessage, e);
3064             return OsConstants.EPERM;
3065         }
3066 
3067         return renameDirectoryUncheckedForFuse(oldPath, newPath, fileList);
3068     }
3069 
renameDirectoryUncheckedForFuse(String oldPath, String newPath, ArrayList<String> fileList)3070     private int renameDirectoryUncheckedForFuse(String oldPath, String newPath,
3071             ArrayList<String> fileList) {
3072         final DatabaseHelper helper;
3073         try {
3074             helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath));
3075         } catch (VolumeNotFoundException e) {
3076             throw new IllegalStateException("Volume not found while trying to update database for "
3077                     + oldPath, e);
3078         }
3079 
3080         helper.beginTransaction();
3081         try {
3082             final Bundle qbExtras = new Bundle();
3083             qbExtras.putStringArrayList(INCLUDED_DEFAULT_DIRECTORIES,
3084                     getIncludedDefaultDirectories());
3085             final boolean wasHidden = FileUtils.shouldDirBeHidden(new File(oldPath));
3086             final boolean isHidden = FileUtils.shouldDirBeHidden(new File(newPath));
3087             for (String filePath : fileList) {
3088                 final String newFilePath = newPath + "/" + filePath;
3089                 final String mimeType = MimeUtils.resolveMimeType(new File(newFilePath));
3090                 if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath,
3091                         getContentValuesForFuseRename(newFilePath, mimeType, wasHidden, isHidden,
3092                                 /* isSameMimeType */ true),
3093                         qbExtras)) {
3094                     Log.e(TAG, "Calling package doesn't have write permission to rename file.");
3095                     return OsConstants.EPERM;
3096                 }
3097             }
3098 
3099             // Rename the directory in lower file system.
3100             int errno = renameInLowerFs(oldPath, newPath);
3101             if (errno == 0) {
3102                 helper.setTransactionSuccessful();
3103             } else {
3104                 return errno;
3105             }
3106         } finally {
3107             helper.endTransaction();
3108         }
3109         // Directory movement might have made new/old path hidden.
3110         scanRenamedDirectoryForFuse(oldPath, newPath);
3111         return 0;
3112     }
3113 
3114     /**
3115      * Rename a file from {@code oldPath} to {@code newPath}.
3116      *
3117      * Renaming a file is split into three parts:
3118      * 1. Check if {@code newPath} supports new file type.
3119      * 2. Try updating database entry from {@code oldPath} to {@code newPath}. This update may fail
3120      *    if calling package doesn't have write permission for {@code oldPath} and {@code newPath}.
3121      * 3. Rename the file in lower file system. If Rename in lower file system succeeds, commit
3122      *    database update.
3123      * @param oldPath path of the file to be renamed.
3124      * @param newPath new path of the file to be renamed.
3125      * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed.
3126      * <ul>
3127      * <li>{@link OsConstants#EPERM} Calling package doesn't have write permission for
3128      * {@code oldPath} or {@code newPath}, or file type is not supported by {@code newPath}.
3129      * This method can also return errno returned from {@code Os.rename} function.
3130      */
renameFileCheckedForFuse(String oldPath, String newPath)3131     private int renameFileCheckedForFuse(String oldPath, String newPath) {
3132         // Check if new mime type is supported in new path.
3133         final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
3134         if (!isMimeTypeSupportedInPath(newPath, newMimeType)) {
3135             return OsConstants.EPERM;
3136         }
3137         return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ false) ;
3138     }
3139 
renameFileUncheckedForFuse(String oldPath, String newPath)3140     private int renameFileUncheckedForFuse(String oldPath, String newPath) {
3141         return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ true) ;
3142     }
3143 
renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions)3144     private int renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions) {
3145         final DatabaseHelper helper;
3146         try {
3147             helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath));
3148         } catch (VolumeNotFoundException e) {
3149             throw new IllegalStateException("Failed to update database row with " + oldPath, e);
3150         }
3151 
3152         final boolean wasHidden = FileUtils.shouldFileBeHidden(new File(oldPath));
3153         final boolean isHidden = FileUtils.shouldFileBeHidden(new File(newPath));
3154         helper.beginTransaction();
3155         try {
3156             final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
3157             final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath));
3158             final boolean isSameMimeType = newMimeType.equalsIgnoreCase(oldMimeType);
3159             ContentValues contentValues = getContentValuesForFuseRename(newPath, newMimeType,
3160                     wasHidden, isHidden, isSameMimeType);
3161             if (!updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues)) {
3162                 if (!bypassRestrictions) {
3163                     // Check for other URI format grants for oldPath only. Check right before
3164                     // returning EPERM, to leave positive case performance unaffected.
3165                     if (!renameWithOtherUriGrants(helper, oldPath, newPath, contentValues)) {
3166                         Log.e(TAG, "Calling package doesn't have write permission to rename file.");
3167                         return OsConstants.EPERM;
3168                     }
3169                 } else if (!maybeRemoveOwnerPackageForFuseRename(helper, newPath)) {
3170                     Log.wtf(TAG, "Couldn't clear owner package name for " + newPath);
3171                     return OsConstants.EPERM;
3172                 }
3173             }
3174 
3175             // Try renaming oldPath to newPath in lower file system.
3176             int errno = renameInLowerFs(oldPath, newPath);
3177             if (errno == 0) {
3178                 helper.setTransactionSuccessful();
3179             } else {
3180                 return errno;
3181             }
3182         } finally {
3183             helper.endTransaction();
3184         }
3185         // The above code should have taken are of the mime/media type of the new file,
3186         // even if it was moved to/from a hidden directory.
3187         // This leaves cases where the source/dest of the move is a .nomedia file itself. Eg:
3188         // 1) /sdcard/foo/.nomedia => /sdcard/foo/bar.mp3
3189         //    in this case, the code above has given bar.mp3 the correct mime type, but we should
3190         //    still can /sdcard/foo, because it's now no longer hidden
3191         // 2) /sdcard/foo/.nomedia => /sdcard/bar/.nomedia
3192         //    in this case, we need to scan both /sdcard/foo and /sdcard/bar/
3193         // 3) /sdcard/foo/bar.mp3 => /sdcard/foo/.nomedia
3194         //    in this case, we need to scan all of /sdcard/foo
3195         if (extractDisplayName(oldPath).equals(".nomedia")) {
3196             scanFileAsMediaProvider(new File(oldPath).getParentFile(), REASON_DEMAND);
3197         }
3198         if (extractDisplayName(newPath).equals(".nomedia")) {
3199             scanFileAsMediaProvider(new File(newPath).getParentFile(), REASON_DEMAND);
3200         }
3201 
3202         return 0;
3203     }
3204 
3205     /**
3206      * Rename file by checking for other URI grants on oldPath
3207      *
3208      * We don't support replace scenario by checking for other URI grants on newPath (if it exists).
3209      */
renameWithOtherUriGrants(DatabaseHelper helper, String oldPath, String newPath, ContentValues contentValues)3210     private boolean renameWithOtherUriGrants(DatabaseHelper helper, String oldPath, String newPath,
3211             ContentValues contentValues) {
3212         final Uri oldPathGrantedUri = getOtherUriGrantsForPath(oldPath, /* forWrite */ true);
3213         if (oldPathGrantedUri == null) {
3214             return false;
3215         }
3216         return updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues, Bundle.EMPTY,
3217                 oldPathGrantedUri);
3218     }
3219 
3220     /**
3221      * Rename file/directory without imposing any restrictions.
3222      *
3223      * We don't impose any rename restrictions for apps that bypass scoped storage restrictions.
3224      * However, we update database entries for renamed files to keep the database consistent.
3225      */
renameUncheckedForFuse(String oldPath, String newPath)3226     private int renameUncheckedForFuse(String oldPath, String newPath) {
3227         if (new File(oldPath).isFile()) {
3228             return renameFileUncheckedForFuse(oldPath, newPath);
3229         } else {
3230             return renameDirectoryUncheckedForFuse(oldPath, newPath,
3231                     getAllFilesForRenameDirectory(oldPath));
3232         }
3233     }
3234 
3235     /**
3236      * Rename file or directory from {@code oldPath} to {@code newPath}.
3237      *
3238      * @param oldPath path of the file or directory to be renamed.
3239      * @param newPath new path of the file or directory to be renamed.
3240      * @param uid UID of the calling package.
3241      * @return 0 on successful rename, appropriate errno value if the rename is not allowed.
3242      * <ul>
3243      * <li>{@link OsConstants#ENOENT} Renaming a non-existing file or renaming a file from path that
3244      * is not indexed by MediaProvider database.
3245      * <li>{@link OsConstants#EPERM} Renaming a default directory or renaming a file to a file type
3246      * not supported by new path.
3247      * This method can also return errno returned from {@code Os.rename} function.
3248      *
3249      * Called from JNI in jni/MediaProviderWrapper.cpp
3250      */
3251     @Keep
renameForFuse(String oldPath, String newPath, int uid)3252     public int renameForFuse(String oldPath, String newPath, int uid) {
3253         final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. ";
3254         final LocalCallingIdentity token =
3255                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
3256         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), oldPath);
3257 
3258         try {
3259             if (isPrivatePackagePathNotAccessibleByCaller(oldPath)
3260                     || isPrivatePackagePathNotAccessibleByCaller(newPath)) {
3261                 return OsConstants.EACCES;
3262             }
3263 
3264             if (!newPath.equals(getAbsoluteSanitizedPath(newPath))) {
3265                 Log.e(TAG, "New path name contains invalid characters.");
3266                 return OsConstants.EPERM;
3267             }
3268 
3269             if (shouldBypassDatabaseAndSetDirtyForFuse(uid, oldPath)
3270                     && shouldBypassDatabaseAndSetDirtyForFuse(uid, newPath)) {
3271                 return renameInLowerFs(oldPath, newPath);
3272             }
3273 
3274             if (shouldBypassFuseRestrictions(/*forWrite*/ true, oldPath)
3275                     && shouldBypassFuseRestrictions(/*forWrite*/ true, newPath)) {
3276                 return renameUncheckedForFuse(oldPath, newPath);
3277             }
3278             // Legacy apps that made is this far don't have the right storage permission and hence
3279             // are not allowed to access anything other than their external app directory
3280             if (isCallingPackageRequestingLegacy()) {
3281                 return OsConstants.EACCES;
3282             }
3283 
3284             final String[] oldRelativePath = sanitizePath(extractRelativePath(oldPath));
3285             final String[] newRelativePath = sanitizePath(extractRelativePath(newPath));
3286             if (oldRelativePath.length == 0 || newRelativePath.length == 0) {
3287                 // Rename not allowed on paths that can't be translated to RELATIVE_PATH.
3288                 Log.e(TAG, errorMessage +  "Invalid path.");
3289                 return OsConstants.EPERM;
3290             }
3291             if (oldRelativePath.length == 1 && TextUtils.isEmpty(oldRelativePath[0])) {
3292                 // Allow rename of files/folders other than default directories.
3293                 final String displayName = extractDisplayName(oldPath);
3294                 for (String defaultFolder : DEFAULT_FOLDER_NAMES) {
3295                     if (displayName.equals(defaultFolder)) {
3296                         Log.e(TAG, errorMessage + oldPath + " is a default folder."
3297                                 + " Renaming a default folder is not allowed.");
3298                         return OsConstants.EPERM;
3299                     }
3300                 }
3301             }
3302             if (newRelativePath.length == 1 && TextUtils.isEmpty(newRelativePath[0])) {
3303                 Log.e(TAG, errorMessage +  newPath + " is in root folder."
3304                         + " Renaming a file/directory to root folder is not allowed");
3305                 return OsConstants.EPERM;
3306             }
3307 
3308             final File directoryAndroid = new File(
3309                     extractVolumePath(oldPath).toLowerCase(Locale.ROOT),
3310                     DIRECTORY_ANDROID_LOWER_CASE
3311             );
3312             final File directoryAndroidMedia = new File(directoryAndroid, DIRECTORY_MEDIA);
3313             String newPathLowerCase = newPath.toLowerCase(Locale.ROOT);
3314             if (directoryAndroidMedia.getAbsolutePath().equalsIgnoreCase(oldPath)) {
3315                 // Don't allow renaming 'Android/media' directory.
3316                 // Android/[data|obb] are bind mounted and these paths don't go through FUSE.
3317                 Log.e(TAG, errorMessage +  oldPath + " is a default folder in app external "
3318                         + "directory. Renaming a default folder is not allowed.");
3319                 return OsConstants.EPERM;
3320             } else if (FileUtils.contains(directoryAndroid, new File(newPathLowerCase))) {
3321                 if (newRelativePath.length <= 2) {
3322                     // Path is directly under Android, Android/media, Android/data, Android/obb or
3323                     // some other directory under Android. Don't allow moving files and directories
3324                     // in these paths. Files and directories are only allowed to move to path
3325                     // Android/media/<app_specific_directory>/*
3326                     Log.e(TAG, errorMessage +  newPath + " is in app external directory. "
3327                             + "Renaming a file/directory to app external directory is not "
3328                             + "allowed.");
3329                     return OsConstants.EPERM;
3330                 } else if (!FileUtils.contains(directoryAndroidMedia, new File(newPathLowerCase))) {
3331                     // New path is not in Android/media/*. Don't allow moving of files or
3332                     // directories to app external directory other than media directory.
3333                     Log.e(TAG, errorMessage +  newPath + " is not in external media directory."
3334                             + "File/directory can only be renamed to a path in external media "
3335                             + "directory. Renaming file/directory to path in other external "
3336                             + "directories is not allowed");
3337                     return OsConstants.EPERM;
3338                 }
3339             }
3340 
3341             // Continue renaming files/directories if rename of oldPath to newPath is allowed.
3342             if (new File(oldPath).isFile()) {
3343                 return renameFileCheckedForFuse(oldPath, newPath);
3344             } else {
3345                 return renameDirectoryCheckedForFuse(oldPath, newPath);
3346             }
3347         } finally {
3348             restoreLocalCallingIdentity(token);
3349         }
3350     }
3351 
3352     @Override
checkUriPermission(@onNull Uri uri, int uid, int modeFlags)3353     public int checkUriPermission(@NonNull Uri uri, int uid,
3354             /* @Intent.AccessUriMode */ int modeFlags) {
3355         final LocalCallingIdentity token = clearLocalCallingIdentity(
3356                 LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid));
3357 
3358         if (isRedactedUri(uri)) {
3359             if ((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
3360                 // we don't allow write grants on redacted uris.
3361                 return PackageManager.PERMISSION_DENIED;
3362             }
3363 
3364             uri = getUriForRedactedUri(uri);
3365         }
3366 
3367         if (isPickerUri(uri)) {
3368             // Do not allow implicit access (by the virtue of ownership/permission) to picker uris.
3369             // Picker uris should have explicit permission grants.
3370             // If the calling app A has an explicit grant on picker uri, UriGrantsManagerService
3371             // will check the grant status and allow app A to grant the uri to app B (without
3372             // calling into MediaProvider)
3373             return PackageManager.PERMISSION_DENIED;
3374         }
3375 
3376         try {
3377             final boolean allowHidden = isCallingPackageAllowedHidden();
3378             final int table = matchUri(uri, allowHidden);
3379 
3380             final DatabaseHelper helper;
3381             try {
3382                 helper = getDatabaseForUri(uri);
3383             } catch (VolumeNotFoundException e) {
3384                 return PackageManager.PERMISSION_DENIED;
3385             }
3386 
3387             final int type;
3388             if ((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
3389                 type = TYPE_UPDATE;
3390             } else {
3391                 type = TYPE_QUERY;
3392             }
3393 
3394             final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, null);
3395             try (Cursor c = qb.query(helper,
3396                     new String[] { BaseColumns._ID }, null, null, null, null, null, null, null)) {
3397                 if (c.getCount() == 1) {
3398                     c.moveToFirst();
3399                     final long cursorId = c.getLong(0);
3400 
3401                     long uriId = -1;
3402                     try {
3403                         uriId = ContentUris.parseId(uri);
3404                     } catch (NumberFormatException ignored) {
3405                         // if the id is not a number, the uri doesn't have a valid ID at the end of
3406                         // the uri, (i.e., uri is uri of the table not of the item/row)
3407                     }
3408 
3409                     if (uriId != -1 && cursorId == uriId) {
3410                         return PackageManager.PERMISSION_GRANTED;
3411                     }
3412                 }
3413             }
3414 
3415             // For the uri with id cases, if it isn't returned in above query section, the result
3416             // isn't as expected. Don't grant the permission.
3417             switch (table) {
3418                 case AUDIO_MEDIA_ID:
3419                 case IMAGES_MEDIA_ID:
3420                 case VIDEO_MEDIA_ID:
3421                 case DOWNLOADS_ID:
3422                 case FILES_ID:
3423                 case AUDIO_MEDIA_ID_GENRES_ID:
3424                 case AUDIO_GENRES_ID:
3425                 case AUDIO_PLAYLISTS_ID:
3426                 case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
3427                 case AUDIO_ARTISTS_ID:
3428                 case AUDIO_ALBUMS_ID:
3429                     return PackageManager.PERMISSION_DENIED;
3430                 default:
3431                     // continue below
3432             }
3433 
3434             // If the uri is a valid content uri and doesn't have a valid ID at the end of the uri,
3435             // (i.e., uri is uri of the table not of the item/row), and app doesn't request prefix
3436             // grant, we are willing to grant this uri permission since this doesn't grant them any
3437             // extra access. This grant will only grant permissions on given uri, it will not grant
3438             // access to db rows of the corresponding table.
3439             if ((modeFlags & Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) == 0) {
3440                 return PackageManager.PERMISSION_GRANTED;
3441             }
3442         } finally {
3443             restoreLocalCallingIdentity(token);
3444         }
3445         return PackageManager.PERMISSION_DENIED;
3446     }
3447 
3448     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)3449     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
3450             String sortOrder) {
3451         return query(uri, projection,
3452                 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, sortOrder), null);
3453     }
3454 
3455     @Override
query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)3456     public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal) {
3457         return query(uri, projection, queryArgs, signal, /* forSelf */ false);
3458     }
3459 
query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal, boolean forSelf)3460     private Cursor query(Uri uri, String[] projection, Bundle queryArgs,
3461             CancellationSignal signal, boolean forSelf) {
3462         Trace.beginSection("MP.query [" + uri + ']');
3463         try {
3464             return queryInternal(uri, projection, queryArgs, signal, forSelf);
3465         } catch (FallbackException e) {
3466             return e.translateForQuery(getCallingPackageTargetSdkVersion());
3467         } finally {
3468             Trace.endSection();
3469         }
3470     }
3471 
queryInternal(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal, boolean forSelf)3472     private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs,
3473             CancellationSignal signal, boolean forSelf) throws FallbackException {
3474         if (isPickerUri(uri)) {
3475             return mPickerUriResolver.query(uri, projection, mCallingIdentity.get().pid,
3476                     mCallingIdentity.get().uid, mCallingIdentity.get().getPackageName());
3477         }
3478 
3479         final String volumeName = getVolumeName(uri);
3480         PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
3481         queryArgs = (queryArgs != null) ? queryArgs : new Bundle();
3482 
3483         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
3484         queryArgs.remove(INCLUDED_DEFAULT_DIRECTORIES);
3485 
3486         final ArraySet<String> honoredArgs = new ArraySet<>();
3487         DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator);
3488 
3489         Uri redactedUri = null;
3490         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
3491         queryArgs.remove(QUERY_ARG_REDACTED_URI);
3492         if (isRedactedUri(uri)) {
3493             redactedUri = uri;
3494             uri = getUriForRedactedUri(uri);
3495             queryArgs.putParcelable(QUERY_ARG_REDACTED_URI, redactedUri);
3496         }
3497 
3498         uri = safeUncanonicalize(uri);
3499 
3500         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
3501         final boolean allowHidden = isCallingPackageAllowedHidden();
3502         final int table = matchUri(uri, allowHidden);
3503 
3504         // handle MEDIA_SCANNER before calling getDatabaseForUri()
3505         if (table == MEDIA_SCANNER) {
3506             // create a cursor to return volume currently being scanned by the media scanner
3507             MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME});
3508             c.addRow(new String[] {mMediaScannerVolume});
3509             return c;
3510         }
3511 
3512         // Used temporarily (until we have unique media IDs) to get an identifier
3513         // for the current sd card, so that the music app doesn't have to use the
3514         // non-public getFatVolumeId method
3515         if (table == FS_ID) {
3516             MatrixCursor c = new MatrixCursor(new String[] {"fsid"});
3517             // current FAT volume ID
3518             int volumeId = -1;
3519             c.addRow(new Integer[] {volumeId});
3520             return c;
3521         }
3522 
3523         if (table == VERSION) {
3524             MatrixCursor c = new MatrixCursor(new String[] {"version"});
3525             c.addRow(new Integer[] {DatabaseHelper.getDatabaseVersion(getContext())});
3526             return c;
3527         }
3528 
3529         // TODO(b/195008831): Add test to verify that apps can't access
3530         if (table == PICKER_INTERNAL_MEDIA_ALL) {
3531             return mPickerDataLayer.fetchAllMedia(queryArgs);
3532         } else if (table == PICKER_INTERNAL_MEDIA_LOCAL) {
3533             return mPickerDataLayer.fetchLocalMedia(queryArgs);
3534         } else if (table == PICKER_INTERNAL_ALBUMS_ALL) {
3535             return mPickerDataLayer.fetchAllAlbums(queryArgs);
3536         } else if (table == PICKER_INTERNAL_ALBUMS_LOCAL) {
3537             return mPickerDataLayer.fetchLocalAlbums(queryArgs);
3538         }
3539 
3540         final DatabaseHelper helper = getDatabaseForUri(uri);
3541         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, queryArgs,
3542                 honoredArgs::add);
3543         // Allowing hidden column _user_id for this query to support Cloned Profile use case.
3544         if (table == FILES) {
3545             qb.allowColumn(FileColumns._USER_ID);
3546         }
3547 
3548         if (targetSdkVersion < Build.VERSION_CODES.R) {
3549             // Some apps are abusing "ORDER BY" clauses to inject "LIMIT"
3550             // clauses; gracefully lift them out.
3551             DatabaseUtils.recoverAbusiveSortOrder(queryArgs);
3552 
3553             // Some apps are abusing the Uri query parameters to inject LIMIT
3554             // clauses; gracefully lift them out.
3555             DatabaseUtils.recoverAbusiveLimit(uri, queryArgs);
3556         }
3557 
3558         if (targetSdkVersion < Build.VERSION_CODES.Q) {
3559             // Some apps are abusing the "WHERE" clause by injecting "GROUP BY"
3560             // clauses; gracefully lift them out.
3561             DatabaseUtils.recoverAbusiveSelection(queryArgs);
3562 
3563             // Some apps are abusing the first column to inject "DISTINCT";
3564             // gracefully lift them out.
3565             if ((projection != null) && (projection.length > 0)
3566                     && projection[0].startsWith("DISTINCT ")) {
3567                 projection[0] = projection[0].substring("DISTINCT ".length());
3568                 qb.setDistinct(true);
3569             }
3570 
3571             // Some apps are generating thumbnails with getThumbnail(), but then
3572             // ignoring the returned Bitmap and querying the raw table; give
3573             // them a row with enough information to find the original image.
3574             final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION);
3575             if ((table == IMAGES_THUMBNAILS || table == VIDEO_THUMBNAILS)
3576                     && !TextUtils.isEmpty(selection)) {
3577                 final Matcher matcher = PATTERN_SELECTION_ID.matcher(selection);
3578                 if (matcher.matches()) {
3579                     final long id = Long.parseLong(matcher.group(1));
3580 
3581                     final Uri fullUri;
3582                     if (table == IMAGES_THUMBNAILS) {
3583                         fullUri = ContentUris.withAppendedId(
3584                                 Images.Media.getContentUri(volumeName), id);
3585                     } else if (table == VIDEO_THUMBNAILS) {
3586                         fullUri = ContentUris.withAppendedId(
3587                                 Video.Media.getContentUri(volumeName), id);
3588                     } else {
3589                         throw new IllegalArgumentException();
3590                     }
3591 
3592                     final MatrixCursor cursor = new MatrixCursor(projection);
3593                     final File file = ContentResolver.encodeToFile(
3594                             fullUri.buildUpon().appendPath("thumbnail").build());
3595                     final String data = file.getAbsolutePath();
3596                     cursor.newRow().add(MediaColumns._ID, null)
3597                             .add(Images.Thumbnails.IMAGE_ID, id)
3598                             .add(Video.Thumbnails.VIDEO_ID, id)
3599                             .add(MediaColumns.DATA, data);
3600                     return cursor;
3601                 }
3602             }
3603         }
3604 
3605         // Update locale if necessary.
3606         if (helper.isInternal() && !Locale.getDefault().equals(mLastLocale)) {
3607             Log.i(TAG, "Updating locale within queryInternal");
3608             onLocaleChanged(false);
3609         }
3610 
3611         Cursor c = qb.query(helper, projection, queryArgs, signal);
3612 
3613         // Starting U, we are filtering owner package names for apps without visibility
3614         // on other packages. Apps with QUERY_ALL_PACKAGES permission are not affected.
3615         if (shouldFilterOwnerPackageNameFlag() && isApplicableForOwnerPackageNameFiltering(c)) {
3616             final long startTime = SystemClock.elapsedRealtime();
3617             final String[] resultOwnerPackageNames = getOwnerPackageNames(c);
3618             if (resultOwnerPackageNames.length != 0) {
3619                 final Set<String> queryablePackages = getQueryablePackages(resultOwnerPackageNames);
3620                 if (resultOwnerPackageNames.length != queryablePackages.size()) {
3621                     c = filterOwnerPackageNames(c, queryablePackages);
3622                 }
3623             }
3624             final long durationMillis = SystemClock.elapsedRealtime() - startTime;
3625             Log.d(TAG, "Filtering owner package names took " + durationMillis + " ms");
3626         }
3627 
3628         if (c != null && !forSelf) {
3629             // As a performance optimization, only configure notifications when
3630             // resulting cursor will leave our process
3631             final boolean callerIsRemote = mCallingIdentity.get().pid != android.os.Process.myPid();
3632             if (callerIsRemote && !isFuseThread()) {
3633                 c.setNotificationUri(getContext().getContentResolver(), uri);
3634             }
3635 
3636             final Bundle extras = new Bundle();
3637             extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS,
3638                     honoredArgs.toArray(new String[honoredArgs.size()]));
3639             c.setExtras(extras);
3640         }
3641 
3642         // Query was on a redacted URI, update the sensitive information such as the _ID, DATA etc.
3643         if (redactedUri != null && c != null) {
3644             try {
3645                 return getRedactedUriCursor(redactedUri, c);
3646             } finally {
3647                 c.close();
3648             }
3649         }
3650 
3651         return c;
3652     }
3653 
3654     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
getQueryablePackages(String[] packageNames)3655     private Set<String> getQueryablePackages(String[] packageNames) {
3656         final boolean[] canPackageBeQueried;
3657         try {
3658             canPackageBeQueried = mPackageManager.canPackageQuery(
3659                     mCallingIdentity.get().getPackageName(), packageNames);
3660         } catch (NameNotFoundException e) {
3661             Log.e(TAG, "Invalid package name", e);
3662             // If package manager throws an error, only assume calling package as queryable package
3663             return new HashSet<>(Arrays.asList(mCallingIdentity.get().getPackageName()));
3664         }
3665 
3666         final Set<String> queryablePackages = new HashSet<>();
3667         for (int i = 0; i < packageNames.length; i++) {
3668             if (canPackageBeQueried[i]) {
3669                 queryablePackages.add(packageNames[i]);
3670             }
3671         }
3672         return queryablePackages;
3673     }
3674 
getOwnerPackageNames(Cursor c)3675     private String[] getOwnerPackageNames(Cursor c) {
3676         final Set<String> ownerPackageNames = new HashSet<>();
3677         final int ownerPackageNameColIndex = c.getColumnIndex(MediaColumns.OWNER_PACKAGE_NAME);
3678 
3679         while (c.moveToNext()) {
3680             final String ownerPackageName = c.getString(ownerPackageNameColIndex);
3681             if (!Strings.isNullOrEmpty(ownerPackageName)) {
3682                 ownerPackageNames.add(ownerPackageName);
3683             }
3684         }
3685         c.moveToPosition(-1);
3686 
3687         return ownerPackageNames.toArray(new String[0]);
3688     }
3689 
filterOwnerPackageNames(@onNull Cursor c, @NonNull Set<String> queryablePackages)3690     private Cursor filterOwnerPackageNames(@NonNull Cursor c,
3691             @NonNull Set<String> queryablePackages) {
3692         final MatrixCursor filteredCursor = new MatrixCursor(c.getColumnNames(), c.getCount());
3693 
3694         while (c.moveToNext()) {
3695             final MatrixCursor.RowBuilder row = filteredCursor.newRow();
3696             for (int colIndex = 0; colIndex < c.getColumnCount(); colIndex++) {
3697                 if (OWNER_PACKAGE_NAME.equals(c.getColumnName(colIndex))
3698                         && !queryablePackages.contains(c.getString(colIndex))) {
3699                     row.add(null);
3700                 } else if (c.getType(colIndex) == FIELD_TYPE_BLOB) {
3701                     row.add(c.getBlob(colIndex));
3702                 } else {
3703                     row.add(c.getString(colIndex));
3704                 }
3705             }
3706         }
3707         return filteredCursor;
3708     }
3709 
isApplicableForOwnerPackageNameFiltering(Cursor c)3710     private boolean isApplicableForOwnerPackageNameFiltering(Cursor c) {
3711         return SdkLevel.isAtLeastU() && c != null
3712                 && Arrays.asList(c.getColumnNames()).contains(MediaColumns.OWNER_PACKAGE_NAME)
3713                 && getContext().checkPermission(QUERY_ALL_PACKAGES,
3714                 mCallingIdentity.get().pid, mCallingIdentity.get().uid) == PERMISSION_DENIED;
3715     }
3716 
shouldFilterOwnerPackageNameFlag()3717     private boolean shouldFilterOwnerPackageNameFlag() {
3718         return true;
3719     }
3720 
isUriSupportedForRedaction(Uri uri)3721     private boolean isUriSupportedForRedaction(Uri uri) {
3722         final int match = matchUri(uri, true);
3723         return REDACTED_URI_SUPPORTED_TYPES.contains(match);
3724     }
3725 
getRedactedUriCursor(Uri redactedUri, @NonNull Cursor c)3726     private Cursor getRedactedUriCursor(Uri redactedUri, @NonNull Cursor c) {
3727         final HashSet<String> columnNames = new HashSet<>(Arrays.asList(c.getColumnNames()));
3728         final MatrixCursor redactedUriCursor = new MatrixCursor(c.getColumnNames());
3729         final String redactedUriId = redactedUri.getLastPathSegment();
3730 
3731         if (!c.moveToFirst()) {
3732             return redactedUriCursor;
3733         }
3734 
3735         // NOTE: It is safe to assume that there will only be one entry corresponding to a
3736         // redacted URI as it corresponds to a unique DB entry.
3737         if (c.getCount() != 1) {
3738             throw new AssertionError("Two rows corresponding to " + redactedUri.toString()
3739                     + " found, when only one expected");
3740         }
3741 
3742         final MatrixCursor.RowBuilder row = redactedUriCursor.newRow();
3743         for (String columnName : c.getColumnNames()) {
3744             final int colIndex = c.getColumnIndex(columnName);
3745             if (c.getType(colIndex) == FIELD_TYPE_BLOB) {
3746                 row.add(c.getBlob(colIndex));
3747             } else {
3748                 row.add(c.getString(colIndex));
3749             }
3750         }
3751 
3752         String ext = getFileExtensionFromCursor(c, columnNames);
3753         ext = ext == null ? "" : "." + ext;
3754         final String displayName = redactedUriId + ext;
3755         final String data = buildPrimaryVolumeFile(uidToUserId(Binder.getCallingUid()),
3756                 getRedactedRelativePath(), displayName).getAbsolutePath();
3757 
3758         updateRow(columnNames, MediaColumns._ID, row, redactedUriId);
3759         updateRow(columnNames, MediaColumns.DISPLAY_NAME, row, displayName);
3760         updateRow(columnNames, MediaColumns.RELATIVE_PATH, row, getRedactedRelativePath());
3761         updateRow(columnNames, MediaColumns.BUCKET_DISPLAY_NAME, row, getRedactedRelativePath());
3762         updateRow(columnNames, MediaColumns.DATA, row, data);
3763         updateRow(columnNames, MediaColumns.DOCUMENT_ID, row, null);
3764         updateRow(columnNames, MediaColumns.INSTANCE_ID, row, null);
3765         updateRow(columnNames, MediaColumns.BUCKET_ID, row, null);
3766 
3767         return redactedUriCursor;
3768     }
3769 
3770     @Nullable
getFileExtensionFromCursor(@onNull Cursor c, @NonNull HashSet<String> columnNames)3771     private static String getFileExtensionFromCursor(@NonNull Cursor c,
3772             @NonNull HashSet<String> columnNames) {
3773         if (columnNames.contains(MediaColumns.DATA)) {
3774             return extractFileExtension(c.getString(c.getColumnIndex(MediaColumns.DATA)));
3775         }
3776         if (columnNames.contains(MediaColumns.DISPLAY_NAME)) {
3777             return extractFileExtension(c.getString(c.getColumnIndex(MediaColumns.DISPLAY_NAME)));
3778         }
3779         return null;
3780     }
3781 
updateRow(HashSet<String> columnNames, String columnName, MatrixCursor.RowBuilder row, Object val)3782     private void updateRow(HashSet<String> columnNames, String columnName,
3783             MatrixCursor.RowBuilder row, Object val) {
3784         if (columnNames.contains(columnName)) {
3785             row.add(columnName, val);
3786         }
3787     }
3788 
getUriForRedactedUri(Uri redactedUri)3789     private Uri getUriForRedactedUri(Uri redactedUri) {
3790         final Uri.Builder builder = redactedUri.buildUpon();
3791         builder.path(null);
3792         final List<String> segments = redactedUri.getPathSegments();
3793         for (int i = 0; i < segments.size() - 1; i++) {
3794             builder.appendPath(segments.get(i));
3795         }
3796 
3797         DatabaseHelper helper;
3798         try {
3799             helper = getDatabaseForUri(redactedUri);
3800         } catch (VolumeNotFoundException e) {
3801             throw e.rethrowAsIllegalArgumentException();
3802         }
3803 
3804         try (final Cursor c = helper.runWithoutTransaction(
3805                 (db) -> db.query("files", new String[]{MediaColumns._ID},
3806                         FileColumns.REDACTED_URI_ID + "=?",
3807                         new String[]{redactedUri.getLastPathSegment()}, null, null, null))) {
3808             if (!c.moveToFirst()) {
3809                 throw new IllegalArgumentException(
3810                         "Uri: " + redactedUri.toString() + " not found.");
3811             }
3812 
3813             builder.appendPath(c.getString(0));
3814             return builder.build();
3815         }
3816     }
3817 
isRedactedUri(Uri uri)3818     private boolean isRedactedUri(Uri uri) {
3819         String id = uri.getLastPathSegment();
3820         return id != null && id.startsWith(REDACTED_URI_ID_PREFIX)
3821                 && id.length() == REDACTED_URI_ID_SIZE;
3822     }
3823 
3824     @Override
getType(Uri url)3825     public String getType(Uri url) {
3826         final int match = matchUri(url, true);
3827         switch (match) {
3828             case IMAGES_MEDIA_ID:
3829             case AUDIO_MEDIA_ID:
3830             case AUDIO_PLAYLISTS_ID:
3831             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
3832             case VIDEO_MEDIA_ID:
3833             case DOWNLOADS_ID:
3834             case FILES_ID:
3835                 if (SdkLevel.isAtLeastU()) {
3836                     // Starting Android 14, there is permission check for
3837                     // getting types requiring internal query.
3838                     return queryForTypeAsCaller(url);
3839                 } else {
3840                     return queryForTypeAsSelf(url);
3841                 }
3842 
3843             case IMAGES_MEDIA:
3844             case IMAGES_THUMBNAILS:
3845                 return Images.Media.CONTENT_TYPE;
3846 
3847             case AUDIO_ALBUMART_ID:
3848             case AUDIO_ALBUMART_FILE_ID:
3849             case IMAGES_THUMBNAILS_ID:
3850             case VIDEO_THUMBNAILS_ID:
3851                 return "image/jpeg";
3852 
3853             case AUDIO_MEDIA:
3854             case AUDIO_GENRES_ID_MEMBERS:
3855             case AUDIO_PLAYLISTS_ID_MEMBERS:
3856                 return Audio.Media.CONTENT_TYPE;
3857 
3858             case AUDIO_GENRES:
3859             case AUDIO_MEDIA_ID_GENRES:
3860                 return Audio.Genres.CONTENT_TYPE;
3861             case AUDIO_GENRES_ID:
3862             case AUDIO_MEDIA_ID_GENRES_ID:
3863                 return Audio.Genres.ENTRY_CONTENT_TYPE;
3864             case AUDIO_PLAYLISTS:
3865                 return Audio.Playlists.CONTENT_TYPE;
3866 
3867             case VIDEO_MEDIA:
3868                 return Video.Media.CONTENT_TYPE;
3869             case DOWNLOADS:
3870                 return Downloads.CONTENT_TYPE;
3871 
3872             case PICKER_ID:
3873                 return mPickerUriResolver.getType(url, Binder.getCallingPid(),
3874                         Binder.getCallingUid());
3875         }
3876         throw new IllegalStateException("Unknown URL : " + url);
3877     }
3878 
queryForTypeAsSelf(Uri url)3879     private String queryForTypeAsSelf(Uri url) {
3880         final LocalCallingIdentity token = clearLocalCallingIdentity();
3881         try {
3882             return queryForTypeAsCaller(url);
3883         } finally {
3884             restoreLocalCallingIdentity(token);
3885         }
3886     }
3887 
queryForTypeAsCaller(Uri url)3888     private String queryForTypeAsCaller(Uri url) {
3889         try (Cursor cursor = queryForSingleItem(url,
3890                 new String[] { MediaColumns.MIME_TYPE }, null, null, null)) {
3891             return cursor.getString(0);
3892         } catch (FileNotFoundException e) {
3893             throw new IllegalArgumentException(e.getMessage());
3894         }
3895     }
3896 
3897     @VisibleForTesting
ensureFileColumns(@onNull Uri uri, @NonNull ContentValues values)3898     void ensureFileColumns(@NonNull Uri uri, @NonNull ContentValues values)
3899             throws VolumeArgumentException, VolumeNotFoundException {
3900         final LocalUriMatcher matcher = new LocalUriMatcher(MediaStore.AUTHORITY);
3901         final int match = matcher.matchUri(uri, true);
3902         ensureNonUniqueFileColumns(match, uri, Bundle.EMPTY, values, null /* currentPath */);
3903     }
3904 
ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)3905     private void ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
3906             @NonNull ContentValues values, @Nullable String currentPath)
3907             throws VolumeArgumentException, VolumeNotFoundException {
3908         ensureFileColumns(match, uri, extras, values, true, currentPath);
3909     }
3910 
ensureNonUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)3911     private void ensureNonUniqueFileColumns(int match, @NonNull Uri uri,
3912             @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)
3913             throws VolumeArgumentException, VolumeNotFoundException {
3914         ensureFileColumns(match, uri, extras, values, false, currentPath);
3915     }
3916 
3917     /**
3918      * Get the various file-related {@link MediaColumns} in the given
3919      * {@link ContentValues} into a consistent condition. Also validates that defined
3920      * columns are valid for the given {@link Uri}, such as ensuring that only
3921      * {@code image/*} can be inserted into
3922      * {@link android.provider.MediaStore.Images}.
3923      */
ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)3924     private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
3925             @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)
3926             throws VolumeArgumentException, VolumeNotFoundException {
3927         Trace.beginSection("MP.ensureFileColumns");
3928 
3929         Objects.requireNonNull(uri);
3930         Objects.requireNonNull(extras);
3931         Objects.requireNonNull(values);
3932 
3933         // Figure out defaults based on Uri being modified
3934         String defaultMimeType = ClipDescription.MIMETYPE_UNKNOWN;
3935         int defaultMediaType = FileColumns.MEDIA_TYPE_NONE;
3936         String defaultPrimary = Environment.DIRECTORY_DOWNLOADS;
3937         String defaultSecondary = null;
3938         List<String> allowedPrimary = Arrays.asList(
3939                 Environment.DIRECTORY_DOWNLOADS,
3940                 Environment.DIRECTORY_DOCUMENTS);
3941         switch (match) {
3942             case AUDIO_MEDIA:
3943             case AUDIO_MEDIA_ID:
3944                 defaultMimeType = "audio/mpeg";
3945                 defaultMediaType = FileColumns.MEDIA_TYPE_AUDIO;
3946                 defaultPrimary = Environment.DIRECTORY_MUSIC;
3947                 if (SdkLevel.isAtLeastS()) {
3948                     allowedPrimary = Arrays.asList(
3949                             Environment.DIRECTORY_ALARMS,
3950                             Environment.DIRECTORY_AUDIOBOOKS,
3951                             Environment.DIRECTORY_MUSIC,
3952                             Environment.DIRECTORY_NOTIFICATIONS,
3953                             Environment.DIRECTORY_PODCASTS,
3954                             Environment.DIRECTORY_RECORDINGS,
3955                             Environment.DIRECTORY_RINGTONES);
3956                 } else {
3957                     allowedPrimary = Arrays.asList(
3958                             Environment.DIRECTORY_ALARMS,
3959                             Environment.DIRECTORY_AUDIOBOOKS,
3960                             Environment.DIRECTORY_MUSIC,
3961                             Environment.DIRECTORY_NOTIFICATIONS,
3962                             Environment.DIRECTORY_PODCASTS,
3963                             FileUtils.DIRECTORY_RECORDINGS,
3964                             Environment.DIRECTORY_RINGTONES);
3965                 }
3966                 break;
3967             case VIDEO_MEDIA:
3968             case VIDEO_MEDIA_ID:
3969                 defaultMimeType = "video/mp4";
3970                 defaultMediaType = FileColumns.MEDIA_TYPE_VIDEO;
3971                 defaultPrimary = Environment.DIRECTORY_MOVIES;
3972                 allowedPrimary = Arrays.asList(
3973                         Environment.DIRECTORY_DCIM,
3974                         Environment.DIRECTORY_MOVIES,
3975                         Environment.DIRECTORY_PICTURES);
3976                 break;
3977             case IMAGES_MEDIA:
3978             case IMAGES_MEDIA_ID:
3979                 defaultMimeType = "image/jpeg";
3980                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
3981                 defaultPrimary = Environment.DIRECTORY_PICTURES;
3982                 allowedPrimary = Arrays.asList(
3983                         Environment.DIRECTORY_DCIM,
3984                         Environment.DIRECTORY_PICTURES);
3985                 break;
3986             case AUDIO_ALBUMART:
3987             case AUDIO_ALBUMART_ID:
3988                 defaultMimeType = "image/jpeg";
3989                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
3990                 defaultPrimary = Environment.DIRECTORY_MUSIC;
3991                 allowedPrimary = Collections.singletonList(defaultPrimary);
3992                 defaultSecondary = DIRECTORY_THUMBNAILS;
3993                 break;
3994             case VIDEO_THUMBNAILS:
3995             case VIDEO_THUMBNAILS_ID:
3996                 defaultMimeType = "image/jpeg";
3997                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
3998                 defaultPrimary = Environment.DIRECTORY_MOVIES;
3999                 allowedPrimary = Collections.singletonList(defaultPrimary);
4000                 defaultSecondary = DIRECTORY_THUMBNAILS;
4001                 break;
4002             case IMAGES_THUMBNAILS:
4003             case IMAGES_THUMBNAILS_ID:
4004                 defaultMimeType = "image/jpeg";
4005                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
4006                 defaultPrimary = Environment.DIRECTORY_PICTURES;
4007                 allowedPrimary = Collections.singletonList(defaultPrimary);
4008                 defaultSecondary = DIRECTORY_THUMBNAILS;
4009                 break;
4010             case AUDIO_PLAYLISTS:
4011             case AUDIO_PLAYLISTS_ID:
4012                 defaultMimeType = "audio/mpegurl";
4013                 defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
4014                 defaultPrimary = Environment.DIRECTORY_MUSIC;
4015                 allowedPrimary = Arrays.asList(
4016                         Environment.DIRECTORY_MUSIC,
4017                         Environment.DIRECTORY_MOVIES);
4018                 break;
4019             case DOWNLOADS:
4020             case DOWNLOADS_ID:
4021                 defaultPrimary = Environment.DIRECTORY_DOWNLOADS;
4022                 allowedPrimary = Collections.singletonList(defaultPrimary);
4023                 break;
4024             case FILES:
4025             case FILES_ID:
4026                 // Use defaults above
4027                 break;
4028             default:
4029                 Log.w(TAG, "Unhandled location " + uri + "; assuming generic files");
4030                 break;
4031         }
4032 
4033         final String resolvedVolumeName = resolveVolumeName(uri);
4034 
4035         if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))
4036                 && MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)) {
4037             // TODO: promote this to top-level check
4038             throw new UnsupportedOperationException(
4039                     "Writing to internal storage is not supported.");
4040         }
4041 
4042         // Force values when raw path provided
4043         if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
4044             FileUtils.computeValuesFromData(values, isFuseThread());
4045         }
4046 
4047         final boolean isTargetSdkROrHigher =
4048                 getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R;
4049         final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
4050         final String mimeTypeFromExt = TextUtils.isEmpty(displayName) ? null :
4051                 MimeUtils.resolveMimeType(new File(displayName));
4052 
4053         if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) {
4054             if (isTargetSdkROrHigher) {
4055                 // Extract the MIME type from the display name if we couldn't resolve it from the
4056                 // raw path
4057                 if (mimeTypeFromExt != null) {
4058                     values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
4059                 } else {
4060                     // We couldn't resolve mimeType, it means that both display name and MIME type
4061                     // were missing in values, so we use defaultMimeType.
4062                     values.put(MediaColumns.MIME_TYPE, defaultMimeType);
4063                 }
4064             } else if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
4065                 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
4066             } else {
4067                 // We don't use mimeTypeFromExt to preserve legacy behavior.
4068                 values.put(MediaColumns.MIME_TYPE, defaultMimeType);
4069             }
4070         }
4071 
4072         String mimeType = values.getAsString(MediaColumns.MIME_TYPE);
4073         if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
4074             // We allow any mimeType for generic uri with default media type as MEDIA_TYPE_NONE.
4075         } else if (mimeType != null &&
4076                 MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) == null) {
4077             if (mimeTypeFromExt != null &&
4078                     defaultMediaType == MimeUtils.resolveMediaType(mimeTypeFromExt)) {
4079                 // If mimeType from extension matches the defaultMediaType of uri, we use mimeType
4080                 // from file extension as mimeType. This is an effort to guess the mimeType when we
4081                 // get unsupported mimeType.
4082                 // Note: We can't force defaultMimeType because when we force defaultMimeType, we
4083                 // will force the file extension as well. For example, if DISPLAY_NAME=Foo.png and
4084                 // mimeType="image/*". If we force mimeType to be "image/jpeg", we append the file
4085                 // name with the new file extension i.e., "Foo.png.jpg" where as the expected file
4086                 // name was "Foo.png"
4087                 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
4088             } else if (isTargetSdkROrHigher) {
4089                 // We are here because given mimeType is unsupported also we couldn't guess valid
4090                 // mimeType from file extension.
4091                 throw new IllegalArgumentException("Unsupported MIME type " + mimeType);
4092             } else {
4093                 // We can't throw error for legacy apps, so we try to use defaultMimeType.
4094                 values.put(MediaColumns.MIME_TYPE, defaultMimeType);
4095             }
4096         }
4097 
4098         // Give ourselves reasonable defaults when missing
4099         if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) {
4100             values.put(MediaColumns.DISPLAY_NAME,
4101                     String.valueOf(System.currentTimeMillis()));
4102         }
4103         final Integer formatObject = values.getAsInteger(FileColumns.FORMAT);
4104         final int format = formatObject == null ? 0 : formatObject;
4105         if (format == MtpConstants.FORMAT_ASSOCIATION) {
4106             values.putNull(MediaColumns.MIME_TYPE);
4107         }
4108 
4109         mimeType = values.getAsString(MediaColumns.MIME_TYPE);
4110         // Quick check MIME type against table
4111         if (mimeType != null) {
4112             PulledMetrics.logMimeTypeAccess(getCallingUidOrSelf(), mimeType);
4113             final int actualMediaType = MimeUtils.resolveMediaType(mimeType);
4114             if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
4115                 // Give callers an opportunity to work with playlists and
4116                 // subtitles using the generic files table
4117                 switch (actualMediaType) {
4118                     case FileColumns.MEDIA_TYPE_PLAYLIST:
4119                         defaultMimeType = "audio/mpegurl";
4120                         defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
4121                         defaultPrimary = Environment.DIRECTORY_MUSIC;
4122                         allowedPrimary = new ArrayList<>(allowedPrimary);
4123                         allowedPrimary.add(Environment.DIRECTORY_MUSIC);
4124                         allowedPrimary.add(Environment.DIRECTORY_MOVIES);
4125                         break;
4126                     case FileColumns.MEDIA_TYPE_SUBTITLE:
4127                         defaultMimeType = "application/x-subrip";
4128                         defaultMediaType = FileColumns.MEDIA_TYPE_SUBTITLE;
4129                         defaultPrimary = Environment.DIRECTORY_MOVIES;
4130                         allowedPrimary = new ArrayList<>(allowedPrimary);
4131                         allowedPrimary.add(Environment.DIRECTORY_MUSIC);
4132                         allowedPrimary.add(Environment.DIRECTORY_MOVIES);
4133                         break;
4134                 }
4135             } else if (defaultMediaType != actualMediaType) {
4136                 final String[] split = defaultMimeType.split("/");
4137                 throw new IllegalArgumentException(
4138                         "MIME type " + mimeType + " cannot be inserted into " + uri
4139                                 + "; expected MIME type under " + split[0] + "/*");
4140             }
4141         }
4142 
4143         // Use default directories when missing
4144         if (TextUtils.isEmpty(values.getAsString(MediaColumns.RELATIVE_PATH))) {
4145             if (defaultSecondary != null) {
4146                 values.put(MediaColumns.RELATIVE_PATH,
4147                         defaultPrimary + '/' + defaultSecondary + '/');
4148             } else {
4149                 values.put(MediaColumns.RELATIVE_PATH,
4150                         defaultPrimary + '/');
4151             }
4152         }
4153 
4154         // Generate path when undefined
4155         if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
4156             // Note that just the volume name isn't enough to determine the path,
4157             // since we can manage different volumes with the same name for
4158             // different users. Instead, if we have a current path (which implies
4159             // an already existing file to be renamed), use that to derive the
4160             // user-id of the file, and in turn use that to derive the correct
4161             // volume. Cross-user renames are not supported without a specified
4162             // DATA column.
4163             File volumePath;
4164             UserHandle userHandle = mCallingIdentity.get().getUser();
4165             if (currentPath != null) {
4166                 int userId = FileUtils.extractUserId(currentPath);
4167                 if (userId != -1) {
4168                     userHandle = UserHandle.of(userId);
4169                 }
4170             }
4171             try {
4172                 volumePath = mVolumeCache.getVolumePath(resolvedVolumeName, userHandle);
4173             } catch (FileNotFoundException e) {
4174                 throw new IllegalArgumentException(e);
4175             }
4176 
4177             FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ !isFuseThread());
4178             FileUtils.computeDataFromValues(values, volumePath, isFuseThread());
4179 
4180             // Create result file
4181             File res = new File(values.getAsString(MediaColumns.DATA));
4182             try {
4183                 if (makeUnique) {
4184                     res = FileUtils.buildUniqueFile(res.getParentFile(),
4185                             mimeType, res.getName());
4186                 } else {
4187                     res = FileUtils.buildNonUniqueFile(res.getParentFile(),
4188                             mimeType, res.getName());
4189                 }
4190             } catch (FileNotFoundException e) {
4191                 throw new IllegalStateException(
4192                         "Failed to build unique file: " + res + " " + values);
4193             }
4194 
4195             // Require that content lives under well-defined directories to help
4196             // keep the user's content organized
4197 
4198             // Start by saying unchanged directories are valid
4199             final String currentDir = (currentPath != null)
4200                     ? new File(currentPath).getParent() : null;
4201             boolean validPath = res.getParent().equals(currentDir);
4202 
4203             // Next, consider allowing based on allowed primary directory
4204             final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/");
4205             final String primary = extractTopLevelDir(relativePath);
4206             if (!validPath) {
4207                 validPath = containsIgnoreCase(allowedPrimary, primary);
4208             }
4209 
4210             // Next, consider allowing paths when referencing a related item
4211             final Uri relatedUri = extras.getParcelable(QUERY_ARG_RELATED_URI);
4212             if (!validPath && relatedUri != null) {
4213                 try (Cursor c = queryForSingleItem(relatedUri, new String[] {
4214                         MediaColumns.MIME_TYPE,
4215                         MediaColumns.RELATIVE_PATH,
4216                 }, null, null, null)) {
4217                     // If top-level MIME type matches, and relative path
4218                     // matches, then allow caller to place things here
4219 
4220                     final String expectedType = MimeUtils.extractPrimaryType(
4221                             c.getString(0));
4222                     final String actualType = MimeUtils.extractPrimaryType(
4223                             values.getAsString(MediaColumns.MIME_TYPE));
4224                     if (!Objects.equals(expectedType, actualType)) {
4225                         throw new IllegalArgumentException("Placement of " + actualType
4226                                 + " item not allowed in relation to " + expectedType + " item");
4227                     }
4228 
4229                     final String expectedPath = c.getString(1);
4230                     final String actualPath = values.getAsString(MediaColumns.RELATIVE_PATH);
4231                     if (!Objects.equals(expectedPath, actualPath)) {
4232                         throw new IllegalArgumentException("Placement of " + actualPath
4233                                 + " item not allowed in relation to " + expectedPath + " item");
4234                     }
4235 
4236                     // If we didn't see any trouble above, then we'll allow it
4237                     validPath = true;
4238                 } catch (FileNotFoundException e) {
4239                     Log.w(TAG, "Failed to find related item " + relatedUri + ": " + e);
4240                 }
4241             }
4242 
4243             // Consider allowing external media directory of calling package
4244             if (!validPath) {
4245                 final String pathOwnerPackage = extractPathOwnerPackageName(res.getAbsolutePath());
4246                 if (pathOwnerPackage != null) {
4247                     validPath = isExternalMediaDirectory(res.getAbsolutePath()) &&
4248                             isCallingIdentitySharedPackageName(pathOwnerPackage);
4249                 }
4250             }
4251 
4252             // Allow apps with MANAGE_EXTERNAL_STORAGE to create files anywhere
4253             if (!validPath) {
4254                 validPath = isCallingPackageManager();
4255             }
4256 
4257             // Allow system gallery to create image/video files.
4258             if (!validPath) {
4259                 // System gallery can create image/video files in any existing directory, it can
4260                 // also create subdirectories in any existing top-level directory. However, system
4261                 // gallery is not allowed to create non-default top level directory.
4262                 final boolean createNonDefaultTopLevelDir = primary != null &&
4263                         !FileUtils.buildPath(volumePath, primary).exists();
4264                 validPath = !createNonDefaultTopLevelDir && canSystemGalleryAccessTheFile(
4265                         res.getAbsolutePath());
4266             }
4267 
4268             // Nothing left to check; caller can't use this path
4269             if (!validPath) {
4270                 throw new IllegalArgumentException(
4271                         "Primary directory " + primary + " not allowed for " + uri
4272                                 + "; allowed directories are " + allowedPrimary);
4273             }
4274 
4275             boolean isFuseThread = isFuseThread();
4276             // Check if the following are true:
4277             // 1. Not a FUSE thread
4278             // 2. |res| is a child of a default dir and the default dir is missing
4279             // If true, we want to update the mTime of the volume root, after creating the dir
4280             // on the lower filesystem. This fixes some FileManagers relying on the mTime change
4281             // for UI updates
4282             File defaultDirVolumePath =
4283                     isFuseThread ? null : checkDefaultDirMissing(resolvedVolumeName, res);
4284             // Ensure all parent folders of result file exist
4285             res.getParentFile().mkdirs();
4286             if (!res.getParentFile().exists()) {
4287                 throw new IllegalStateException("Failed to create directory: " + res);
4288             }
4289             touchFusePath(defaultDirVolumePath);
4290 
4291             values.put(MediaColumns.DATA, res.getAbsolutePath());
4292             // buildFile may have changed the file name, compute values to extract new DISPLAY_NAME.
4293             // Note: We can't extract displayName from res.getPath() because for pending & trashed
4294             // files DISPLAY_NAME will not be same as file name.
4295             FileUtils.computeValuesFromData(values, isFuseThread);
4296         } else {
4297             assertFileColumnsConsistent(match, uri, values);
4298         }
4299 
4300         assertPrivatePathNotInValues(values);
4301 
4302         // Drop columns that aren't relevant for special tables
4303         switch (match) {
4304             case AUDIO_ALBUMART:
4305             case VIDEO_THUMBNAILS:
4306             case IMAGES_THUMBNAILS:
4307                 final Set<String> valid = getProjectionMap(MediaStore.Images.Thumbnails.class)
4308                         .keySet();
4309                 for (String key : new ArraySet<>(values.keySet())) {
4310                     if (!valid.contains(key)) {
4311                         values.remove(key);
4312                     }
4313                 }
4314                 break;
4315         }
4316 
4317         Trace.endSection();
4318     }
4319 
4320     /**
4321      * For apps targetSdk >= S: Check that values does not contain any external private path.
4322      * For all apps: Check that values does not contain any other app's external private paths.
4323      */
assertPrivatePathNotInValues(ContentValues values)4324     private void assertPrivatePathNotInValues(ContentValues values)
4325             throws IllegalArgumentException {
4326         ArrayList<String> relativePaths = new ArrayList<String>();
4327         relativePaths.add(extractRelativePath(values.getAsString(MediaColumns.DATA)));
4328         relativePaths.add(values.getAsString(MediaColumns.RELATIVE_PATH));
4329 
4330         for (final String relativePath : relativePaths) {
4331             if (!isDataOrObbRelativePath(relativePath)) {
4332                 continue;
4333             }
4334 
4335             /**
4336              * Don't allow apps to insert/update database row to files in Android/data or
4337              * Android/obb dirs. These are app private directories and files in these private
4338              * directories can't be added to public media collection.
4339              *
4340              * Note: For backwards compatibility we allow apps with targetSdk < S to insert private
4341              * files to MediaProvider
4342              */
4343             if (CompatChanges.isChangeEnabled(ENABLE_CHECKS_FOR_PRIVATE_FILES,
4344                     Binder.getCallingUid())) {
4345                 throw new IllegalArgumentException(
4346                         "Inserting private file: " + relativePath + " is not allowed.");
4347             }
4348 
4349             /**
4350              * Restrict all (legacy and non-legacy) apps from inserting paths in other
4351              * app's private directories.
4352              * Allow legacy apps to insert/update files in app private directories for backward
4353              * compatibility but don't allow them to do so in other app's private directories.
4354              */
4355             if (!isCallingIdentityAllowedAccessToDataOrObbPath(relativePath)) {
4356                 throw new IllegalArgumentException(
4357                         "Inserting private file: " + relativePath + " is not allowed.");
4358             }
4359         }
4360     }
4361 
4362     /**
4363      * @return the default dir if {@code file} is a child of default dir and it's missing,
4364      * {@code null} otherwise.
4365      */
checkDefaultDirMissing(String volumeName, File file)4366     private File checkDefaultDirMissing(String volumeName, File file) {
4367         String topLevelDir = FileUtils.extractTopLevelDir(file.getPath());
4368         if (topLevelDir != null && FileUtils.isDefaultDirectoryName(topLevelDir)) {
4369             try {
4370                 File volumePath = getVolumePath(volumeName);
4371                 if (!new File(volumePath, topLevelDir).exists()) {
4372                     return volumePath;
4373                 }
4374             } catch (FileNotFoundException e) {
4375                 Log.w(TAG, "Failed to checkDefaultDirMissing for " + file, e);
4376             }
4377         }
4378         return null;
4379     }
4380 
4381     /** Updates mTime of {@code path} on the FUSE filesystem */
touchFusePath(@ullable File path)4382     private void touchFusePath(@Nullable File path) {
4383         if (path != null) {
4384             // Touch root of volume to update mTime on FUSE filesystem
4385             // This allows FileManagers that may be relying on mTime changes to update their UI
4386             File fusePath = toFuseFile(path);
4387             Log.i(TAG, "Touching FUSE path " + fusePath);
4388             fusePath.setLastModified(System.currentTimeMillis());
4389         }
4390     }
4391 
4392     /**
4393      * Check that any requested {@link MediaColumns#DATA} paths actually
4394      * live on the storage volume being targeted.
4395      */
assertFileColumnsConsistent(int match, Uri uri, ContentValues values)4396     private void assertFileColumnsConsistent(int match, Uri uri, ContentValues values)
4397             throws VolumeArgumentException, VolumeNotFoundException {
4398         if (!values.containsKey(MediaColumns.DATA)) return;
4399 
4400         final String volumeName = resolveVolumeName(uri);
4401         try {
4402             // Quick check that the requested path actually lives on volume
4403             final Collection<File> allowed = getAllowedVolumePaths(volumeName);
4404             final File actual = new File(values.getAsString(MediaColumns.DATA))
4405                     .getCanonicalFile();
4406             if (!FileUtils.contains(allowed, actual)) {
4407                 throw new VolumeArgumentException(actual, allowed);
4408             }
4409         } catch (IOException e) {
4410             throw new VolumeNotFoundException(volumeName);
4411         }
4412     }
4413 
4414     @Override
bulkInsert(Uri uri, ContentValues[] values)4415     public int bulkInsert(Uri uri, ContentValues[] values) {
4416         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
4417         final boolean allowHidden = isCallingPackageAllowedHidden();
4418         final int match = matchUri(uri, allowHidden);
4419 
4420         if (match == VOLUMES) {
4421             return super.bulkInsert(uri, values);
4422         }
4423 
4424         if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) {
4425             final String resolvedVolumeName = resolveVolumeName(uri);
4426 
4427             final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
4428             final Uri playlistUri = ContentUris.withAppendedId(
4429                     MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId);
4430 
4431             final String audioVolumeName =
4432                     MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)
4433                             ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL;
4434 
4435             // Require that caller has write access to underlying media
4436             enforceCallingPermission(playlistUri, Bundle.EMPTY, true);
4437             for (ContentValues each : values) {
4438                 final long audioId = each.getAsLong(Audio.Playlists.Members.AUDIO_ID);
4439                 final Uri audioUri = Audio.Media.getContentUri(audioVolumeName, audioId);
4440                 enforceCallingPermission(audioUri, Bundle.EMPTY, false);
4441             }
4442 
4443             return bulkInsertPlaylist(playlistUri, values);
4444         }
4445 
4446         final DatabaseHelper helper;
4447         try {
4448             helper = getDatabaseForUri(uri);
4449         } catch (VolumeNotFoundException e) {
4450             return e.translateForUpdateDelete(targetSdkVersion);
4451         }
4452 
4453         helper.beginTransaction();
4454         try {
4455             final int result = super.bulkInsert(uri, values);
4456             helper.setTransactionSuccessful();
4457             return result;
4458         } finally {
4459             helper.endTransaction();
4460         }
4461     }
4462 
bulkInsertPlaylist(@onNull Uri uri, @NonNull ContentValues[] values)4463     private int bulkInsertPlaylist(@NonNull Uri uri, @NonNull ContentValues[] values) {
4464         Trace.beginSection("MP.bulkInsertPlaylist");
4465         try {
4466             try {
4467                 return addPlaylistMembers(uri, values);
4468             } catch (SQLiteConstraintException e) {
4469                 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
4470                     throw e;
4471                 } else {
4472                     return 0;
4473                 }
4474             }
4475         } catch (FallbackException e) {
4476             return e.translateForBulkInsert(getCallingPackageTargetSdkVersion());
4477         } finally {
4478             Trace.endSection();
4479         }
4480     }
4481 
insertDirectory(@onNull SQLiteDatabase db, @NonNull String path)4482     private long insertDirectory(@NonNull SQLiteDatabase db, @NonNull String path) {
4483         if (LOGV) Log.v(TAG, "inserting directory " + path);
4484         ContentValues values = new ContentValues();
4485         values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
4486         values.put(FileColumns.DATA, path);
4487         values.put(FileColumns.PARENT, getParent(db, path));
4488         values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path));
4489         values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
4490         values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
4491         values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
4492         values.put(FileColumns.IS_DOWNLOAD, isDownload(path) ? 1 : 0);
4493 
4494         // Getting UserId from the directory path, as clone user shares the MediaProvider
4495         // of user 0.
4496         int userIdFromPath = FileUtils.extractUserId(path);
4497         // In some cases, like querying public volumes, userId is not available in path. We
4498         // take userId from the user running MediaProvider process (sUserId).
4499         if (userIdFromPath != -1) {
4500             if (isAppCloneUserForFuse(userIdFromPath)) {
4501                 values.put(FileColumns._USER_ID, userIdFromPath);
4502             } else {
4503                 values.put(FileColumns._USER_ID, sUserId);
4504             }
4505         }
4506 
4507         File file = new File(path);
4508         if (file.exists()) {
4509             values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
4510         }
4511         return db.insert("files", FileColumns.DATE_MODIFIED, values);
4512     }
4513 
getParent(@onNull SQLiteDatabase db, @NonNull String path)4514     private long getParent(@NonNull SQLiteDatabase db, @NonNull String path) {
4515         final String parentPath = new File(path).getParent();
4516         if (Objects.equals("/", parentPath)) {
4517             return -1;
4518         } else {
4519             synchronized (mDirectoryCache) {
4520                 Long id = mDirectoryCache.get(parentPath);
4521                 if (id != null) {
4522                     return id;
4523                 }
4524             }
4525 
4526             final long id;
4527             try (Cursor c = db.query("files", new String[] { FileColumns._ID },
4528                     FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) {
4529                 if (c.moveToFirst()) {
4530                     id = c.getLong(0);
4531                 } else {
4532                     id = insertDirectory(db, parentPath);
4533                 }
4534             }
4535 
4536             synchronized (mDirectoryCache) {
4537                 mDirectoryCache.put(parentPath, id);
4538             }
4539             return id;
4540         }
4541     }
4542 
4543     /**
4544      * @param c the Cursor whose title to retrieve
4545      * @return the result of {@link #getDefaultTitle(String)} if the result is valid; otherwise
4546      * the value of the {@code MediaStore.Audio.Media.TITLE} column
4547      */
getDefaultTitleFromCursor(Cursor c)4548     private String getDefaultTitleFromCursor(Cursor c) {
4549         String title = null;
4550         final int columnIndex = c.getColumnIndex("title_resource_uri");
4551         // Necessary to check for existence because we may be reading from an old DB version
4552         if (columnIndex > -1) {
4553             final String titleResourceUri = c.getString(columnIndex);
4554             if (titleResourceUri != null) {
4555                 try {
4556                     title = getDefaultTitle(titleResourceUri);
4557                 } catch (Exception e) {
4558                     // Best attempt only
4559                 }
4560             }
4561         }
4562         if (title == null) {
4563             title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE));
4564         }
4565         return title;
4566     }
4567 
4568     /**
4569      * @param title_resource_uri The title resource for which to retrieve the default localization
4570      * @return The title localized to {@code Locale.US}, or {@code null} if unlocalizable
4571      * @throws Exception Thrown if the title appears to be localizable, but the localization failed
4572      * for any reason. For example, the application from which the localized title is fetched is not
4573      * installed, or it does not have the resource which needs to be localized
4574      */
getDefaultTitle(String title_resource_uri)4575     private String getDefaultTitle(String title_resource_uri) throws Exception{
4576         try {
4577             return getTitleFromResourceUri(title_resource_uri, false);
4578         } catch (Exception e) {
4579             Log.e(TAG, "Error getting default title for " + title_resource_uri, e);
4580             throw e;
4581         }
4582     }
4583 
4584     /**
4585      * @param title_resource_uri The title resource to localize
4586      * @return The localized title, or {@code null} if unlocalizable
4587      * @throws Exception Thrown if the title appears to be localizable, but the localization failed
4588      * for any reason. For example, the application from which the localized title is fetched is not
4589      * installed, or it does not have the resource which needs to be localized
4590      */
getLocalizedTitle(String title_resource_uri)4591     private String getLocalizedTitle(String title_resource_uri) throws Exception {
4592         try {
4593             return getTitleFromResourceUri(title_resource_uri, true);
4594         } catch (Exception e) {
4595             Log.e(TAG, "Error getting localized title for " + title_resource_uri, e);
4596             throw e;
4597         }
4598     }
4599 
4600     /**
4601      * Localizable titles conform to this URI pattern:
4602      *   Scheme: {@link ContentResolver.SCHEME_ANDROID_RESOURCE}
4603      *   Authority: Package Name of ringtone title provider
4604      *   First Path Segment: Type of resource (must be "string")
4605      *   Second Path Segment: Resource name of title
4606      *
4607      * @param title_resource_uri The title resource to retrieve
4608      * @param localize Whether or not to localize the title
4609      * @return The title, or {@code null} if unlocalizable
4610      * @throws Exception Thrown if the title appears to be localizable, but the localization failed
4611      * for any reason. For example, the application from which the localized title is fetched is not
4612      * installed, or it does not have the resource which needs to be localized
4613      */
getTitleFromResourceUri(String title_resource_uri, boolean localize)4614     private String getTitleFromResourceUri(String title_resource_uri, boolean localize)
4615         throws Exception {
4616         if (TextUtils.isEmpty(title_resource_uri)) {
4617             return null;
4618         }
4619         final Uri titleUri = Uri.parse(title_resource_uri);
4620         final String scheme = titleUri.getScheme();
4621         if (!ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
4622             return null;
4623         }
4624         final List<String> pathSegments = titleUri.getPathSegments();
4625         if (pathSegments.size() != 2) {
4626             Log.e(TAG, "Error getting localized title for " + title_resource_uri
4627                 + ", must have 2 path segments");
4628             return null;
4629         }
4630         final String type = pathSegments.get(0);
4631         if (!"string".equals(type)) {
4632             Log.e(TAG, "Error getting localized title for " + title_resource_uri
4633                 + ", first path segment must be \"string\"");
4634             return null;
4635         }
4636         final String packageName = titleUri.getAuthority();
4637         final Resources resources;
4638         if (localize) {
4639             resources = mPackageManager.getResourcesForApplication(packageName);
4640         } else {
4641             final Context packageContext = getContext().createPackageContext(packageName, 0);
4642             final Configuration configuration = packageContext.getResources().getConfiguration();
4643             configuration.setLocale(Locale.US);
4644             resources = packageContext.createConfigurationContext(configuration).getResources();
4645         }
4646         final String resourceIdentifier = pathSegments.get(1);
4647         final int id = resources.getIdentifier(resourceIdentifier, type, packageName);
4648         return resources.getString(id);
4649     }
4650 
onLocaleChanged()4651     public void onLocaleChanged() {
4652         onLocaleChanged(true);
4653     }
4654 
onLocaleChanged(boolean forceUpdate)4655     private void onLocaleChanged(boolean forceUpdate) {
4656         mInternalDatabase.runWithTransaction((db) -> {
4657             if (forceUpdate || !mLastLocale.equals(Locale.getDefault())) {
4658                 localizeTitles(db);
4659                 mLastLocale = Locale.getDefault();
4660             }
4661             return null;
4662         });
4663     }
4664 
localizeTitles(@onNull SQLiteDatabase db)4665     private void localizeTitles(@NonNull SQLiteDatabase db) {
4666         try (Cursor c = db.query("files", new String[]{"_id", "title_resource_uri"},
4667             "title_resource_uri IS NOT NULL", null, null, null, null)) {
4668             while (c.moveToNext()) {
4669                 final String id = c.getString(0);
4670                 final String titleResourceUri = c.getString(1);
4671                 final ContentValues values = new ContentValues();
4672                 try {
4673                     values.put(AudioColumns.TITLE_RESOURCE_URI, titleResourceUri);
4674                     computeAudioLocalizedValues(values);
4675                     computeAudioKeyValues(values);
4676                     db.update("files", values, "_id=?", new String[]{id});
4677                 } catch (Exception e) {
4678                     Log.e(TAG, "Error updating localized title for " + titleResourceUri
4679                         + ", keeping old localization");
4680                 }
4681             }
4682         }
4683     }
4684 
insertFile(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, int mediaType)4685     private Uri insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper,
4686             int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values,
4687             int mediaType) throws VolumeArgumentException, VolumeNotFoundException {
4688         boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA)
4689                 || TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA));
4690 
4691         // Make sure all file-related columns are defined
4692         ensureUniqueFileColumns(match, uri, extras, values, null);
4693 
4694         switch (mediaType) {
4695             case FileColumns.MEDIA_TYPE_AUDIO: {
4696                 computeAudioLocalizedValues(values);
4697                 computeAudioKeyValues(values);
4698                 break;
4699             }
4700         }
4701 
4702         // compute bucket_id and bucket_display_name for all files
4703         String path = values.getAsString(MediaStore.MediaColumns.DATA);
4704         FileUtils.computeValuesFromData(values, isFuseThread());
4705         values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
4706 
4707         String title = values.getAsString(MediaStore.MediaColumns.TITLE);
4708         if (title == null && path != null) {
4709             title = extractFileName(path);
4710         }
4711         values.put(FileColumns.TITLE, title);
4712 
4713         String mimeType = null;
4714         int format = MtpConstants.FORMAT_ASSOCIATION;
4715         if (path != null && new File(path).isDirectory()) {
4716             values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
4717             values.putNull(MediaStore.MediaColumns.MIME_TYPE);
4718         } else {
4719             mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE);
4720             final Integer formatObject = values.getAsInteger(FileColumns.FORMAT);
4721             format = (formatObject == null ? 0 : formatObject);
4722         }
4723 
4724         if (format == 0) {
4725             format = MimeUtils.resolveFormatCode(mimeType);
4726         }
4727         if (path != null && path.endsWith("/")) {
4728             // TODO: convert to using FallbackException once VERSION_CODES.S is defined
4729             Log.e(TAG, "directory has trailing slash: " + path);
4730             return null;
4731         }
4732         if (format != 0) {
4733             values.put(FileColumns.FORMAT, format);
4734         }
4735 
4736         if (mimeType == null && path != null && format != MtpConstants.FORMAT_ASSOCIATION) {
4737             mimeType = MimeUtils.resolveMimeType(new File(path));
4738         }
4739 
4740         if (mimeType != null) {
4741             values.put(FileColumns.MIME_TYPE, mimeType);
4742             if (isCallingPackageSelf() && values.containsKey(FileColumns.MEDIA_TYPE)) {
4743                 // Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and
4744                 // FileColumns.MEDIA_TYPE is already populated.
4745             } else if (isFuseThread() && path != null
4746                     && FileUtils.shouldFileBeHidden(new File(path))) {
4747                 // We should only mark MEDIA_TYPE as MEDIA_TYPE_NONE for Fuse Thread.
4748                 // MediaProvider#insert() returns the uri by appending the "rowId" to the given
4749                 // uri, hence to ensure the correct working of the returned uri, we shouldn't
4750                 // change the MEDIA_TYPE in insert operation and let scan change it for us.
4751                 values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
4752             } else {
4753                 values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType));
4754             }
4755         } else {
4756             values.put(FileColumns.MEDIA_TYPE, mediaType);
4757         }
4758 
4759         qb.allowColumn(FileColumns._MODIFIER);
4760         if (isCallingPackageSelf() && values.containsKey(FileColumns._MODIFIER)) {
4761             // We can't identify if the call is coming from media scan, hence
4762             // we let ModernMediaScanner send FileColumns._MODIFIER value.
4763         } else if (isFuseThread()) {
4764             values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE);
4765         } else {
4766             values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR);
4767         }
4768 
4769         // There is no meaning of an owner in the internal storage. It is shared by all users.
4770         // So we only set the user_id field in the database for external storage.
4771         qb.allowColumn(FileColumns._USER_ID);
4772         int ownerUserId = FileUtils.extractUserId(path);
4773         if (helper.isExternal()) {
4774             if (isAppCloneUserForFuse(ownerUserId)) {
4775                 values.put(FileColumns._USER_ID, ownerUserId);
4776             } else {
4777                 values.put(FileColumns._USER_ID, sUserId);
4778             }
4779         }
4780 
4781         final long rowId;
4782         Uri newUri = uri;
4783         {
4784             if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
4785                 String name = values.getAsString(Audio.Playlists.NAME);
4786                 if (name == null && path == null) {
4787                     // MediaScanner will compute the name from the path if we have one
4788                     throw new IllegalArgumentException(
4789                             "no name was provided when inserting abstract playlist");
4790                 }
4791             } else {
4792                 if (path == null) {
4793                     // path might be null for playlists created on the device
4794                     // or transfered via MTP
4795                     throw new IllegalArgumentException(
4796                             "no path was provided when inserting new file");
4797                 }
4798             }
4799 
4800             // make sure modification date and size are set
4801             if (path != null) {
4802                 File file = new File(path);
4803                 if (file.exists()) {
4804                     values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
4805                     if (!values.containsKey(FileColumns.SIZE)) {
4806                         values.put(FileColumns.SIZE, file.length());
4807                     }
4808                 }
4809                 // Checking if the file/directory is hidden can be expensive based on the depth of
4810                 // the directory tree. Call shouldFileBeHidden() only when the caller of insert()
4811                 // cares about returned uri.
4812                 if (!isCallingPackageSelf() && !isFuseThread()
4813                         && FileUtils.shouldFileBeHidden(file)) {
4814                     newUri = MediaStore.Files.getContentUri(MediaStore.getVolumeName(uri));
4815                 }
4816             }
4817 
4818             rowId = insertAllowingUpsert(qb, helper, values, path);
4819         }
4820         if (format == MtpConstants.FORMAT_ASSOCIATION) {
4821             synchronized (mDirectoryCache) {
4822                 mDirectoryCache.put(path, rowId);
4823             }
4824         }
4825 
4826         return ContentUris.withAppendedId(newUri, rowId);
4827     }
4828 
4829     /**
4830      * Inserts a new row in MediaProvider database with {@code values}. Treats insert as upsert for
4831      * double inserts from same package.
4832      */
insertAllowingUpsert(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)4833     private long insertAllowingUpsert(@NonNull SQLiteQueryBuilder qb,
4834             @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)
4835             throws SQLiteConstraintException {
4836         return helper.runWithTransaction((db) -> {
4837             Long parent = values.getAsLong(FileColumns.PARENT);
4838             if (parent == null) {
4839                 if (path != null) {
4840                     final long parentId = getParent(db, path);
4841                     values.put(FileColumns.PARENT, parentId);
4842                 }
4843             }
4844 
4845             try {
4846                 return qb.insert(helper, values);
4847             } catch (SQLiteConstraintException e) {
4848                 final String packages = getAllowedPackagesForUpsert(
4849                         values.getAsString(MediaColumns.OWNER_PACKAGE_NAME));
4850                 SQLiteQueryBuilder qbForUpsert = getQueryBuilderForUpsert(path);
4851                 final long rowId = getIdIfPathOwnedByPackages(qbForUpsert, helper, path, packages);
4852                 // Apps sometimes create a file via direct path and then insert it into
4853                 // MediaStore via ContentResolver. The former should create a database entry,
4854                 // so we have to treat the latter as an upsert.
4855                 // TODO(b/149917493) Perform all INSERT operations as UPSERT.
4856                 if (rowId != -1 && qbForUpsert.update(helper, values, "_id=?",
4857                         new String[]{Long.toString(rowId)}) == 1) {
4858                     return rowId;
4859                 }
4860                 // Rethrow SQLiteConstraintException on failed upsert.
4861                 throw e;
4862             }
4863         });
4864     }
4865 
4866     /**
4867      * @return row id of the entry with path {@code path} if the owner is one of {@code packages}.
4868      */
4869     private long getIdIfPathOwnedByPackages(@NonNull SQLiteQueryBuilder qb,
4870             @NonNull DatabaseHelper helper, String path, String packages) {
4871         final String[] projection = new String[] {FileColumns._ID};
4872         final  String ownerPackageMatchClause = DatabaseUtils.bindSelection(
4873                 MediaColumns.OWNER_PACKAGE_NAME + " IN " + packages);
4874         final String selection = FileColumns.DATA + " =? AND " + ownerPackageMatchClause;
4875 
4876         try (Cursor c = qb.query(helper, projection, selection, new String[] {path}, null, null,
4877                 null, null, null)) {
4878             if (c.moveToFirst()) {
4879                 return c.getLong(0);
4880             }
4881         }
4882         return -1;
4883     }
4884 
4885     /**
4886      * Gets packages that should match to upsert a db row.
4887      *
4888      * A database row can be upserted if
4889      * <ul>
4890      * <li> Calling package or one of the shared packages owns the db row.
4891      * <li> {@code givenOwnerPackage} owns the db row. This is useful when DownloadProvider
4892      * requests upsert on behalf of another app
4893      * </ul>
4894      */
4895     private String getAllowedPackagesForUpsert(@Nullable String givenOwnerPackage) {
4896         ArrayList<String> packages = new ArrayList<>(
4897                 Arrays.asList(mCallingIdentity.get().getSharedPackageNamesArray()));
4898 
4899         // If givenOwnerPackage is CallingIdentity, packages list would already have shared package
4900         // names of givenOwnerPackage. If givenOwnerPackage is not CallingIdentity, since
4901         // DownloadProvider can upsert a row on behalf of app, we should include all shared packages
4902         // of givenOwnerPackage.
4903         if (givenOwnerPackage != null && isCallingPackageDelegator() &&
4904                 !isCallingIdentitySharedPackageName(givenOwnerPackage)) {
4905             // Allow DownloadProvider to Upsert if givenOwnerPackage is owner of the db row.
4906             packages.addAll(Arrays.asList(getSharedPackagesForPackage(givenOwnerPackage)));
4907         }
4908         return bindList((Object[]) packages.toArray());
4909     }
4910 
4911     /**
4912      * @return {@link SQLiteQueryBuilder} for upsert with Files uri. This disables strict columns
4913      * check to allow upsert to update any column with Files uri.
4914      */
4915     private SQLiteQueryBuilder getQueryBuilderForUpsert(@NonNull String path) {
4916         final boolean allowHidden = isCallingPackageAllowedHidden();
4917         Bundle extras = new Bundle();
4918         extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE);
4919         extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE);
4920 
4921         // When Fuse inserts a file to database it doesn't set is_download column. When app tries
4922         // insert with Downloads uri, upsert fails because getIdIfPathExistsForCallingPackage can't
4923         // find a row ID with is_download=1. Use Files uri to get queryBuilder & update any existing
4924         // row irrespective of is_download=1.
4925         final Uri uri = FileUtils.getContentUriForPath(path);
4926         SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri,
4927                 extras, null);
4928 
4929         // We won't be able to update columns that are not part of projection map of Files table. We
4930         // have already checked strict columns in previous insert operation which failed with
4931         // exception. Any malicious column usage would have got caught in insert operation, hence we
4932         // can safely disable strict column check for upsert.
4933         qb.setStrictColumns(false);
4934         return qb;
4935     }
4936 
4937     private void maybePut(@NonNull ContentValues values, @NonNull String key,
4938             @Nullable String value) {
4939         if (value != null) {
4940             values.put(key, value);
4941         }
4942     }
4943 
4944     private boolean maybeMarkAsDownload(@NonNull ContentValues values) {
4945         final String path = values.getAsString(MediaColumns.DATA);
4946         if (path != null && isDownload(path)) {
4947             values.put(FileColumns.IS_DOWNLOAD, 1);
4948             return true;
4949         }
4950         return false;
4951     }
4952 
4953     @NonNull
4954     private static String resolveVolumeName(@NonNull Uri uri) {
4955         final String volumeName = getVolumeName(uri);
4956         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
4957             return MediaStore.VOLUME_EXTERNAL_PRIMARY;
4958         } else {
4959             return volumeName;
4960         }
4961     }
4962 
4963     /**
4964      * @deprecated all operations should be routed through the overload that
4965      *             accepts a {@link Bundle} of extras.
4966      */
4967     @Override
4968     @Deprecated
4969     public Uri insert(Uri uri, ContentValues values) {
4970         return insert(uri, values, null);
4971     }
4972 
4973     @Override
4974     @Nullable
4975     public Uri insert(@NonNull Uri uri, @Nullable ContentValues values,
4976             @Nullable Bundle extras) {
4977         Trace.beginSection("MP.insert [" + uri + ']');
4978         try {
4979             try {
4980                 return insertInternal(uri, values, extras);
4981             } catch (SQLiteConstraintException e) {
4982                 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
4983                     throw e;
4984                 } else {
4985                     return null;
4986                 }
4987             }
4988         } catch (FallbackException e) {
4989             return e.translateForInsert(getCallingPackageTargetSdkVersion());
4990         } finally {
4991             Trace.endSection();
4992         }
4993     }
4994 
4995     @Nullable
4996     private Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
4997             @Nullable Bundle extras) throws FallbackException {
4998         final String originalVolumeName = getVolumeName(uri);
4999         PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), originalVolumeName);
5000 
5001         extras = (extras != null) ? extras : new Bundle();
5002         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
5003         extras.remove(QUERY_ARG_REDACTED_URI);
5004 
5005         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
5006         extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
5007 
5008         final boolean allowHidden = isCallingPackageAllowedHidden();
5009         final int match = matchUri(uri, allowHidden);
5010 
5011         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
5012         final String resolvedVolumeName = resolveVolumeName(uri);
5013 
5014         // handle MEDIA_SCANNER before calling getDatabaseForUri()
5015         if (match == MEDIA_SCANNER) {
5016             mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);
5017 
5018             final DatabaseHelper helper = getDatabaseForUri(
5019                     MediaStore.Files.getContentUri(mMediaScannerVolume));
5020 
5021             helper.mScanStartTime = SystemClock.elapsedRealtime();
5022             return MediaStore.getMediaScannerUri();
5023         }
5024 
5025         if (match == VOLUMES) {
5026             String name = initialValues.getAsString("name");
5027             MediaVolume volume = null;
5028             try {
5029                 volume = getVolume(name);
5030                 Uri attachedVolume = attachVolume(volume, /* validate */ true);
5031                 if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) {
5032                     final DatabaseHelper helper = getDatabaseForUri(
5033                             MediaStore.Files.getContentUri(mMediaScannerVolume));
5034                     helper.mScanStartTime = SystemClock.elapsedRealtime();
5035                 }
5036                 return attachedVolume;
5037             } catch (FileNotFoundException e) {
5038                 Log.w(TAG, "Couldn't find volume with name " + volume.getName());
5039                 return null;
5040             }
5041         }
5042 
5043         final DatabaseHelper helper = getDatabaseForUri(uri);
5044         switch (match) {
5045             case AUDIO_PLAYLISTS_ID:
5046             case AUDIO_PLAYLISTS_ID_MEMBERS: {
5047                 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
5048                 final Uri playlistUri = ContentUris.withAppendedId(
5049                         MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId);
5050 
5051                 final long audioId = initialValues
5052                         .getAsLong(MediaStore.Audio.Playlists.Members.AUDIO_ID);
5053                 final String audioVolumeName =
5054                         MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)
5055                                 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL;
5056                 final Uri audioUri = ContentUris.withAppendedId(
5057                         MediaStore.Audio.Media.getContentUri(audioVolumeName), audioId);
5058 
5059                 // Require that caller has write access to underlying media
5060                 enforceCallingPermission(playlistUri, Bundle.EMPTY, true);
5061                 enforceCallingPermission(audioUri, Bundle.EMPTY, false);
5062 
5063                 // Playlist contents are always persisted directly into playlist
5064                 // files on disk to ensure that we can reliably migrate between
5065                 // devices and recover from database corruption
5066                 final long id = addPlaylistMembers(playlistUri, initialValues);
5067                 acceptWithExpansion(helper::notifyInsert, resolvedVolumeName, playlistId,
5068                         FileColumns.MEDIA_TYPE_PLAYLIST, false);
5069                 return ContentUris.withAppendedId(MediaStore.Audio.Playlists.Members
5070                         .getContentUri(originalVolumeName, playlistId), id);
5071             }
5072         }
5073 
5074         String path = null;
5075         String ownerPackageName = null;
5076         if (initialValues != null) {
5077             // IDs are forever; nobody should be editing them
5078             initialValues.remove(MediaColumns._ID);
5079 
5080             // Expiration times are hard-coded; let's derive them
5081             FileUtils.computeDateExpires(initialValues);
5082 
5083             // Ignore or augment incoming raw filesystem paths
5084             for (String column : sDataColumns.keySet()) {
5085                 if (!initialValues.containsKey(column)) continue;
5086 
5087                 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
5088                     // Mutation allowed
5089                 } else if (isCallingPackageManager()) {
5090                     // Apps with MANAGE_EXTERNAL_STORAGE have all files access, hence they are
5091                     // allowed to insert files anywhere.
5092                 } else if (getCallingPackageTargetSdkVersion() >=
5093                         Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
5094                     // Throwing an exception so that it doesn't result in some unexpected
5095                     // behavior for apps and make them aware of what is happening.
5096                     throw new IllegalArgumentException("Mutation of " + column
5097                         + " is not allowed.");
5098                 } else {
5099                     Log.w(TAG, "Ignoring mutation of  " + column + " from "
5100                         + getCallingPackageOrSelf());
5101                     initialValues.remove(column);
5102                 }
5103             }
5104 
5105             path = initialValues.getAsString(MediaStore.MediaColumns.DATA);
5106 
5107             if (!isCallingPackageSelf()) {
5108                 initialValues.remove(FileColumns.IS_DOWNLOAD);
5109             }
5110 
5111             // We no longer track location metadata
5112             if (initialValues.containsKey(ImageColumns.LATITUDE)) {
5113                 initialValues.putNull(ImageColumns.LATITUDE);
5114             }
5115             if (initialValues.containsKey(ImageColumns.LONGITUDE)) {
5116                 initialValues.putNull(ImageColumns.LONGITUDE);
5117             }
5118             if (getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) {
5119                 // These columns are removed in R.
5120                 if (initialValues.containsKey("primary_directory")) {
5121                     initialValues.remove("primary_directory");
5122                 }
5123                 if (initialValues.containsKey("secondary_directory")) {
5124                     initialValues.remove("secondary_directory");
5125                 }
5126             }
5127 
5128             if (isCallingPackageSelf() || isCallingPackageShell()) {
5129                 // When media inserted by ourselves during a scan, or by the
5130                 // shell, the best we can do is guess ownership based on path
5131                 // when it's not explicitly provided
5132                 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
5133                 if (TextUtils.isEmpty(ownerPackageName)) {
5134                     ownerPackageName = extractPathOwnerPackageName(path);
5135                 }
5136             } else if (isCallingPackageDelegator()) {
5137                 // When caller is a delegator, we handle ownership as a hybrid
5138                 // of the two other cases: we're willing to accept any ownership
5139                 // transfer attempted during insert, but we fall back to using
5140                 // the Binder identity if they don't request a specific owner
5141                 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
5142                 if (TextUtils.isEmpty(ownerPackageName)) {
5143                     ownerPackageName = getCallingPackageOrSelf();
5144                 }
5145             } else {
5146                 // Remote callers have no direct control over owner column; we force
5147                 // it be whoever is creating the content.
5148                 initialValues.remove(FileColumns.OWNER_PACKAGE_NAME);
5149                 ownerPackageName = getCallingPackageOrSelf();
5150             }
5151         }
5152 
5153         long rowId = -1;
5154         Uri newUri = null;
5155 
5156         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null);
5157 
5158         switch (match) {
5159             case IMAGES_MEDIA: {
5160                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
5161                 final boolean isDownload = maybeMarkAsDownload(initialValues);
5162                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
5163                         FileColumns.MEDIA_TYPE_IMAGE);
5164                 break;
5165             }
5166 
5167             case IMAGES_THUMBNAILS: {
5168                 if (helper.isInternal()) {
5169                     throw new UnsupportedOperationException(
5170                             "Writing to internal storage is not supported.");
5171                 }
5172 
5173                 // Require that caller has write access to underlying media
5174                 final long imageId = initialValues.getAsLong(MediaStore.Images.Thumbnails.IMAGE_ID);
5175                 enforceCallingPermission(ContentUris.withAppendedId(
5176                         MediaStore.Images.Media.getContentUri(resolvedVolumeName), imageId),
5177                         extras, true);
5178 
5179                 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
5180 
5181                 rowId = qb.insert(helper, initialValues);
5182                 if (rowId > 0) {
5183                     newUri = ContentUris.withAppendedId(Images.Thumbnails.
5184                             getContentUri(originalVolumeName), rowId);
5185                 }
5186                 break;
5187             }
5188 
5189             case VIDEO_THUMBNAILS: {
5190                 if (helper.isInternal()) {
5191                     throw new UnsupportedOperationException(
5192                             "Writing to internal storage is not supported.");
5193                 }
5194 
5195                 // Require that caller has write access to underlying media
5196                 final long videoId = initialValues.getAsLong(MediaStore.Video.Thumbnails.VIDEO_ID);
5197                 enforceCallingPermission(ContentUris.withAppendedId(
5198                         MediaStore.Video.Media.getContentUri(resolvedVolumeName), videoId),
5199                         Bundle.EMPTY, true);
5200 
5201                 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
5202 
5203                 rowId = qb.insert(helper, initialValues);
5204                 if (rowId > 0) {
5205                     newUri = ContentUris.withAppendedId(Video.Thumbnails.
5206                             getContentUri(originalVolumeName), rowId);
5207                 }
5208                 break;
5209             }
5210 
5211             case AUDIO_MEDIA: {
5212                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
5213                 final boolean isDownload = maybeMarkAsDownload(initialValues);
5214                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
5215                         FileColumns.MEDIA_TYPE_AUDIO);
5216                 break;
5217             }
5218 
5219             case AUDIO_MEDIA_ID_GENRES: {
5220                 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
5221             }
5222 
5223             case AUDIO_GENRES: {
5224                 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
5225             }
5226 
5227             case AUDIO_GENRES_ID_MEMBERS: {
5228                 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
5229             }
5230 
5231             case AUDIO_PLAYLISTS: {
5232                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
5233                 final boolean isDownload = maybeMarkAsDownload(initialValues);
5234                 ContentValues values = new ContentValues(initialValues);
5235                 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
5236                 // Playlist names are stored as display names, but leave
5237                 // values untouched if the caller is ModernMediaScanner
5238                 if (!isCallingPackageSelf()) {
5239                     if (values.containsKey(Playlists.NAME)) {
5240                         values.put(MediaColumns.DISPLAY_NAME, values.getAsString(Playlists.NAME));
5241                     }
5242                     if (!values.containsKey(MediaColumns.MIME_TYPE)) {
5243                         values.put(MediaColumns.MIME_TYPE, "audio/mpegurl");
5244                     }
5245                 }
5246                 newUri = insertFile(qb, helper, match, uri, extras, values,
5247                         FileColumns.MEDIA_TYPE_PLAYLIST);
5248                 if (newUri != null) {
5249                     // Touch empty playlist file on disk so its ready for renames
5250                     if (Binder.getCallingUid() != android.os.Process.myUid()) {
5251                         try (OutputStream out = ContentResolver.wrap(this)
5252                                 .openOutputStream(newUri)) {
5253                         } catch (IOException ignored) {
5254                         }
5255                     }
5256                 }
5257                 break;
5258             }
5259 
5260             case VIDEO_MEDIA: {
5261                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
5262                 final boolean isDownload = maybeMarkAsDownload(initialValues);
5263                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
5264                         FileColumns.MEDIA_TYPE_VIDEO);
5265                 break;
5266             }
5267 
5268             case AUDIO_ALBUMART: {
5269                 if (helper.isInternal()) {
5270                     throw new UnsupportedOperationException("no internal album art allowed");
5271                 }
5272 
5273                 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
5274 
5275                 rowId = qb.insert(helper, initialValues);
5276                 if (rowId > 0) {
5277                     newUri = ContentUris.withAppendedId(uri, rowId);
5278                 }
5279                 break;
5280             }
5281 
5282             case FILES: {
5283                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
5284                 final boolean isDownload = maybeMarkAsDownload(initialValues);
5285                 final String mimeType = initialValues.getAsString(MediaColumns.MIME_TYPE);
5286                 final int mediaType = MimeUtils.resolveMediaType(mimeType);
5287                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
5288                         mediaType);
5289                 break;
5290             }
5291 
5292             case DOWNLOADS:
5293                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
5294                 initialValues.put(FileColumns.IS_DOWNLOAD, 1);
5295                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
5296                         FileColumns.MEDIA_TYPE_NONE);
5297                 break;
5298 
5299             default:
5300                 throw new UnsupportedOperationException("Invalid URI " + uri);
5301         }
5302 
5303         // Remember that caller is owner of this item, to speed up future
5304         // permission checks for this caller
5305         mCallingIdentity.get().setOwned(rowId, true);
5306 
5307         if (path != null && path.toLowerCase(Locale.ROOT).endsWith("/.nomedia")) {
5308             scanFileAsMediaProvider(new File(path).getParentFile(), REASON_DEMAND);
5309         }
5310 
5311         return newUri;
5312     }
5313 
5314     @Override
5315     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
5316                 throws OperationApplicationException {
5317         // Open transactions on databases for requested volumes
5318         final Set<DatabaseHelper> transactions = new ArraySet<>();
5319         try {
5320             for (ContentProviderOperation op : operations) {
5321                 final DatabaseHelper helper = getDatabaseForUri(op.getUri());
5322                 if (transactions.contains(helper)) continue;
5323 
5324                 if (!helper.isTransactionActive()) {
5325                     helper.beginTransaction();
5326                     transactions.add(helper);
5327                 } else {
5328                     // We normally don't allow nested transactions (since we
5329                     // don't have a good way to selectively roll them back) but
5330                     // if the incoming operation is ignoring exceptions, then we
5331                     // don't need to worry about partial rollback and can
5332                     // piggyback on the larger active transaction
5333                     if (!op.isExceptionAllowed()) {
5334                         throw new IllegalStateException("Nested transactions not supported");
5335                     }
5336                 }
5337             }
5338 
5339             final ContentProviderResult[] result = super.applyBatch(operations);
5340             for (DatabaseHelper helper : transactions) {
5341                 helper.setTransactionSuccessful();
5342             }
5343             return result;
5344         } catch (VolumeNotFoundException e) {
5345             throw e.rethrowAsIllegalArgumentException();
5346         } finally {
5347             for (DatabaseHelper helper : transactions) {
5348                 helper.endTransaction();
5349             }
5350         }
5351     }
5352 
5353     private void appendWhereStandaloneMatch(@NonNull SQLiteQueryBuilder qb,
5354             @NonNull String column, /* @Match */ int match, Uri uri) {
5355         switch (match) {
5356             case MATCH_INCLUDE:
5357                 // No special filtering needed
5358                 break;
5359             case MATCH_EXCLUDE:
5360                 appendWhereStandalone(qb, getWhereClauseForMatchExclude(column));
5361                 break;
5362             case MATCH_ONLY:
5363                 appendWhereStandalone(qb, column + "=?", 1);
5364                 break;
5365             case MATCH_VISIBLE_FOR_FILEPATH:
5366                 final String whereClause =
5367                         getWhereClauseForMatchableVisibleFromFilePath(uri, column);
5368                 if (whereClause != null) {
5369                     appendWhereStandalone(qb, whereClause);
5370                 }
5371                 break;
5372             default:
5373                 throw new IllegalArgumentException();
5374         }
5375     }
5376 
5377     private static void appendWhereStandalone(@NonNull SQLiteQueryBuilder qb,
5378             @Nullable String selection, @Nullable Object... selectionArgs) {
5379         qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs));
5380     }
5381 
5382     private static void appendWhereStandaloneFilter(@NonNull SQLiteQueryBuilder qb,
5383             @NonNull String[] columns, @Nullable String filter) {
5384         if (TextUtils.isEmpty(filter)) return;
5385         for (String filterWord : filter.split("\\s+")) {
5386             appendWhereStandalone(qb, String.join("||", columns) + " LIKE ? ESCAPE '\\'",
5387                     "%" + DatabaseUtils.escapeForLike(Audio.keyFor(filterWord)) + "%");
5388         }
5389     }
5390 
5391     /**
5392      * Gets {@link LocalCallingIdentity} for the calling package
5393      * TODO(b/170465810) Change the method name after refactoring.
5394      */
5395     LocalCallingIdentity getCachedCallingIdentityForTranscoding(int uid) {
5396         return getCachedCallingIdentityForFuse(uid);
5397     }
5398 
5399     /**
5400      * Gets shared packages names for given {@code packageName}
5401      */
5402     private String[] getSharedPackagesForPackage(String packageName) {
5403         try {
5404             final int packageUid = getContext().getPackageManager()
5405                     .getPackageUid(packageName, 0);
5406             return getContext().getPackageManager().getPackagesForUid(packageUid);
5407         } catch (NameNotFoundException ignored) {
5408             return new String[] {packageName};
5409         }
5410     }
5411 
5412     private static final int TYPE_QUERY = 0;
5413     private static final int TYPE_INSERT = 1;
5414     private static final int TYPE_UPDATE = 2;
5415     private static final int TYPE_DELETE = 3;
5416 
5417     /**
5418      * Creating a new method for Transcoding to avoid any merge conflicts.
5419      * TODO(b/170465810): Remove this when getQueryBuilder code is refactored.
5420      */
5421     @NonNull SQLiteQueryBuilder getQueryBuilderForTranscoding(int type, int match,
5422             @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) {
5423         // Force MediaProvider calling identity when accessing the db from transcoding to avoid
5424         // generating 'strict' SQL e.g forcing owner_package_name matches
5425         // We already handle the required permission checks for the app before we get here
5426         final LocalCallingIdentity token = clearLocalCallingIdentity();
5427         try {
5428             return getQueryBuilder(type, match, uri, extras, honored);
5429         } finally {
5430             restoreLocalCallingIdentity(token);
5431         }
5432     }
5433 
5434     /**
5435      * Generate a {@link SQLiteQueryBuilder} that is filtered based on the
5436      * runtime permissions and/or {@link Uri} grants held by the caller.
5437      * <ul>
5438      * <li>If caller holds a {@link Uri} grant, access is allowed according to
5439      * that grant.
5440      * <li>If caller holds the write permission for a collection, they can
5441      * read/write all contents of that collection.
5442      * <li>If caller holds the read permission for a collection, they can read
5443      * all contents of that collection, but writes are limited to content they
5444      * own.
5445      * <li>If caller holds no permissions for a collection, all reads/write are
5446      * limited to content they own.
5447      * </ul>
5448      */
5449     private @NonNull SQLiteQueryBuilder getQueryBuilder(int type, int match,
5450             @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) {
5451         Trace.beginSection("MP.getQueryBuilder");
5452         try {
5453             return getQueryBuilderInternal(type, match, uri, extras, honored);
5454         } finally {
5455             Trace.endSection();
5456         }
5457     }
5458 
5459     private @NonNull SQLiteQueryBuilder getQueryBuilderInternal(int type, int match,
5460             @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) {
5461         final boolean forWrite;
5462         switch (type) {
5463             case TYPE_QUERY: forWrite = false; break;
5464             case TYPE_INSERT: forWrite = true; break;
5465             case TYPE_UPDATE: forWrite = true; break;
5466             case TYPE_DELETE: forWrite = true; break;
5467             default: throw new IllegalStateException();
5468         }
5469 
5470         if (forWrite) {
5471             final Uri redactedUri = extras.getParcelable(QUERY_ARG_REDACTED_URI);
5472             if (redactedUri != null) {
5473                 throw new UnsupportedOperationException(
5474                         "Writes on: " + redactedUri.toString() + " are not supported");
5475             }
5476         }
5477 
5478         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
5479         if (uri.getBooleanQueryParameter("distinct", false)) {
5480             qb.setDistinct(true);
5481         }
5482         qb.setStrict(true);
5483         if (isCallingPackageSelf()) {
5484             // When caller is system, such as the media scanner, we're willing
5485             // to let them access any columns they want
5486         } else {
5487             qb.setTargetSdkVersion(getCallingPackageTargetSdkVersion());
5488             qb.setStrictColumns(true);
5489             qb.setStrictGrammar(true);
5490         }
5491 
5492         // TODO: throw when requesting a currently unmounted volume
5493         final String volumeName = MediaStore.getVolumeName(uri);
5494         final String includeVolumes;
5495         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
5496             includeVolumes = bindList(mVolumeCache.getExternalVolumeNames().toArray());
5497         } else {
5498             includeVolumes = bindList(volumeName);
5499         }
5500 
5501         int matchPending = extras.getInt(QUERY_ARG_MATCH_PENDING, MATCH_DEFAULT);
5502         int matchTrashed = extras.getInt(QUERY_ARG_MATCH_TRASHED, MATCH_DEFAULT);
5503         int matchFavorite = extras.getInt(QUERY_ARG_MATCH_FAVORITE, MATCH_DEFAULT);
5504 
5505 
5506         // Handle callers using legacy arguments
5507         if (MediaStore.getIncludePending(uri)) matchPending = MATCH_INCLUDE;
5508 
5509         // Resolve any remaining default options
5510         final int defaultMatchForPendingAndTrashed;
5511         if (isFuseThread()) {
5512             // Write operations always check for file ownership, we don't need additional write
5513             // permission check for is_pending and is_trashed.
5514             defaultMatchForPendingAndTrashed =
5515                     forWrite ? MATCH_INCLUDE : MATCH_VISIBLE_FOR_FILEPATH;
5516         } else {
5517             defaultMatchForPendingAndTrashed = MATCH_EXCLUDE;
5518         }
5519         if (matchPending == MATCH_DEFAULT) matchPending = defaultMatchForPendingAndTrashed;
5520         if (matchTrashed == MATCH_DEFAULT) matchTrashed = defaultMatchForPendingAndTrashed;
5521         if (matchFavorite == MATCH_DEFAULT) matchFavorite = MATCH_INCLUDE;
5522 
5523         // Handle callers using legacy filtering
5524         final String filter = uri.getQueryParameter("filter");
5525 
5526         // Only accept ALL_VOLUMES parameter up until R, because we're not convinced we want
5527         // to commit to this as an API.
5528         final boolean includeAllVolumes = shouldIncludeRecentlyUnmountedVolumes(uri, extras);
5529 
5530         appendAccessCheckQuery(qb, forWrite, uri, match, extras, volumeName);
5531 
5532         switch (match) {
5533             case IMAGES_MEDIA_ID:
5534                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
5535                 matchPending = MATCH_INCLUDE;
5536                 matchTrashed = MATCH_INCLUDE;
5537                 // fall-through
5538             case IMAGES_MEDIA: {
5539                 if (type == TYPE_QUERY) {
5540                     qb.setTables("images");
5541                     qb.setProjectionMap(
5542                             getProjectionMap(Images.Media.class));
5543                 } else {
5544                     qb.setTables("files");
5545                     qb.setProjectionMap(
5546                             getProjectionMap(Images.Media.class, Files.FileColumns.class));
5547                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
5548                             FileColumns.MEDIA_TYPE_IMAGE);
5549                 }
5550                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
5551                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
5552                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
5553                 if (honored != null) {
5554                     honored.accept(QUERY_ARG_MATCH_PENDING);
5555                     honored.accept(QUERY_ARG_MATCH_TRASHED);
5556                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
5557                 }
5558                 if (!includeAllVolumes) {
5559                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
5560                 }
5561                 break;
5562             }
5563             case IMAGES_THUMBNAILS_ID:
5564                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
5565                 // fall-through
5566             case IMAGES_THUMBNAILS: {
5567                 qb.setTables("thumbnails");
5568 
5569                 final ArrayMap<String, String> projectionMap = new ArrayMap<>(
5570                         getProjectionMap(Images.Thumbnails.class));
5571                 projectionMap.put(Images.Thumbnails.THUMB_DATA,
5572                         "NULL AS " + Images.Thumbnails.THUMB_DATA);
5573                 qb.setProjectionMap(projectionMap);
5574 
5575                 break;
5576             }
5577             case AUDIO_MEDIA_ID:
5578                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
5579                 matchPending = MATCH_INCLUDE;
5580                 matchTrashed = MATCH_INCLUDE;
5581                 // fall-through
5582             case AUDIO_MEDIA: {
5583                 if (type == TYPE_QUERY) {
5584                     qb.setTables("audio");
5585                     qb.setProjectionMap(
5586                             getProjectionMap(Audio.Media.class));
5587                 } else {
5588                     qb.setTables("files");
5589                     qb.setProjectionMap(
5590                             getProjectionMap(Audio.Media.class, Files.FileColumns.class));
5591                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
5592                             FileColumns.MEDIA_TYPE_AUDIO);
5593                 }
5594                 appendWhereStandaloneFilter(qb, new String[] {
5595                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
5596                 }, filter);
5597                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
5598                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
5599                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
5600                 if (honored != null) {
5601                     honored.accept(QUERY_ARG_MATCH_PENDING);
5602                     honored.accept(QUERY_ARG_MATCH_TRASHED);
5603                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
5604                 }
5605                 if (!includeAllVolumes) {
5606                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
5607                 }
5608                 break;
5609             }
5610             case AUDIO_MEDIA_ID_GENRES_ID:
5611                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5));
5612                 // fall-through
5613             case AUDIO_MEDIA_ID_GENRES: {
5614                 if (type == TYPE_QUERY) {
5615                     qb.setTables("audio_genres");
5616                     qb.setProjectionMap(getProjectionMap(Audio.Genres.class));
5617                 } else {
5618                     throw new UnsupportedOperationException("Genres cannot be directly modified");
5619                 }
5620                 appendWhereStandalone(qb, "_id IN (SELECT genre_id FROM " +
5621                         "audio WHERE _id=?)", uri.getPathSegments().get(3));
5622                 break;
5623             }
5624             case AUDIO_GENRES_ID:
5625                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
5626                 // fall-through
5627             case AUDIO_GENRES: {
5628                 qb.setTables("audio_genres");
5629                 qb.setProjectionMap(getProjectionMap(Audio.Genres.class));
5630                 break;
5631             }
5632             case AUDIO_GENRES_ID_MEMBERS:
5633                 appendWhereStandalone(qb, "genre_id=?", uri.getPathSegments().get(3));
5634                 // fall-through
5635             case AUDIO_GENRES_ALL_MEMBERS: {
5636                 if (type == TYPE_QUERY) {
5637                     qb.setTables("audio");
5638 
5639                     final ArrayMap<String, String> projectionMap = new ArrayMap<>(
5640                             getProjectionMap(Audio.Genres.Members.class));
5641                     projectionMap.put(Audio.Genres.Members.AUDIO_ID,
5642                             "_id AS " + Audio.Genres.Members.AUDIO_ID);
5643                     qb.setProjectionMap(projectionMap);
5644                 } else {
5645                     throw new UnsupportedOperationException("Genres cannot be directly modified");
5646                 }
5647                 appendWhereStandaloneFilter(qb, new String[] {
5648                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
5649                 }, filter);
5650                 // In order to be consistent with other audio views like audio_artist, audio_albums,
5651                 // and audio_genres, exclude pending and trashed item
5652                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, MATCH_EXCLUDE, uri);
5653                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, MATCH_EXCLUDE, uri);
5654                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
5655                 if (honored != null) {
5656                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
5657                 }
5658                 if (!includeAllVolumes) {
5659                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
5660                 }
5661                 break;
5662             }
5663             case AUDIO_PLAYLISTS_ID:
5664                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
5665                 matchPending = MATCH_INCLUDE;
5666                 matchTrashed = MATCH_INCLUDE;
5667                 // fall-through
5668             case AUDIO_PLAYLISTS: {
5669                 if (type == TYPE_QUERY) {
5670                     qb.setTables("audio_playlists");
5671                     qb.setProjectionMap(
5672                             getProjectionMap(Audio.Playlists.class));
5673                 } else {
5674                     qb.setTables("files");
5675                     qb.setProjectionMap(
5676                             getProjectionMap(Audio.Playlists.class, Files.FileColumns.class));
5677                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
5678                             FileColumns.MEDIA_TYPE_PLAYLIST);
5679                 }
5680                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
5681                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
5682                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
5683                 if (honored != null) {
5684                     honored.accept(QUERY_ARG_MATCH_PENDING);
5685                     honored.accept(QUERY_ARG_MATCH_TRASHED);
5686                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
5687                 }
5688                 if (!includeAllVolumes) {
5689                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
5690                 }
5691                 break;
5692             }
5693             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
5694                 appendWhereStandalone(qb, "audio_playlists_map._id=?",
5695                         uri.getPathSegments().get(5));
5696                 // fall-through
5697             case AUDIO_PLAYLISTS_ID_MEMBERS: {
5698                 appendWhereStandalone(qb, "playlist_id=?", uri.getPathSegments().get(3));
5699                 if (type == TYPE_QUERY) {
5700                     qb.setTables("audio_playlists_map, audio");
5701 
5702                     final ArrayMap<String, String> projectionMap = new ArrayMap<>(
5703                             getProjectionMap(Audio.Playlists.Members.class));
5704                     projectionMap.put(Audio.Playlists.Members._ID,
5705                             "audio_playlists_map._id AS " + Audio.Playlists.Members._ID);
5706                     qb.setProjectionMap(projectionMap);
5707 
5708                     appendWhereStandalone(qb, "audio._id = audio_id");
5709                     // Since we use audio table along with audio_playlists_map
5710                     // for querying, we should only include database rows of
5711                     // the attached volumes.
5712                     if (!includeAllVolumes) {
5713                         appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN "
5714                              + includeVolumes);
5715                     }
5716                 } else {
5717                     qb.setTables("audio_playlists_map");
5718                     qb.setProjectionMap(getProjectionMap(Audio.Playlists.Members.class));
5719                 }
5720                 appendWhereStandaloneFilter(qb, new String[] {
5721                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
5722                 }, filter);
5723                 break;
5724             }
5725             case AUDIO_ALBUMART_ID:
5726                 appendWhereStandalone(qb, "album_id=?", uri.getPathSegments().get(3));
5727                 // fall-through
5728             case AUDIO_ALBUMART: {
5729                 qb.setTables("album_art");
5730 
5731                 final ArrayMap<String, String> projectionMap = new ArrayMap<>(
5732                         getProjectionMap(Audio.Thumbnails.class));
5733                 projectionMap.put(Audio.Thumbnails._ID,
5734                         "album_id AS " + Audio.Thumbnails._ID);
5735                 qb.setProjectionMap(projectionMap);
5736 
5737                 break;
5738             }
5739             case AUDIO_ARTISTS_ID_ALBUMS: {
5740                 if (type == TYPE_QUERY) {
5741                     qb.setTables("audio_artists_albums");
5742                     qb.setProjectionMap(getProjectionMap(Audio.Artists.Albums.class));
5743 
5744                     final String artistId = uri.getPathSegments().get(3);
5745                     appendWhereStandalone(qb, "artist_id=?", artistId);
5746                 } else {
5747                     throw new UnsupportedOperationException("Albums cannot be directly modified");
5748                 }
5749                 appendWhereStandaloneFilter(qb, new String[] {
5750                         AudioColumns.ALBUM_KEY
5751                 }, filter);
5752                 break;
5753             }
5754             case AUDIO_ARTISTS_ID:
5755                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
5756                 // fall-through
5757             case AUDIO_ARTISTS: {
5758                 if (type == TYPE_QUERY) {
5759                     qb.setTables("audio_artists");
5760                     qb.setProjectionMap(getProjectionMap(Audio.Artists.class));
5761                 } else {
5762                     throw new UnsupportedOperationException("Artists cannot be directly modified");
5763                 }
5764                 appendWhereStandaloneFilter(qb, new String[] {
5765                         AudioColumns.ARTIST_KEY
5766                 }, filter);
5767                 break;
5768             }
5769             case AUDIO_ALBUMS_ID:
5770                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
5771                 // fall-through
5772             case AUDIO_ALBUMS: {
5773                 if (type == TYPE_QUERY) {
5774                     qb.setTables("audio_albums");
5775                     qb.setProjectionMap(getProjectionMap(Audio.Albums.class));
5776                 } else {
5777                     throw new UnsupportedOperationException("Albums cannot be directly modified");
5778                 }
5779                 appendWhereStandaloneFilter(qb, new String[] {
5780                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY
5781                 }, filter);
5782                 break;
5783             }
5784             case VIDEO_MEDIA_ID:
5785                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
5786                 matchPending = MATCH_INCLUDE;
5787                 matchTrashed = MATCH_INCLUDE;
5788                 // fall-through
5789             case VIDEO_MEDIA: {
5790                 if (type == TYPE_QUERY) {
5791                     qb.setTables("video");
5792                     qb.setProjectionMap(
5793                             getProjectionMap(Video.Media.class));
5794                 } else {
5795                     qb.setTables("files");
5796                     qb.setProjectionMap(
5797                             getProjectionMap(Video.Media.class, Files.FileColumns.class));
5798                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
5799                             FileColumns.MEDIA_TYPE_VIDEO);
5800                 }
5801                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
5802                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
5803                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
5804                 if (honored != null) {
5805                     honored.accept(QUERY_ARG_MATCH_PENDING);
5806                     honored.accept(QUERY_ARG_MATCH_TRASHED);
5807                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
5808                 }
5809                 if (!includeAllVolumes) {
5810                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
5811                 }
5812                 break;
5813             }
5814             case VIDEO_THUMBNAILS_ID:
5815                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
5816                 // fall-through
5817             case VIDEO_THUMBNAILS: {
5818                 qb.setTables("videothumbnails");
5819                 qb.setProjectionMap(getProjectionMap(Video.Thumbnails.class));
5820                 break;
5821             }
5822             case FILES_ID:
5823                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2));
5824                 matchPending = MATCH_INCLUDE;
5825                 matchTrashed = MATCH_INCLUDE;
5826                 // fall-through
5827             case FILES: {
5828                 qb.setTables("files");
5829                 qb.setProjectionMap(getProjectionMap(Files.FileColumns.class));
5830 
5831                 appendWhereStandaloneFilter(qb, new String[] {
5832                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
5833                 }, filter);
5834                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
5835                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
5836                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
5837                 if (honored != null) {
5838                     honored.accept(QUERY_ARG_MATCH_PENDING);
5839                     honored.accept(QUERY_ARG_MATCH_TRASHED);
5840                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
5841                 }
5842                 if (!includeAllVolumes) {
5843                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
5844                 }
5845                 break;
5846             }
5847             case DOWNLOADS_ID:
5848                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2));
5849                 matchPending = MATCH_INCLUDE;
5850                 matchTrashed = MATCH_INCLUDE;
5851                 // fall-through
5852             case DOWNLOADS: {
5853                 if (type == TYPE_QUERY) {
5854                     qb.setTables("downloads");
5855                     qb.setProjectionMap(
5856                             getProjectionMap(Downloads.class));
5857                 } else {
5858                     qb.setTables("files");
5859                     qb.setProjectionMap(
5860                             getProjectionMap(Downloads.class, Files.FileColumns.class));
5861                     appendWhereStandalone(qb, FileColumns.IS_DOWNLOAD + "=1");
5862                 }
5863 
5864                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
5865                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
5866                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
5867                 if (honored != null) {
5868                     honored.accept(QUERY_ARG_MATCH_PENDING);
5869                     honored.accept(QUERY_ARG_MATCH_TRASHED);
5870                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
5871                 }
5872                 if (!includeAllVolumes) {
5873                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
5874                 }
5875                 break;
5876             }
5877             default:
5878                 throw new UnsupportedOperationException(
5879                         "Unknown or unsupported URL: " + uri.toString());
5880         }
5881 
5882         // To ensure we're enforcing our security model, all operations must
5883         // have a projection map configured
5884         if (qb.getProjectionMap() == null) {
5885             throw new IllegalStateException("All queries must have a projection map");
5886         }
5887 
5888         // If caller is an older app, we're willing to let through a
5889         // greylist of technically invalid columns
5890         if (getCallingPackageTargetSdkVersion() < Build.VERSION_CODES.Q) {
5891             qb.setProjectionGreylist(sGreylist);
5892         }
5893 
5894         // Starting U, if owner package name is used in query arguments,
5895         // we are restricting result set to only self-owned packages.
5896         if (shouldFilterOwnerPackageNameFlag() && shouldFilterByOwnerPackageName(extras, type)) {
5897             Log.d(TAG, "Restricting result set to only packages owned by calling package: "
5898                     + mCallingIdentity.get().getSharedPackagesAsString());
5899             final String ownerPackageMatchClause = getWhereForOwnerPackageMatch(
5900                     mCallingIdentity.get());
5901             appendWhereStandalone(qb, ownerPackageMatchClause);
5902         }
5903 
5904         return qb;
5905     }
5906 
5907     private boolean shouldFilterByOwnerPackageName(Bundle queryArgs, int type) {
5908         return type == TYPE_QUERY && SdkLevel.isAtLeastU() && containsOwnerPackageName(queryArgs)
5909                 && getContext().checkPermission(QUERY_ALL_PACKAGES, mCallingIdentity.get().pid,
5910                 mCallingIdentity.get().uid) == PERMISSION_DENIED;
5911     }
5912 
5913     private boolean containsOwnerPackageName(Bundle queryArgs) {
5914         final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION, "")
5915                 .toLowerCase(Locale.ROOT);
5916         final String groupBy = queryArgs.getString(QUERY_ARG_SQL_GROUP_BY, "")
5917                 .toLowerCase(Locale.ROOT);
5918         final String sort = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER, "")
5919                 .toLowerCase(Locale.ROOT);
5920         final String having = queryArgs.getString(QUERY_ARG_SQL_HAVING, "")
5921                 .toLowerCase(Locale.ROOT);
5922 
5923         return selection.contains(OWNER_PACKAGE_NAME) || groupBy.contains(OWNER_PACKAGE_NAME)
5924                 || sort.contains(OWNER_PACKAGE_NAME) || having.contains(OWNER_PACKAGE_NAME);
5925     }
5926 
5927     private void appendAccessCheckQuery(@NonNull SQLiteQueryBuilder qb, boolean forWrite,
5928             @NonNull Uri uri, int uriType, @NonNull Bundle extras, @NonNull String volumeName) {
5929         Objects.requireNonNull(extras);
5930         final Uri redactedUri = extras.getParcelable(QUERY_ARG_REDACTED_URI);
5931 
5932         final boolean allowGlobal;
5933         if (redactedUri != null) {
5934             allowGlobal = checkCallingPermissionGlobal(redactedUri, false);
5935         } else {
5936             allowGlobal = checkCallingPermissionGlobal(uri, forWrite);
5937         }
5938 
5939         if (allowGlobal) {
5940             return;
5941         }
5942 
5943         if (hasAccessToCollection(mCallingIdentity.get(), uriType, forWrite)) {
5944             // has direct access to whole collection, no special filtering needed.
5945             return;
5946         }
5947 
5948         final ArrayList<String> options = new ArrayList<>();
5949         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)
5950                 && hasUserSelectedAccess(mCallingIdentity.get(), uriType, forWrite)) {
5951             // If app has READ_MEDIA_VISUAL_USER_SELECTED permission, allow access on files granted
5952             // via PhotoPicker launched for Permission. These grants are defined in media_grants
5953             // table.
5954             // We exclude volume internal from the query because media_grants are not supported.
5955             options.add(getWhereForUserSelectedAccess(mCallingIdentity.get(), uriType));
5956         }
5957 
5958         // Allow access to files which are owned by the caller. Or allow access to files based on
5959         // legacy or any other special access permissions.
5960         options.add(getWhereForConstrainedAccess(mCallingIdentity.get(), uriType, forWrite,
5961                 extras));
5962 
5963         appendWhereStandalone(qb, TextUtils.join(" OR ", options));
5964     }
5965 
5966     /**
5967      * @return {@code true} if app requests to include database rows from
5968      * recently unmounted volume.
5969      * {@code false} otherwise.
5970      */
5971     private boolean shouldIncludeRecentlyUnmountedVolumes(Uri uri, Bundle extras) {
5972         if (isFuseThread()) {
5973             // File path requests don't require to query from unmounted volumes.
5974             return false;
5975         }
5976 
5977         boolean isIncludeVolumesChangeEnabled = SdkLevel.isAtLeastS() &&
5978                 CompatChanges.isChangeEnabled(ENABLE_INCLUDE_ALL_VOLUMES, Binder.getCallingUid());
5979         if ("1".equals(uri.getQueryParameter(ALL_VOLUMES))) {
5980             // Support uri parameter only in R OS and below. Apps should use
5981             // MediaStore#QUERY_ARG_RECENTLY_UNMOUNTED_VOLUMES on S OS onwards.
5982             if (!isIncludeVolumesChangeEnabled) {
5983                 return true;
5984             }
5985             throw new IllegalArgumentException("Unsupported uri parameter \"all_volumes\"");
5986         }
5987         if (isIncludeVolumesChangeEnabled) {
5988             // MediaStore#QUERY_ARG_INCLUDE_RECENTLY_UNMOUNTED_VOLUMES is only supported on S OS and
5989             // for app targeting targetSdk>=S.
5990             return extras.getBoolean(MediaStore.QUERY_ARG_INCLUDE_RECENTLY_UNMOUNTED_VOLUMES,
5991                     false);
5992         }
5993         return false;
5994     }
5995 
5996     /**
5997      * Determine if given {@link Uri} has a
5998      * {@link MediaColumns#OWNER_PACKAGE_NAME} column.
5999      */
6000     private boolean hasOwnerPackageName(Uri uri) {
6001         // It's easier to maintain this as an inverted list
6002         final int table = matchUri(uri, true);
6003         switch (table) {
6004             case IMAGES_THUMBNAILS_ID:
6005             case IMAGES_THUMBNAILS:
6006             case VIDEO_THUMBNAILS_ID:
6007             case VIDEO_THUMBNAILS:
6008             case AUDIO_ALBUMART:
6009             case AUDIO_ALBUMART_ID:
6010             case AUDIO_ALBUMART_FILE_ID:
6011                 return false;
6012             default:
6013                 return true;
6014         }
6015     }
6016 
6017     /**
6018      * @deprecated all operations should be routed through the overload that
6019      *             accepts a {@link Bundle} of extras.
6020      */
6021     @Override
6022     @Deprecated
6023     public int delete(Uri uri, String selection, String[] selectionArgs) {
6024         return delete(uri,
6025                 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null));
6026     }
6027 
6028     @Override
6029     public int delete(@NonNull Uri uri, @Nullable Bundle extras) {
6030         Trace.beginSection("MP.delete [" + uri + ']');
6031         try {
6032             return deleteInternal(uri, extras);
6033         } catch (FallbackException e) {
6034             return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion());
6035         } finally {
6036             Trace.endSection();
6037         }
6038     }
6039 
6040     private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras)
6041             throws FallbackException {
6042         final String volumeName = getVolumeName(uri);
6043         PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
6044 
6045         extras = (extras != null) ? extras : new Bundle();
6046         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
6047         extras.remove(QUERY_ARG_REDACTED_URI);
6048 
6049         if (isRedactedUri(uri)) {
6050             // we don't support deletion on redacted uris.
6051             return 0;
6052         }
6053 
6054         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
6055         extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
6056 
6057         uri = safeUncanonicalize(uri);
6058         final boolean allowHidden = isCallingPackageAllowedHidden();
6059         final int match = matchUri(uri, allowHidden);
6060 
6061         switch (match) {
6062             case AUDIO_MEDIA_ID:
6063             case AUDIO_PLAYLISTS_ID:
6064             case VIDEO_MEDIA_ID:
6065             case IMAGES_MEDIA_ID:
6066             case DOWNLOADS_ID:
6067             case FILES_ID: {
6068                 if (!isFuseThread() && getCachedCallingIdentityForFuse(Binder.getCallingUid()).
6069                         removeDeletedRowId(Long.parseLong(uri.getLastPathSegment()))) {
6070                     // Apps sometimes delete the file via filePath and then try to delete the db row
6071                     // using MediaProvider#delete. Since we would have already deleted the db row
6072                     // during the filePath operation, the latter will result in a security
6073                     // exception. Apps which don't expect an exception will break here. Since we
6074                     // have already deleted the db row, silently return zero as deleted count.
6075                     return 0;
6076                 }
6077             }
6078             break;
6079             default:
6080                 // For other match types, given uri will not correspond to a valid file.
6081                 break;
6082         }
6083 
6084         final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION);
6085         final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);
6086 
6087         int count = 0;
6088 
6089         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
6090 
6091         // handle MEDIA_SCANNER before calling getDatabaseForUri()
6092         if (match == MEDIA_SCANNER) {
6093             if (mMediaScannerVolume == null) {
6094                 return 0;
6095             }
6096 
6097             final DatabaseHelper helper = getDatabaseForUri(
6098                     MediaStore.Files.getContentUri(mMediaScannerVolume));
6099 
6100             helper.mScanStopTime = SystemClock.elapsedRealtime();
6101 
6102             mMediaScannerVolume = null;
6103             return 1;
6104         }
6105 
6106         if (match == VOLUMES_ID) {
6107             detachVolume(uri);
6108             count = 1;
6109         }
6110 
6111         final DatabaseHelper helper = getDatabaseForUri(uri);
6112         switch (match) {
6113             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
6114                 extras.putString(QUERY_ARG_SQL_SELECTION,
6115                         BaseColumns._ID + "=" + uri.getPathSegments().get(5));
6116                 // fall-through
6117             case AUDIO_PLAYLISTS_ID_MEMBERS: {
6118                 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
6119                 final Uri playlistUri = ContentUris.withAppendedId(
6120                         MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);
6121 
6122                 // Playlist contents are always persisted directly into playlist
6123                 // files on disk to ensure that we can reliably migrate between
6124                 // devices and recover from database corruption
6125                 int numOfRemovedPlaylistMembers = removePlaylistMembers(playlistUri, extras);
6126                 if (numOfRemovedPlaylistMembers > 0) {
6127                     acceptWithExpansion(helper::notifyDelete, volumeName, playlistId,
6128                             FileColumns.MEDIA_TYPE_PLAYLIST, false);
6129                 }
6130                 return numOfRemovedPlaylistMembers;
6131             }
6132         }
6133 
6134         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null);
6135 
6136         {
6137             // Give callers interacting with a specific media item a chance to
6138             // escalate access if they don't already have it
6139             switch (match) {
6140                 case AUDIO_MEDIA_ID:
6141                 case VIDEO_MEDIA_ID:
6142                 case IMAGES_MEDIA_ID:
6143                     enforceCallingPermission(uri, extras, true);
6144             }
6145 
6146             final String[] projection = new String[] {
6147                     FileColumns.MEDIA_TYPE,
6148                     FileColumns.DATA,
6149                     FileColumns._ID,
6150                     FileColumns.IS_DOWNLOAD,
6151                     FileColumns.MIME_TYPE,
6152             };
6153             final boolean isFilesTable = qb.getTables().equals("files");
6154             final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>();
6155             final int[] countPerMediaType = new int[FileColumns.MEDIA_TYPE_COUNT];
6156             if (isFilesTable) {
6157                 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
6158                 if (deleteparam == null || ! deleteparam.equals("false")) {
6159                     Cursor c = qb.query(helper, projection, userWhere, userWhereArgs,
6160                             null, null, null, null, null);
6161                     try {
6162                         while (c.moveToNext()) {
6163                             final int mediaType = c.getInt(0);
6164                             final String data = c.getString(1);
6165                             final long id = c.getLong(2);
6166                             final int isDownload = c.getInt(3);
6167                             final String mimeType = c.getString(4);
6168 
6169                             // TODO(b/188782594) Consider logging mime type access on delete too.
6170 
6171                             // Forget that caller is owner of this item
6172                             mCallingIdentity.get().setOwned(id, false);
6173 
6174                             deleteIfAllowed(uri, extras, data);
6175                             int res = qb.delete(helper, BaseColumns._ID + "=" + id, null);
6176                             count += res;
6177                             // Avoid ArrayIndexOutOfBounds if more mediaTypes are added,
6178                             // but mediaTypeSize is not updated
6179                             if (res > 0 && mediaType < countPerMediaType.length) {
6180                                 countPerMediaType[mediaType] += res;
6181                             }
6182 
6183                             if (isDownload == 1) {
6184                                 deletedDownloadIds.put(id, mimeType);
6185                             }
6186                         }
6187                     } finally {
6188                         FileUtils.closeQuietly(c);
6189                     }
6190                     // Do not allow deletion if the file/object is referenced as parent
6191                     // by some other entries. It could cause database corruption.
6192                     appendWhereStandalone(qb, ID_NOT_PARENT_CLAUSE);
6193                 }
6194             }
6195 
6196             switch (match) {
6197                 case AUDIO_GENRES_ID_MEMBERS:
6198                     throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
6199 
6200                 case IMAGES_THUMBNAILS_ID:
6201                 case IMAGES_THUMBNAILS:
6202                 case VIDEO_THUMBNAILS_ID:
6203                 case VIDEO_THUMBNAILS:
6204                     // Delete the referenced files first.
6205                     Cursor c = qb.query(helper, sDataOnlyColumn, userWhere, userWhereArgs, null,
6206                             null, null, null, null);
6207                     if (c != null) {
6208                         try {
6209                             while (c.moveToNext()) {
6210                                 deleteIfAllowed(uri, extras, c.getString(0));
6211                             }
6212                         } finally {
6213                             FileUtils.closeQuietly(c);
6214                         }
6215                     }
6216                     count += deleteRecursive(qb, helper, userWhere, userWhereArgs);
6217                     break;
6218 
6219                 default:
6220                     count += deleteRecursive(qb, helper, userWhere, userWhereArgs);
6221                     break;
6222             }
6223 
6224             if (deletedDownloadIds.size() > 0) {
6225                 notifyDownloadManagerOnDelete(helper, deletedDownloadIds);
6226             }
6227 
6228             // Check for other URI format grants for File API call only. Check right before
6229             // returning count = 0, to leave positive cases performance unaffected.
6230             if (count == 0 && isFuseThread()) {
6231                 count += deleteWithOtherUriGrants(uri, helper, projection, userWhere, userWhereArgs,
6232                         extras);
6233             }
6234 
6235             if (isFilesTable && !isCallingPackageSelf()) {
6236                 Metrics.logDeletion(volumeName, mCallingIdentity.get().uid,
6237                         getCallingPackageOrSelf(), count, countPerMediaType);
6238             }
6239         }
6240 
6241         return count;
6242     }
6243 
6244     private int deleteWithOtherUriGrants(@NonNull Uri uri, DatabaseHelper helper,
6245             String[] projection, String userWhere, String[] userWhereArgs,
6246             @Nullable Bundle extras) {
6247         try (Cursor c = queryForSingleItemAsMediaProvider(uri, projection, userWhere, userWhereArgs,
6248                     null)) {
6249             final int mediaType = c.getInt(0);
6250             final String data = c.getString(1);
6251             final long id = c.getLong(2);
6252             final int isDownload = c.getInt(3);
6253             final String mimeType = c.getString(4);
6254 
6255             final Uri uriGranted = getOtherUriGrantsForPath(data, mediaType, Long.toString(id),
6256                     /* forWrite */ true);
6257             if (uriGranted != null) {
6258                 // 1. delete file
6259                 deleteIfAllowed(uriGranted, extras, data);
6260                 // 2. delete file row from the db
6261                 final boolean allowHidden = isCallingPackageAllowedHidden();
6262                 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE,
6263                         matchUri(uriGranted, allowHidden), uriGranted, extras, null);
6264                 int count = qb.delete(helper, BaseColumns._ID + "=" + id, null);
6265 
6266                 if (isDownload == 1) {
6267                     final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>();
6268                     deletedDownloadIds.put(id, mimeType);
6269                     notifyDownloadManagerOnDelete(helper, deletedDownloadIds);
6270                 }
6271                 return count;
6272             }
6273         } catch (FileNotFoundException ignored) {
6274             // Do nothing. Returns 0 files deleted.
6275         }
6276         return 0;
6277     }
6278 
6279     private void notifyDownloadManagerOnDelete(DatabaseHelper helper,
6280             LongSparseArray<String> deletedDownloadIds) {
6281         // Do this on a background thread, since we don't want to make binder
6282         // calls as part of a FUSE call.
6283         helper.postBackground(() -> {
6284             DownloadManager dm = getContext().getSystemService(DownloadManager.class);
6285             if (dm != null) {
6286                 dm.onMediaStoreDownloadsDeleted(deletedDownloadIds);
6287             }
6288         });
6289     }
6290 
6291     /**
6292      * Executes identical delete repeatedly within a single transaction until
6293      * stability is reached. Combined with {@link #ID_NOT_PARENT_CLAUSE}, this
6294      * can be used to recursively delete all matching entries, since it only
6295      * deletes parents when no references remaining.
6296      */
6297     private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere,
6298             String[] userWhereArgs) {
6299         return helper.runWithTransaction((db) -> {
6300             synchronized (mDirectoryCache) {
6301                 mDirectoryCache.clear();
6302             }
6303 
6304             int n = 0;
6305             int total = 0;
6306             do {
6307                 n = qb.delete(helper, userWhere, userWhereArgs);
6308                 total += n;
6309             } while (n > 0);
6310             return total;
6311         });
6312     }
6313 
6314     @Nullable
6315     @VisibleForTesting
6316     Uri getRedactedUri(@NonNull Uri uri) {
6317         if (!isUriSupportedForRedaction(uri)) {
6318             return null;
6319         }
6320 
6321         DatabaseHelper helper;
6322         try {
6323             helper = getDatabaseForUri(uri);
6324         } catch (VolumeNotFoundException e) {
6325             throw e.rethrowAsIllegalArgumentException();
6326         }
6327 
6328         try (final Cursor c = helper.runWithoutTransaction(
6329                 (db) -> db.query("files",
6330                         new String[]{FileColumns.REDACTED_URI_ID}, FileColumns._ID + "=?",
6331                         new String[]{uri.getLastPathSegment()}, null, null, null))) {
6332             // Database entry for uri not found.
6333             if (!c.moveToFirst()) return null;
6334 
6335             String redactedUriID = c.getString(c.getColumnIndex(FileColumns.REDACTED_URI_ID));
6336             if (redactedUriID == null) {
6337                 // No redacted has even been created for this uri. Create a new redacted URI ID for
6338                 // the uri and store it in the DB.
6339                 redactedUriID = REDACTED_URI_ID_PREFIX + UUID.randomUUID().toString().replace("-",
6340                         "");
6341 
6342                 ContentValues cv = new ContentValues();
6343                 cv.put(FileColumns.REDACTED_URI_ID, redactedUriID);
6344                 int rowsAffected = helper.runWithTransaction(
6345                         (db) -> db.update("files", cv, FileColumns._ID + "=?",
6346                                 new String[]{uri.getLastPathSegment()}));
6347                 if (rowsAffected == 0) {
6348                     // this shouldn't happen ideally, only reason this might happen is if the db
6349                     // entry got deleted in b/w in which case we should return null.
6350                     return null;
6351                 }
6352             }
6353 
6354             // Create and return a uri with ID = redactedUriID.
6355             final Uri.Builder builder = ContentUris.removeId(uri).buildUpon();
6356             builder.appendPath(redactedUriID);
6357 
6358             return builder.build();
6359         }
6360     }
6361 
6362     @NonNull
6363     @VisibleForTesting
6364     List<Uri> getRedactedUri(@NonNull List<Uri> uris) {
6365         ArrayList<Uri> redactedUris = new ArrayList<>();
6366         for (Uri uri : uris) {
6367             redactedUris.add(getRedactedUri(uri));
6368         }
6369 
6370         return redactedUris;
6371     }
6372 
6373     @Override
6374     public Bundle call(String method, String arg, Bundle extras) {
6375         Trace.beginSection("MP.call [" + method + ']');
6376         try {
6377             return callInternal(method, arg, extras);
6378         } finally {
6379             Trace.endSection();
6380         }
6381     }
6382 
6383     private Bundle callInternal(String method, String arg, Bundle extras) {
6384         switch (method) {
6385             case MediaStore.RESOLVE_PLAYLIST_MEMBERS_CALL: {
6386                 final LocalCallingIdentity token = clearLocalCallingIdentity();
6387                 final CallingIdentity providerToken = clearCallingIdentity();
6388                 try {
6389                     final Uri playlistUri = extras.getParcelable(MediaStore.EXTRA_URI);
6390                     resolvePlaylistMembers(playlistUri);
6391                 } finally {
6392                     restoreCallingIdentity(providerToken);
6393                     restoreLocalCallingIdentity(token);
6394                 }
6395                 return null;
6396             }
6397             case MediaStore.RUN_IDLE_MAINTENANCE_CALL: {
6398                 // Protect ourselves from random apps by requiring a generic
6399                 // permission held by common debugging components, such as shell
6400                 getContext().enforceCallingOrSelfPermission(
6401                         android.Manifest.permission.DUMP, TAG);
6402                 final LocalCallingIdentity token = clearLocalCallingIdentity();
6403                 final CallingIdentity providerToken = clearCallingIdentity();
6404                 try {
6405                     onIdleMaintenance(new CancellationSignal());
6406                 } finally {
6407                     restoreCallingIdentity(providerToken);
6408                     restoreLocalCallingIdentity(token);
6409                 }
6410                 return null;
6411             }
6412             case MediaStore.WAIT_FOR_IDLE_CALL: {
6413                 // TODO(b/195009139): Remove after overriding wait for idle in test to sync picker
6414                 // Syncing the picker while waiting for idle fixes tests with the picker db
6415                 // flag enabled because the picker db is in a consistent state with the external
6416                 // db after the sync
6417                 syncAllMedia();
6418                 ForegroundThread.waitForIdle();
6419                 final CountDownLatch latch = new CountDownLatch(1);
6420                 BackgroundThread.getExecutor().execute(latch::countDown);
6421                 try {
6422                     latch.await(30, TimeUnit.SECONDS);
6423                 } catch (InterruptedException e) {
6424                     throw new IllegalStateException(e);
6425                 }
6426                 return null;
6427             }
6428             case MediaStore.SCAN_FILE_CALL: {
6429                 final LocalCallingIdentity token = clearLocalCallingIdentity();
6430                 final CallingIdentity providerToken = clearCallingIdentity();
6431 
6432                 final String filePath = arg;
6433                 final Uri uri;
6434                 try {
6435                     File file;
6436                     try {
6437                         file = FileUtils.getCanonicalFile(filePath);
6438                     } catch (IOException e) {
6439                         file = null;
6440                     }
6441 
6442                     uri = file != null ? scanFile(file, REASON_DEMAND) : null;
6443                 } finally {
6444                     restoreCallingIdentity(providerToken);
6445                     restoreLocalCallingIdentity(token);
6446                 }
6447 
6448                 // TODO(b/262244882): maybe enforceCallingPermissionInternal(uri, ...)
6449 
6450                 final Bundle res = new Bundle();
6451                 res.putParcelable(Intent.EXTRA_STREAM, uri);
6452                 return res;
6453             }
6454             case MediaStore.SCAN_VOLUME_CALL: {
6455                 final int userId = uidToUserId(Binder.getCallingUid());
6456                 final LocalCallingIdentity token = clearLocalCallingIdentity();
6457                 final CallingIdentity providerToken = clearCallingIdentity();
6458 
6459                 final String volumeName = arg;
6460                 try {
6461                     final MediaVolume volume = mVolumeCache.findVolume(volumeName,
6462                             UserHandle.of(userId));
6463                     MediaService.onScanVolume(getContext(), volume, REASON_DEMAND);
6464                 } catch (FileNotFoundException e) {
6465                     Log.w(TAG, "Failed to find volume " + volumeName, e);
6466                 } catch (IOException e) {
6467                     throw new RuntimeException(e);
6468                 } finally {
6469                     restoreCallingIdentity(providerToken);
6470                     restoreLocalCallingIdentity(token);
6471                 }
6472                 return Bundle.EMPTY;
6473             }
6474             case MediaStore.GET_VERSION_CALL: {
6475                 final String volumeName = extras.getString(Intent.EXTRA_TEXT);
6476 
6477                 final DatabaseHelper helper;
6478                 try {
6479                     helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName));
6480                 } catch (VolumeNotFoundException e) {
6481                     throw e.rethrowAsIllegalArgumentException();
6482                 }
6483 
6484                 final String version = helper.runWithoutTransaction((db) ->
6485                         db.getVersion() + ":" + DatabaseHelper.getOrCreateUuid(db));
6486 
6487                 final Bundle res = new Bundle();
6488                 res.putString(Intent.EXTRA_TEXT, version);
6489                 return res;
6490             }
6491             case MediaStore.GET_GENERATION_CALL: {
6492                 final String volumeName = extras.getString(Intent.EXTRA_TEXT);
6493 
6494                 final DatabaseHelper helper;
6495                 try {
6496                     helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName));
6497                 } catch (VolumeNotFoundException e) {
6498                     throw e.rethrowAsIllegalArgumentException();
6499                 }
6500 
6501                 final long generation = helper.runWithoutTransaction(DatabaseHelper::getGeneration);
6502 
6503                 final Bundle res = new Bundle();
6504                 res.putLong(Intent.EXTRA_INDEX, generation);
6505                 return res;
6506             }
6507             case MediaStore.GET_DOCUMENT_URI_CALL: {
6508                 final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI);
6509                 enforceCallingPermission(mediaUri, extras, false);
6510 
6511                 final Uri fileUri;
6512                 final LocalCallingIdentity token = clearLocalCallingIdentity();
6513                 try {
6514                     fileUri = Uri.fromFile(queryForDataFile(mediaUri, null));
6515                 } catch (FileNotFoundException e) {
6516                     throw new IllegalArgumentException(e);
6517                 } finally {
6518                     restoreLocalCallingIdentity(token);
6519                 }
6520 
6521                 try (ContentProviderClient client = getContext().getContentResolver()
6522                         .acquireUnstableContentProviderClient(
6523                                 getExternalStorageProviderAuthority())) {
6524                     extras.putParcelable(MediaStore.EXTRA_URI, fileUri);
6525                     return client.call(method, null, extras);
6526                 } catch (RemoteException e) {
6527                     throw new IllegalStateException(e);
6528                 }
6529             }
6530             case MediaStore.GET_MEDIA_URI_CALL: {
6531                 final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI);
6532                 getContext().enforceCallingUriPermission(documentUri,
6533                         Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG);
6534 
6535                 final int callingPid = mCallingIdentity.get().pid;
6536                 final int callingUid = mCallingIdentity.get().uid;
6537                 final String callingPackage = getCallingPackage();
6538                 final CallingIdentity token = clearCallingIdentity();
6539                 final String authority = documentUri.getAuthority();
6540 
6541                 if (!authority.equals(MediaDocumentsProvider.AUTHORITY) &&
6542                         !authority.equals(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
6543                     throw new IllegalArgumentException("Provider for this Uri is not supported.");
6544                 }
6545 
6546                 try (ContentProviderClient client = getContext().getContentResolver()
6547                         .acquireUnstableContentProviderClient(authority)) {
6548                     final Bundle clientRes = client.call(method, null, extras);
6549                     final Uri fileUri = clientRes.getParcelable(MediaStore.EXTRA_URI);
6550                     final Bundle res = new Bundle();
6551                     final Uri mediaStoreUri = fileUri.getAuthority().equals(MediaStore.AUTHORITY) ?
6552                             fileUri : queryForMediaUri(new File(fileUri.getPath()), null);
6553                     copyUriPermissionGrants(documentUri, mediaStoreUri, callingPid,
6554                             callingUid, callingPackage);
6555                     res.putParcelable(MediaStore.EXTRA_URI, mediaStoreUri);
6556                     return res;
6557                 } catch (FileNotFoundException e) {
6558                     throw new IllegalArgumentException(e);
6559                 } catch (RemoteException e) {
6560                     throw new IllegalStateException(e);
6561                 } finally {
6562                     restoreCallingIdentity(token);
6563                 }
6564             }
6565             case MediaStore.GET_REDACTED_MEDIA_URI_CALL: {
6566                 final Uri uri = extras.getParcelable(MediaStore.EXTRA_URI);
6567                 // NOTE: It is ok to update the DB and return a redacted URI for the cases when
6568                 // the user code only has read access, hence we don't check for write permission.
6569                 enforceCallingPermission(uri, Bundle.EMPTY, false);
6570                 final LocalCallingIdentity token = clearLocalCallingIdentity();
6571                 try {
6572                     final Bundle res = new Bundle();
6573                     res.putParcelable(MediaStore.EXTRA_URI, getRedactedUri(uri));
6574                     return res;
6575                 } finally {
6576                     restoreLocalCallingIdentity(token);
6577                 }
6578             }
6579             case MediaStore.GET_REDACTED_MEDIA_URI_LIST_CALL: {
6580                 final List<Uri> uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST);
6581                 // NOTE: It is ok to update the DB and return a redacted URI for the cases when
6582                 // the user code only has read access, hence we don't check for write permission.
6583                 enforceCallingPermission(uris, false);
6584                 final LocalCallingIdentity token = clearLocalCallingIdentity();
6585                 try {
6586                     final Bundle res = new Bundle();
6587                     res.putParcelableArrayList(MediaStore.EXTRA_URI_LIST,
6588                             (ArrayList<? extends Parcelable>) getRedactedUri(uris));
6589                     return res;
6590                 } finally {
6591                     restoreLocalCallingIdentity(token);
6592                 }
6593             }
6594             case MediaStore.GRANT_MEDIA_READ_FOR_PACKAGE_CALL: {
6595                 final int caller = Binder.getCallingUid();
6596                 int userId;
6597                 final List<Uri> uris;
6598                 String packageName;
6599                 if (checkPermissionSelf(caller)) {
6600                     // If the caller is MediaProvider the accepted parameters are EXTRA_URI_LIST
6601                     // and EXTRA_UID.
6602                     if (!extras.containsKey(
6603                             MediaStore.EXTRA_URI_LIST)
6604                                     && !extras.containsKey(Intent.EXTRA_UID)) {
6605                         throw new IllegalArgumentException(
6606                                 "Missing required extras arguments: EXTRA_URI_LIST or"
6607                                     + " EXTRA_UID");
6608                     }
6609                     uris = extras.getParcelableArrayList(MediaStore.EXTRA_URI_LIST);
6610                     final PackageManager pm = getContext().getPackageManager();
6611                     final int packageUid = extras.getInt(Intent.EXTRA_UID);
6612                     packageName = pm.getNameForUid(packageUid);
6613                     // Get the userId from packageUid as the initiator could be a cloned app, which
6614                     // accesses Media via MP of its parent user and Binder's callingUid reflects
6615                     // the latter.
6616                     userId = uidToUserId(packageUid);
6617                     if (packageName.contains(":")) {
6618                         // Check if the package name includes the package uid. This is expected
6619                         // for packages that are referencing a shared user. PackageManager will
6620                         // return a string such as <packagename>:<uid> in this instance.
6621                         packageName = packageName.split(":")[0];
6622                     }
6623                 } else if (checkPermissionShell(caller)) {
6624                     // If the caller is the shell, the accepted parameters are EXTRA_URI (as string)
6625                     // and EXTRA_PACKAGE_NAME (as string).
6626                     if (!extras.containsKey(MediaStore.EXTRA_URI)
6627                                     && !extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) {
6628                         throw new IllegalArgumentException(
6629                                 "Missing required extras arguments: EXTRA_URI or"
6630                                     + " EXTRA_PACKAGE_NAME");
6631                     }
6632                     packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME);
6633                     uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI)));
6634                     userId = uidToUserId(caller);
6635                 } else {
6636                     // All other callers are unauthorized.
6637                     throw new SecurityException("Create media grants not allowed. "
6638                                 + " Calling app ID:" + UserHandle.getAppId(Binder.getCallingUid())
6639                                 + " Calling UID:" + Binder.getCallingUid()
6640                                 + " Media Provider app ID:" + UserHandle.getAppId(MY_UID)
6641                                 + " Media Provider UID:" + MY_UID);
6642                 }
6643 
6644                 mMediaGrants.addMediaGrantsForPackage(packageName, uris, userId);
6645                 return null;
6646             }
6647             case MediaStore.CREATE_WRITE_REQUEST_CALL:
6648             case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
6649             case MediaStore.CREATE_TRASH_REQUEST_CALL:
6650             case MediaStore.CREATE_DELETE_REQUEST_CALL: {
6651                 final PendingIntent pi = createRequest(method, extras);
6652                 final Bundle res = new Bundle();
6653                 res.putParcelable(MediaStore.EXTRA_RESULT, pi);
6654                 return res;
6655             }
6656             case MediaStore.IS_SYSTEM_GALLERY_CALL:
6657                 final LocalCallingIdentity token = clearLocalCallingIdentity();
6658                 try {
6659                     String packageName = arg;
6660                     int uid = extras.getInt(MediaStore.EXTRA_IS_SYSTEM_GALLERY_UID);
6661                     boolean isSystemGallery = PermissionUtils.checkWriteImagesOrVideoAppOps(
6662                             getContext(), uid, packageName, getContext().getAttributionTag());
6663                     Bundle res = new Bundle();
6664                     res.putBoolean(MediaStore.EXTRA_IS_SYSTEM_GALLERY_RESPONSE, isSystemGallery);
6665                     return res;
6666                 } finally {
6667                     restoreLocalCallingIdentity(token);
6668                 }
6669             case MediaStore.GET_CLOUD_PROVIDER_CALL: {
6670                 // TODO(b/245746037): replace UID check with Permission(MANAGE_CLOUD_MEDIA_PROVIDER)
6671                 // PhotoPickerSettingsActivity will run as either the primary or the managed user.
6672                 // Since the activity shows both personal and work tabs, it will have to make get
6673                 // cloud provider IPC call to both instances of Media Provider - one running as
6674                 // primary profile and the other as managed profile. Hence, UID check will not be
6675                 // feasible here.
6676                 if (!checkPermissionSelf(Binder.getCallingUid())) {
6677                     throw new SecurityException("Get cloud provider not allowed. "
6678                             + " Calling app ID:" + UserHandle.getAppId(Binder.getCallingUid())
6679                             + " Calling UID:" + Binder.getCallingUid()
6680                             + " Media Provider app ID:" + UserHandle.getAppId(MY_UID)
6681                             + " Media Provider UID:" + MY_UID);
6682                 }
6683                 final Bundle bundle = new Bundle();
6684                 bundle.putString(MediaStore.GET_CLOUD_PROVIDER_RESULT,
6685                         mPickerSyncController.getCloudProvider());
6686                 return bundle;
6687             }
6688             case MediaStore.SET_CLOUD_PROVIDER_CALL: {
6689                 // TODO(b/267327327): Add permission check before updating cloud provider. Also
6690                 //  validate the new cloud provider before setting it by using
6691                 //  PickerSyncController#setCloudProvider instead of
6692                 //  PickerSyncController#forceSetCloudProvider.
6693                 final String cloudProvider = extras.getString(MediaStore.EXTRA_CLOUD_PROVIDER);
6694                 Log.i(TAG, "Request received to set cloud provider to " + cloudProvider);
6695                 mPickerSyncController.forceSetCloudProvider(cloudProvider);
6696                 Log.i(TAG, "Completed request to set cloud provider to " + cloudProvider);
6697 
6698                 // Cannot start sync here yet because currently sync and other picker related
6699                 // queries like SET_CLOUD_PROVIDER_CALL and GET_CLOUD_PROVIDER use the same lock.
6700                 // If we start sync here and then user tries to return to the Picker or change the
6701                 // provider again, Picker will ANR and crash.
6702                 return new Bundle();
6703             }
6704             case MediaStore.SYNC_PROVIDERS_CALL: {
6705                 syncAllMedia();
6706                 return new Bundle();
6707             }
6708             case MediaStore.IS_SUPPORTED_CLOUD_PROVIDER_CALL: {
6709                 final boolean isSupported = mPickerSyncController.isProviderSupported(arg,
6710                         Binder.getCallingUid());
6711 
6712                 Bundle bundle = new Bundle();
6713                 bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isSupported);
6714                 return bundle;
6715             }
6716             case MediaStore.IS_CURRENT_CLOUD_PROVIDER_CALL: {
6717                 final boolean isEnabled = mPickerSyncController.isProviderEnabled(arg,
6718                         Binder.getCallingUid());
6719 
6720                 Bundle bundle = new Bundle();
6721                 bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isEnabled);
6722                 return bundle;
6723             }
6724             case MediaStore.NOTIFY_CLOUD_MEDIA_CHANGED_EVENT_CALL: {
6725                 final boolean notifyCloudEventResult;
6726                 if (mPickerSyncController.isProviderEnabled(arg, Binder.getCallingUid())) {
6727                     mPickerSyncController.notifyMediaEvent();
6728                     notifyCloudEventResult = true;
6729                 } else {
6730                     notifyCloudEventResult = false;
6731                 }
6732 
6733                 Bundle bundle = new Bundle();
6734                 bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT,
6735                         notifyCloudEventResult);
6736                 return bundle;
6737             }
6738             case MediaStore.USES_FUSE_PASSTHROUGH: {
6739                 boolean isEnabled = false;
6740                 try {
6741                     FuseDaemon daemon = getFuseDaemonForFile(new File(arg), mVolumeCache);
6742                     if (daemon != null) {
6743                         isEnabled = daemon.usesFusePassthrough();
6744                     }
6745                 } catch (FileNotFoundException e) {
6746                 }
6747 
6748                 Bundle bundle = new Bundle();
6749                 bundle.putBoolean(MediaStore.USES_FUSE_PASSTHROUGH_RESULT, isEnabled);
6750                 return bundle;
6751             }
6752             case MediaStore.RUN_IDLE_MAINTENANCE_FOR_STABLE_URIS: {
6753                 backupDatabases(null);
6754                 return new Bundle();
6755             }
6756             case MediaStore.READ_BACKED_UP_FILE_PATHS: {
6757                 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
6758                         "Permission missing to call READ_BACKED_UP_FILE_PATHS by "
6759                                 + "uid:" + Binder.getCallingUid());
6760                 List<String> cumulatedValues = new ArrayList<String>();
6761                 String[] backedUpFilePaths;
6762                 String lastReadValue = "";
6763                 while (true) {
6764                     backedUpFilePaths = mDatabaseBackupAndRecovery.readBackedUpFilePaths(arg,
6765                             lastReadValue, LEVEL_DB_READ_LIMIT);
6766                     if (backedUpFilePaths.length <= 0) {
6767                         break;
6768                     }
6769                     cumulatedValues.addAll(Arrays.asList(backedUpFilePaths));
6770                     if (backedUpFilePaths.length < LEVEL_DB_READ_LIMIT) {
6771                         break;
6772                     }
6773                     lastReadValue = backedUpFilePaths[backedUpFilePaths.length - 1];
6774                 }
6775 
6776                 Bundle bundle = new Bundle();
6777                 Object[] values =  cumulatedValues.toArray();
6778                 String[] resultArray =  Arrays.copyOf(values, values.length, String[].class);
6779                 bundle.putStringArray(READ_BACKED_UP_FILE_PATHS, resultArray);
6780                 return bundle;
6781             }
6782             case MediaStore.DELETE_BACKED_UP_FILE_PATHS:
6783                 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
6784                         "Permission missing to call DELETE_BACKED_UP_FILE_PATHS by "
6785                                 + "uid:" + Binder.getCallingUid());
6786                 mDatabaseBackupAndRecovery.deleteBackupForVolume(arg);
6787                 return new Bundle();
6788             case MediaStore.GET_BACKUP_FILES:
6789                 getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
6790                         "Permission missing to call GET_BACKUP_FILES by "
6791                                 + "uid:" + Binder.getCallingUid());
6792                 List<File> backupFiles = mDatabaseBackupAndRecovery.getBackupFiles();
6793                 List<String> fileNames = new ArrayList<>();
6794                 for (File file : backupFiles) {
6795                     fileNames.add(file.getName());
6796                 }
6797                 Bundle bundle = new Bundle();
6798                 Object[] values = fileNames.toArray();
6799                 String[] resultArray = Arrays.copyOf(values, values.length, String[].class);
6800                 bundle.putStringArray(GET_BACKUP_FILES, resultArray);
6801                 return bundle;
6802             default:
6803                 throw new UnsupportedOperationException("Unsupported call: " + method);
6804         }
6805     }
6806 
6807     public void backupDatabases(CancellationSignal signal) {
6808         mDatabaseBackupAndRecovery.backupDatabases(mInternalDatabase, mExternalDatabase, signal);
6809     }
6810 
6811     private void syncAllMedia() {
6812         // Clear the binder calling identity so that we can sync the unexported
6813         // local_provider while running as MediaProvider
6814         final long t = Binder.clearCallingIdentity();
6815         try {
6816             Log.v(TAG, "Test initiated cloud provider sync");
6817             mPickerSyncController.syncAllMedia();
6818         } finally {
6819             Binder.restoreCallingIdentity(t);
6820         }
6821     }
6822 
6823     private AssetFileDescriptor getOriginalMediaFormatFileDescriptor(Bundle extras)
6824             throws FileNotFoundException {
6825         try (ParcelFileDescriptor inputPfd =
6826                 extras.getParcelable(MediaStore.EXTRA_FILE_DESCRIPTOR)) {
6827             File file = getFileFromFileDescriptor(inputPfd);
6828             // Convert from FUSE file to lower fs file because the supportsTranscode() check below
6829             // expects a lower fs file format
6830             file = fromFuseFile(file);
6831             if (!mTranscodeHelper.supportsTranscode(file.getPath())) {
6832                 // Note that we should be checking if a file is a modern format and not just
6833                 // that it supports transcoding, unfortunately, checking modern format
6834                 // requires either a db query or media scan which can lead to ANRs if apps
6835                 // or the system implicitly call this method as part of a
6836                 // MediaPlayer#setDataSource.
6837                 throw new FileNotFoundException("Input file descriptor is already original");
6838             }
6839 
6840             FuseDaemon fuseDaemon = getFuseDaemonForFile(file, mVolumeCache);
6841             int uid = Binder.getCallingUid();
6842 
6843             FdAccessResult result = fuseDaemon.checkFdAccess(inputPfd, uid);
6844             if (!result.isSuccess()) {
6845                 throw new FileNotFoundException("Invalid path for original media format file");
6846             }
6847 
6848             String outputPath = result.filePath;
6849             boolean shouldRedact = result.shouldRedact;
6850 
6851             int posixMode = Os.fcntlInt(inputPfd.getFileDescriptor(), F_GETFL,
6852                     0 /* args */);
6853             int modeBits = FileUtils.translateModePosixToPfd(posixMode);
6854 
6855             ParcelFileDescriptor pfd = openWithFuse(outputPath, uid, 0 /* mediaCapabilitiesUid */,
6856                     modeBits, shouldRedact, false /* shouldTranscode */,
6857                     0 /* transcodeReason */);
6858             return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
6859         } catch (IOException e) {
6860             throw new FileNotFoundException("Failed to fetch original file descriptor");
6861         } catch (ErrnoException e) {
6862             Log.w(TAG, "Failed to fetch access mode for file descriptor", e);
6863             throw new FileNotFoundException("Failed to fetch access mode for file descriptor");
6864         }
6865     }
6866 
6867     /**
6868      * Grant similar read/write access for mediaStoreUri as the caller has for documentsUri.
6869      *
6870      * Note: This function assumes that read permission check for documentsUri is already enforced.
6871      * Note: This function currently does not check/grant for persisted Uris. Support for this can
6872      * be added eventually, but the calling application will have to call
6873      * ContentResolver#takePersistableUriPermission(Uri, int) for the mediaStoreUri to persist.
6874      *
6875      * @param documentsUri DocumentsProvider format content Uri
6876      * @param mediaStoreUri MediaStore format content Uri
6877      * @param callingPid pid of the caller
6878      * @param callingUid uid of the caller
6879      * @param callingPackage package name of the caller
6880      */
6881     private void copyUriPermissionGrants(Uri documentsUri, Uri mediaStoreUri,
6882             int callingPid, int callingUid, String callingPackage) {
6883         // No need to check for read permission, as we enforce it already.
6884         int modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
6885         if (getContext().checkUriPermission(documentsUri, callingPid, callingUid,
6886                 Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PERMISSION_GRANTED) {
6887             modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
6888         }
6889         getContext().grantUriPermission(callingPackage, mediaStoreUri, modeFlags);
6890     }
6891 
6892     static List<Uri> collectUris(ClipData clipData) {
6893         final ArrayList<Uri> res = new ArrayList<>();
6894         for (int i = 0; i < clipData.getItemCount(); i++) {
6895             res.add(clipData.getItemAt(i).getUri());
6896         }
6897         return res;
6898     }
6899 
6900     /**
6901      * Return the filesystem path of the real file on disk that is represented
6902      * by the given {@link ParcelFileDescriptor}.
6903      *
6904      * Note that the file may be a FUSE or lower fs file and depending on the purpose might need
6905      * to be converted with {@link FileUtils#toFuseFile} or {@link FileUtils#fromFuseFile}.
6906      *
6907      * Copied from {@link ParcelFileDescriptor#getFile}
6908      */
6909     private static File getFileFromFileDescriptor(ParcelFileDescriptor fileDescriptor)
6910             throws IOException {
6911         try {
6912             final String path = Os.readlink("/proc/self/fd/" + fileDescriptor.getFd());
6913             if (OsConstants.S_ISREG(Os.stat(path).st_mode)) {
6914                 return new File(path);
6915             } else {
6916                 throw new IOException("Not a regular file: " + path);
6917             }
6918         } catch (ErrnoException e) {
6919             throw e.rethrowAsIOException();
6920         }
6921     }
6922 
6923     /**
6924      * Generate the {@link PendingIntent} for the given grant request. This
6925      * method also checks the incoming arguments for security purposes
6926      * before creating the privileged {@link PendingIntent}.
6927      */
6928     @NonNull
6929     private PendingIntent createRequest(@NonNull String method, @NonNull Bundle extras) {
6930         final ClipData clipData = extras.getParcelable(MediaStore.EXTRA_CLIP_DATA);
6931         final List<Uri> uris = collectUris(clipData);
6932 
6933         for (Uri uri : uris) {
6934             final int match = matchUri(uri, false);
6935             switch (match) {
6936                 case IMAGES_MEDIA_ID:
6937                 case AUDIO_MEDIA_ID:
6938                 case VIDEO_MEDIA_ID:
6939                 case AUDIO_PLAYLISTS_ID:
6940                     // Caller is requesting a specific media item by its ID,
6941                     // which means it's valid for requests
6942                     break;
6943                 case FILES_ID:
6944                     // Allow only subtitle files
6945                     if (!isSubtitleFile(uri)) {
6946                         throw new IllegalArgumentException(
6947                                 "All requested items must be Media items");
6948                     }
6949                     break;
6950                 default:
6951                     throw new IllegalArgumentException(
6952                             "All requested items must be referenced by specific ID");
6953             }
6954         }
6955 
6956         // Enforce that limited set of columns can be mutated
6957         final ContentValues values = extras.getParcelable(MediaStore.EXTRA_CONTENT_VALUES);
6958         final List<String> allowedColumns;
6959         switch (method) {
6960             case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
6961                 allowedColumns = Collections.singletonList(
6962                         MediaColumns.IS_FAVORITE);
6963                 break;
6964             case MediaStore.CREATE_TRASH_REQUEST_CALL:
6965                 allowedColumns = Collections.singletonList(
6966                         MediaColumns.IS_TRASHED);
6967                 break;
6968             default:
6969                 allowedColumns = Collections.emptyList();
6970                 break;
6971         }
6972         if (values != null) {
6973             for (String key : values.keySet()) {
6974                 if (!allowedColumns.contains(key)) {
6975                     throw new IllegalArgumentException("Invalid column " + key);
6976                 }
6977             }
6978         }
6979 
6980         final Context context = getContext();
6981         final Intent intent = new Intent(method, null, context, PermissionActivity.class);
6982         intent.putExtras(extras);
6983         return PendingIntent.getActivity(context, PermissionActivity.REQUEST_CODE, intent,
6984                 FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE);
6985     }
6986 
6987     /**
6988      * @return true if the given Files uri has media_type=MEDIA_TYPE_SUBTITLE
6989      */
6990     private boolean isSubtitleFile(Uri uri) {
6991         final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
6992         try (Cursor cursor = queryForSingleItem(uri, new String[]{FileColumns.MEDIA_TYPE}, null,
6993                 null, null)) {
6994             return cursor.getInt(0) == FileColumns.MEDIA_TYPE_SUBTITLE;
6995         } catch (FileNotFoundException e) {
6996             Log.e(TAG, "Couldn't find database row for requested uri " + uri, e);
6997         } finally {
6998             restoreLocalCallingIdentity(tokenInner);
6999         }
7000         return false;
7001     }
7002 
7003     /**
7004      * Ensure that all local databases have a custom collator registered for the
7005      * given {@link ULocale} locale.
7006      *
7007      * @return the corresponding custom collation name to be used in
7008      *         {@code ORDER BY} clauses.
7009      */
7010     @NonNull
7011     private String ensureCustomCollator(@NonNull String locale) {
7012         // Quick check that requested locale looks reasonable
7013         new ULocale(locale);
7014 
7015         final String collationName = "custom_" + locale.replaceAll("[^a-zA-Z]", "");
7016         synchronized (mCustomCollators) {
7017             if (!mCustomCollators.contains(collationName)) {
7018                 for (DatabaseHelper helper : new DatabaseHelper[] {
7019                         mInternalDatabase,
7020                         mExternalDatabase
7021                 }) {
7022                     helper.runWithoutTransaction((db) -> {
7023                         db.execPerConnectionSQL("SELECT icu_load_collation(?, ?);",
7024                                 new String[] { locale, collationName });
7025                         return null;
7026                     });
7027                 }
7028                 mCustomCollators.add(collationName);
7029             }
7030         }
7031         return collationName;
7032     }
7033 
7034     private int pruneThumbnails(@NonNull SQLiteDatabase db, @NonNull CancellationSignal signal) {
7035         int prunedCount = 0;
7036 
7037         // Determine all known media items
7038         final LongArray knownIds = new LongArray();
7039         try (Cursor c = db.query(true, "files", new String[] { BaseColumns._ID },
7040                 null, null, null, null, null, null, signal)) {
7041             while (c.moveToNext()) {
7042                 knownIds.add(c.getLong(0));
7043             }
7044         }
7045 
7046         final long[] knownIdsRaw = knownIds.toArray();
7047         Arrays.sort(knownIdsRaw);
7048 
7049         for (MediaVolume volume : mVolumeCache.getExternalVolumes()) {
7050             final List<File> thumbDirs;
7051             try {
7052                 thumbDirs = getThumbnailDirectories(volume);
7053             } catch (FileNotFoundException e) {
7054                 Log.w(TAG, "Failed to resolve volume " + volume.getName(), e);
7055                 continue;
7056             }
7057 
7058             // Reconcile all thumbnails, deleting stale items
7059             for (File thumbDir : thumbDirs) {
7060                 // Possibly bail before digging into each directory
7061                 signal.throwIfCanceled();
7062 
7063                 final File[] files = thumbDir.listFiles();
7064                 for (File thumbFile : (files != null) ? files : new File[0]) {
7065                     if (Objects.equals(thumbFile.getName(), FILE_DATABASE_UUID)) continue;
7066                     if (Objects.equals(thumbFile.getName(), MEDIA_IGNORE_FILENAME)) continue;
7067                     final String name = FileUtils.extractFileName(thumbFile.getName());
7068                     try {
7069                         final long id = Long.parseLong(name);
7070                         if (Arrays.binarySearch(knownIdsRaw, id) >= 0) {
7071                             // Thumbnail belongs to known media, keep it
7072                             continue;
7073                         }
7074                     } catch (NumberFormatException e) {
7075                     }
7076 
7077                     Log.v(TAG, "Deleting stale thumbnail " + thumbFile);
7078                     deleteAndInvalidate(thumbFile);
7079                     prunedCount++;
7080                 }
7081             }
7082         }
7083 
7084         // Also delete stale items from legacy tables
7085         db.execSQL("delete from thumbnails "
7086                 + "where image_id not in (select _id from images)");
7087         db.execSQL("delete from videothumbnails "
7088                 + "where video_id not in (select _id from video)");
7089 
7090         return prunedCount;
7091     }
7092 
7093     abstract class Thumbnailer {
7094         final String directoryName;
7095 
7096         public Thumbnailer(String directoryName) {
7097             this.directoryName = directoryName;
7098         }
7099 
7100         private File getThumbnailFile(Uri uri) throws IOException {
7101             final String volumeName = resolveVolumeName(uri);
7102             final File volumePath = getVolumePath(volumeName);
7103             return FileUtils.buildPath(volumePath, directoryName,
7104                     DIRECTORY_THUMBNAILS, ContentUris.parseId(uri) + ".jpg");
7105         }
7106 
7107         public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal)
7108                 throws IOException;
7109 
7110         public ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
7111                 throws IOException {
7112             // First attempt to fast-path by opening the thumbnail; if it
7113             // doesn't exist we fall through to create it below
7114             final File thumbFile = getThumbnailFile(uri);
7115             try {
7116                 return FileUtils.openSafely(thumbFile,
7117                         ParcelFileDescriptor.MODE_READ_ONLY);
7118             } catch (FileNotFoundException ignored) {
7119             }
7120 
7121             final File thumbDir = thumbFile.getParentFile();
7122             thumbDir.mkdirs();
7123 
7124             // When multiple threads race for the same thumbnail, the second
7125             // thread could return a file with a thumbnail still in
7126             // progress. We could add heavy per-ID locking to mitigate this
7127             // rare race condition, but it's simpler to have both threads
7128             // generate the same thumbnail using temporary files and rename
7129             // them into place once finished.
7130             final File thumbTempFile = File.createTempFile("thumb", null, thumbDir);
7131 
7132             ParcelFileDescriptor thumbWrite = null;
7133             ParcelFileDescriptor thumbRead = null;
7134             try {
7135                 // Open our temporary file twice: once for local writing, and
7136                 // once for remote reading. Both FDs point at the same
7137                 // underlying inode on disk, so they're stable across renames
7138                 // to avoid race conditions between threads.
7139                 thumbWrite = FileUtils.openSafely(thumbTempFile,
7140                         ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE);
7141                 thumbRead = FileUtils.openSafely(thumbTempFile,
7142                         ParcelFileDescriptor.MODE_READ_ONLY);
7143 
7144                 final Bitmap thumbnail = getThumbnailBitmap(uri, signal);
7145                 thumbnail.compress(Bitmap.CompressFormat.JPEG, 90,
7146                         new FileOutputStream(thumbWrite.getFileDescriptor()));
7147 
7148                 try {
7149                     // Use direct syscall for better failure logs
7150                     Os.rename(thumbTempFile.getAbsolutePath(), thumbFile.getAbsolutePath());
7151                 } catch (ErrnoException e) {
7152                     e.rethrowAsIOException();
7153                 }
7154 
7155                 // Everything above went peachy, so return a duplicate of our
7156                 // already-opened read FD to keep our finally logic below simple
7157                 return thumbRead.dup();
7158 
7159             } finally {
7160                 // Regardless of success or failure, try cleaning up any
7161                 // remaining temporary file and close all our local FDs
7162                 FileUtils.closeQuietly(thumbWrite);
7163                 FileUtils.closeQuietly(thumbRead);
7164                 deleteAndInvalidate(thumbTempFile);
7165             }
7166         }
7167 
7168         public void invalidateThumbnail(Uri uri) throws IOException {
7169             deleteAndInvalidate(getThumbnailFile(uri));
7170         }
7171     }
7172 
7173     private final Thumbnailer mAudioThumbnailer = new Thumbnailer(Environment.DIRECTORY_MUSIC) {
7174         @Override
7175         public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
7176             return ThumbnailUtils.createAudioThumbnail(queryForDataFile(uri, signal),
7177                     mThumbSize, signal);
7178         }
7179     };
7180 
7181     private final Thumbnailer mVideoThumbnailer = new Thumbnailer(Environment.DIRECTORY_MOVIES) {
7182         @Override
7183         public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
7184             return ThumbnailUtils.createVideoThumbnail(queryForDataFile(uri, signal),
7185                     mThumbSize, signal);
7186         }
7187     };
7188 
7189     private final Thumbnailer mImageThumbnailer = new Thumbnailer(Environment.DIRECTORY_PICTURES) {
7190         @Override
7191         public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
7192             return ThumbnailUtils.createImageThumbnail(queryForDataFile(uri, signal),
7193                     mThumbSize, signal);
7194         }
7195     };
7196 
7197     private List<File> getThumbnailDirectories(MediaVolume volume) throws FileNotFoundException {
7198         final File volumePath = volume.getPath();
7199         return Arrays.asList(
7200                 FileUtils.buildPath(volumePath, Environment.DIRECTORY_MUSIC, DIRECTORY_THUMBNAILS),
7201                 FileUtils.buildPath(volumePath, Environment.DIRECTORY_MOVIES, DIRECTORY_THUMBNAILS),
7202                 FileUtils.buildPath(volumePath, Environment.DIRECTORY_PICTURES,
7203                         DIRECTORY_THUMBNAILS));
7204     }
7205 
7206     private void invalidateThumbnails(Uri uri) {
7207         Trace.beginSection("MP.invalidateThumbnails");
7208         try {
7209             invalidateThumbnailsInternal(uri);
7210         } finally {
7211             Trace.endSection();
7212         }
7213     }
7214 
7215     private void invalidateThumbnailsInternal(Uri uri) {
7216         final long id = ContentUris.parseId(uri);
7217         try {
7218             mAudioThumbnailer.invalidateThumbnail(uri);
7219             mVideoThumbnailer.invalidateThumbnail(uri);
7220             mImageThumbnailer.invalidateThumbnail(uri);
7221         } catch (IOException ignored) {
7222         }
7223 
7224         final DatabaseHelper helper;
7225         try {
7226             helper = getDatabaseForUri(uri);
7227         } catch (VolumeNotFoundException e) {
7228             Log.w(TAG, e);
7229             return;
7230         }
7231 
7232         helper.runWithTransaction((db) -> {
7233             final String idString = Long.toString(id);
7234             try (Cursor c = db.rawQuery("select _data from thumbnails where image_id=?"
7235                     + " union all select _data from videothumbnails where video_id=?",
7236                     new String[] { idString, idString })) {
7237                 while (c.moveToNext()) {
7238                     String path = c.getString(0);
7239                     deleteIfAllowed(uri, Bundle.EMPTY, path);
7240                 }
7241             }
7242 
7243             db.execSQL("delete from thumbnails where image_id=?", new String[] { idString });
7244             db.execSQL("delete from videothumbnails where video_id=?", new String[] { idString });
7245             return null;
7246         });
7247     }
7248 
7249     /**
7250      * @deprecated all operations should be routed through the overload that
7251      *             accepts a {@link Bundle} of extras.
7252      */
7253     @Override
7254     @Deprecated
7255     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
7256         return update(uri, values,
7257                 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null));
7258     }
7259 
7260     @Override
7261     public int update(@NonNull Uri uri, @Nullable ContentValues values,
7262             @Nullable Bundle extras) {
7263         Trace.beginSection("MP.update [" + uri + ']');
7264         try {
7265             return updateInternal(uri, values, extras);
7266         } catch (FallbackException e) {
7267             return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion());
7268         } finally {
7269             Trace.endSection();
7270         }
7271     }
7272 
7273     private int updateInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
7274             @Nullable Bundle extras) throws FallbackException {
7275         final String volumeName = getVolumeName(uri);
7276         PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
7277 
7278         extras = (extras != null) ? extras : new Bundle();
7279         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
7280         extras.remove(QUERY_ARG_REDACTED_URI);
7281 
7282         if (isRedactedUri(uri)) {
7283             // we don't support update on redacted uris.
7284             return 0;
7285         }
7286 
7287         // Related items are only considered for new media creation, and they
7288         // can't be leveraged to move existing content into blocked locations
7289         extras.remove(QUERY_ARG_RELATED_URI);
7290         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
7291         extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
7292 
7293         final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION);
7294         final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);
7295 
7296         // Limit the hacky workaround to camera targeting Q and below, to allow newer versions
7297         // of camera that does the right thing to work correctly.
7298         if ("com.google.android.GoogleCamera".equals(getCallingPackageOrSelf())
7299                 && getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) {
7300             if (matchUri(uri, false) == IMAGES_MEDIA_ID) {
7301                 Log.w(TAG, "Working around app bug in b/111966296");
7302                 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri));
7303             } else if (matchUri(uri, false) == VIDEO_MEDIA_ID) {
7304                 Log.w(TAG, "Working around app bug in b/112246630");
7305                 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri));
7306             }
7307         }
7308 
7309         uri = safeUncanonicalize(uri);
7310 
7311         int count;
7312 
7313         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
7314         final boolean allowHidden = isCallingPackageAllowedHidden();
7315         final int match = matchUri(uri, allowHidden);
7316         final DatabaseHelper helper = getDatabaseForUri(uri);
7317 
7318         switch (match) {
7319             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
7320                 extras.putString(QUERY_ARG_SQL_SELECTION,
7321                         BaseColumns._ID + "=" + uri.getPathSegments().get(5));
7322                 // fall-through
7323             case AUDIO_PLAYLISTS_ID_MEMBERS: {
7324                 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
7325                 final Uri playlistUri = ContentUris.withAppendedId(
7326                         MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);
7327                 if (uri.getBooleanQueryParameter("move", false)) {
7328                     // Convert explicit request into query; sigh, moveItem()
7329                     // uses zero-based indexing instead of one-based indexing
7330                     final int from = Integer.parseInt(uri.getPathSegments().get(5)) + 1;
7331                     final int to = initialValues.getAsInteger(Playlists.Members.PLAY_ORDER) + 1;
7332                     extras.putString(QUERY_ARG_SQL_SELECTION,
7333                             Playlists.Members.PLAY_ORDER + "=" + from);
7334                     initialValues.put(Playlists.Members.PLAY_ORDER, to);
7335                 }
7336 
7337                 // Playlist contents are always persisted directly into playlist
7338                 // files on disk to ensure that we can reliably migrate between
7339                 // devices and recover from database corruption
7340                 final int index;
7341                 if (initialValues.containsKey(Playlists.Members.PLAY_ORDER)) {
7342                     index = movePlaylistMembers(playlistUri, initialValues, extras);
7343                 } else {
7344                     index = resolvePlaylistIndex(playlistUri, extras);
7345                 }
7346                 if (initialValues.containsKey(Playlists.Members.AUDIO_ID)) {
7347                     final Bundle queryArgs = new Bundle();
7348                     queryArgs.putString(QUERY_ARG_SQL_SELECTION,
7349                             Playlists.Members.PLAY_ORDER + "=" + (index + 1));
7350                     removePlaylistMembers(playlistUri, queryArgs);
7351 
7352                     final ContentValues values = new ContentValues();
7353                     values.put(Playlists.Members.AUDIO_ID,
7354                             initialValues.getAsString(Playlists.Members.AUDIO_ID));
7355                     values.put(Playlists.Members.PLAY_ORDER, (index + 1));
7356                     addPlaylistMembers(playlistUri, values);
7357                 }
7358 
7359                 acceptWithExpansion(helper::notifyUpdate, volumeName, playlistId,
7360                         FileColumns.MEDIA_TYPE_PLAYLIST, false);
7361                 return 1;
7362             }
7363         }
7364 
7365         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null);
7366 
7367         // Give callers interacting with a specific media item a chance to
7368         // escalate access if they don't already have it
7369         switch (match) {
7370             case AUDIO_MEDIA_ID:
7371             case VIDEO_MEDIA_ID:
7372             case IMAGES_MEDIA_ID:
7373                 enforceCallingPermission(uri, extras, true);
7374         }
7375 
7376         boolean triggerInvalidate = false;
7377         boolean triggerScan = false;
7378         boolean isUriPublished = false;
7379         if (initialValues != null) {
7380             // IDs are forever; nobody should be editing them
7381             initialValues.remove(MediaColumns._ID);
7382 
7383             // Expiration times are hard-coded; let's derive them
7384             FileUtils.computeDateExpires(initialValues);
7385 
7386             // Ignore or augment incoming raw filesystem paths
7387             for (String column : sDataColumns.keySet()) {
7388                 if (!initialValues.containsKey(column)) continue;
7389 
7390                 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
7391                     // Mutation allowed
7392                 } else {
7393                     Log.w(TAG, "Ignoring mutation of  " + column + " from "
7394                             + getCallingPackageOrSelf());
7395                     initialValues.remove(column);
7396                 }
7397             }
7398 
7399             // Enforce allowed ownership transfers
7400             if (initialValues.containsKey(MediaColumns.OWNER_PACKAGE_NAME)) {
7401                 if (isCallingPackageSelf() || isCallingPackageShell()) {
7402                     // When the caller is the media scanner or the shell, we let
7403                     // them change ownership however they see fit; nothing to do
7404                 } else if (isCallingPackageDelegator()) {
7405                     // When the caller is a delegator, allow them to shift
7406                     // ownership only when current owner, or when ownerless
7407                     final String currentOwner;
7408                     final String proposedOwner = initialValues
7409                             .getAsString(MediaColumns.OWNER_PACKAGE_NAME);
7410                     final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
7411                             ContentUris.parseId(uri));
7412                     try (Cursor c = queryForSingleItem(genericUri,
7413                             new String[] { MediaColumns.OWNER_PACKAGE_NAME }, null, null, null)) {
7414                         currentOwner = c.getString(0);
7415                     } catch (FileNotFoundException e) {
7416                         throw new IllegalStateException(e);
7417                     }
7418                     final boolean transferAllowed = (currentOwner == null)
7419                             || Arrays.asList(getSharedPackagesForPackage(getCallingPackageOrSelf()))
7420                                     .contains(currentOwner);
7421                     if (transferAllowed) {
7422                         Log.v(TAG, "Ownership transfer from " + currentOwner + " to "
7423                                 + proposedOwner + " allowed");
7424                     } else {
7425                         Log.w(TAG, "Ownership transfer from " + currentOwner + " to "
7426                                 + proposedOwner + " blocked");
7427                         initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
7428                     }
7429                 } else {
7430                     // Otherwise no ownership changes are allowed
7431                     initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
7432                 }
7433             }
7434 
7435             if (!isCallingPackageSelf()) {
7436                 Trace.beginSection("MP.filter");
7437 
7438                 // We default to filtering mutable columns, except when we know
7439                 // the single item being updated is pending; when it's finally
7440                 // published we'll overwrite these values.
7441                 final Uri finalUri = uri;
7442                 final Supplier<Boolean> isPending = new CachedSupplier<>(() -> {
7443                     return isPending(finalUri);
7444                 });
7445 
7446                 // Column values controlled by media scanner aren't writable by
7447                 // apps, since any edits here don't reflect the metadata on
7448                 // disk, and they'd be overwritten during a rescan.
7449                 for (String column : new ArraySet<>(initialValues.keySet())) {
7450                     if (sMutableColumns.contains(column)) {
7451                         // Mutation normally allowed
7452                     } else if (isPending.get()) {
7453                         // Mutation relaxed while pending
7454                     } else {
7455                         Log.w(TAG, "Ignoring mutation of " + column + " from "
7456                                 + getCallingPackageOrSelf());
7457                         initialValues.remove(column);
7458                         triggerScan = true;
7459                     }
7460 
7461                     // If we're publishing this item, perform a blocking scan to
7462                     // make sure metadata is updated
7463                     if (MediaColumns.IS_PENDING.equals(column)) {
7464                         triggerScan = true;
7465                         isUriPublished = true;
7466                         // Explicitly clear columns used to ignore no-op scans,
7467                         // since we need to force a scan on publish
7468                         initialValues.putNull(MediaColumns.DATE_MODIFIED);
7469                         initialValues.putNull(MediaColumns.SIZE);
7470                     }
7471                 }
7472 
7473                 Trace.endSection();
7474             }
7475 
7476             if ("files".equals(qb.getTables())) {
7477                 maybeMarkAsDownload(initialValues);
7478             }
7479 
7480             // We no longer track location metadata
7481             if (initialValues.containsKey(ImageColumns.LATITUDE)) {
7482                 initialValues.putNull(ImageColumns.LATITUDE);
7483             }
7484             if (initialValues.containsKey(ImageColumns.LONGITUDE)) {
7485                 initialValues.putNull(ImageColumns.LONGITUDE);
7486             }
7487             if (getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) {
7488                 // These columns are removed in R.
7489                 if (initialValues.containsKey("primary_directory")) {
7490                     initialValues.remove("primary_directory");
7491                 }
7492                 if (initialValues.containsKey("secondary_directory")) {
7493                     initialValues.remove("secondary_directory");
7494                 }
7495             }
7496         }
7497 
7498         // If we're not updating anything, then we can skip
7499         if (initialValues.isEmpty()) return 0;
7500 
7501         final boolean isThumbnail;
7502         switch (match) {
7503             case IMAGES_THUMBNAILS:
7504             case IMAGES_THUMBNAILS_ID:
7505             case VIDEO_THUMBNAILS:
7506             case VIDEO_THUMBNAILS_ID:
7507             case AUDIO_ALBUMART:
7508             case AUDIO_ALBUMART_ID:
7509                 isThumbnail = true;
7510                 break;
7511             default:
7512                 isThumbnail = false;
7513                 break;
7514         }
7515 
7516         switch (match) {
7517             case AUDIO_PLAYLISTS:
7518             case AUDIO_PLAYLISTS_ID:
7519                 // Playlist names are stored as display names, but leave
7520                 // values untouched if the caller is ModernMediaScanner
7521                 if (!isCallingPackageSelf()) {
7522                     if (initialValues.containsKey(Playlists.NAME)) {
7523                         initialValues.put(MediaColumns.DISPLAY_NAME,
7524                                 initialValues.getAsString(Playlists.NAME));
7525                     }
7526                     if (!initialValues.containsKey(MediaColumns.MIME_TYPE)) {
7527                         initialValues.put(MediaColumns.MIME_TYPE, "audio/mpegurl");
7528                     }
7529                 }
7530                 break;
7531         }
7532 
7533         // If we're touching columns that would change placement of a file,
7534         // blend in current values and recalculate path
7535         final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT,
7536                 !isCallingPackageSelf());
7537         if (containsAny(initialValues.keySet(), sPlacementColumns)
7538                 && !initialValues.containsKey(MediaColumns.DATA)
7539                 && !isThumbnail
7540                 && allowMovement) {
7541             Trace.beginSection("MP.movement");
7542 
7543             // We only support movement under well-defined collections
7544             switch (match) {
7545                 case AUDIO_MEDIA_ID:
7546                 case AUDIO_PLAYLISTS_ID:
7547                 case VIDEO_MEDIA_ID:
7548                 case IMAGES_MEDIA_ID:
7549                 case DOWNLOADS_ID:
7550                 case FILES_ID:
7551                     break;
7552                 default:
7553                     throw new IllegalArgumentException("Movement of " + uri
7554                             + " which isn't part of well-defined collection not allowed");
7555             }
7556 
7557             final LocalCallingIdentity token = clearLocalCallingIdentity();
7558             final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
7559                     ContentUris.parseId(uri));
7560             try (Cursor c = queryForSingleItem(genericUri,
7561                     sPlacementColumns.toArray(new String[0]), userWhere, userWhereArgs, null)) {
7562                 for (int i = 0; i < c.getColumnCount(); i++) {
7563                     final String column = c.getColumnName(i);
7564                     if (!initialValues.containsKey(column)) {
7565                         initialValues.put(column, c.getString(i));
7566                     }
7567                 }
7568             } catch (FileNotFoundException e) {
7569                 throw new IllegalStateException(e);
7570             } finally {
7571                 restoreLocalCallingIdentity(token);
7572             }
7573 
7574             // Regenerate path using blended values; this will throw if caller
7575             // is attempting to place file into invalid location
7576             final String beforePath = initialValues.getAsString(MediaColumns.DATA);
7577             final String beforeVolume = extractVolumeName(beforePath);
7578             final String beforeOwner = extractPathOwnerPackageName(beforePath);
7579 
7580             initialValues.remove(MediaColumns.DATA);
7581             ensureNonUniqueFileColumns(match, uri, extras, initialValues, beforePath);
7582 
7583             final String probePath = initialValues.getAsString(MediaColumns.DATA);
7584             final String probeVolume = extractVolumeName(probePath);
7585             final String probeOwner = extractPathOwnerPackageName(probePath);
7586             if (Objects.equals(beforePath, probePath)) {
7587                 Log.d(TAG, "Identical paths " + beforePath + "; not moving");
7588             } else if (!Objects.equals(beforeVolume, probeVolume)) {
7589                 throw new IllegalArgumentException("Changing volume from " + beforePath + " to "
7590                         + probePath + " not allowed");
7591             } else if (!isUpdateAllowedForOwnedPath(beforeOwner, probeOwner, beforePath,
7592                     probePath)) {
7593                 throw new IllegalArgumentException("Changing ownership from " + beforePath + " to "
7594                         + probePath + " not allowed");
7595             } else {
7596                 // Now that we've confirmed an actual movement is taking place,
7597                 // ensure we have a unique destination
7598                 initialValues.remove(MediaColumns.DATA);
7599                 ensureUniqueFileColumns(match, uri, extras, initialValues, beforePath);
7600 
7601                 String afterPath = initialValues.getAsString(MediaColumns.DATA);
7602 
7603                 if (isCrossUserEnabled()) {
7604                     String afterVolume = extractVolumeName(afterPath);
7605                     String afterVolumePath =  extractVolumePath(afterPath);
7606                     String beforeVolumePath = extractVolumePath(beforePath);
7607 
7608                     if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equals(beforeVolume)
7609                             && beforeVolume.equals(afterVolume)
7610                             && !beforeVolumePath.equals(afterVolumePath)) {
7611                         // On cross-user enabled devices, it can happen that a rename intended as
7612                         // /storage/emulated/999/foo -> /storage/emulated/999/foo can end up as
7613                         // /storage/emulated/999/foo -> /storage/emulated/0/foo. We now fix-up
7614                         afterPath = afterPath.replaceFirst(afterVolumePath, beforeVolumePath);
7615                     }
7616                 }
7617 
7618                 Log.d(TAG, "Moving " + beforePath + " to " + afterPath);
7619                 try {
7620                     Os.rename(beforePath, afterPath);
7621                     invalidateFuseDentry(beforePath);
7622                     invalidateFuseDentry(afterPath);
7623                 } catch (ErrnoException e) {
7624                     if (e.errno == OsConstants.ENOENT) {
7625                         Log.d(TAG, "Missing file at " + beforePath + "; continuing anyway");
7626                     } else {
7627                         throw new IllegalStateException(e);
7628                     }
7629                 }
7630                 initialValues.put(MediaColumns.DATA, afterPath);
7631 
7632                 // Some indexed metadata may have been derived from the path on
7633                 // disk, so scan this item again to update it
7634                 triggerScan = true;
7635             }
7636 
7637             Trace.endSection();
7638         }
7639 
7640         assertPrivatePathNotInValues(initialValues);
7641 
7642         // Make sure any updated paths look consistent
7643         assertFileColumnsConsistent(match, uri, initialValues);
7644 
7645         if (initialValues.containsKey(FileColumns.DATA)) {
7646             // If we're changing paths, invalidate any thumbnails
7647             triggerInvalidate = true;
7648 
7649             // If the new file exists, trigger a scan to adjust any metadata
7650             // that might be derived from the path
7651             final String data = initialValues.getAsString(FileColumns.DATA);
7652             if (!TextUtils.isEmpty(data) && new File(data).exists()) {
7653                 triggerScan = true;
7654             }
7655         }
7656 
7657         // If we're already doing this update from an internal scan, no need to
7658         // kick off another no-op scan
7659         if (isCallingPackageSelf()) {
7660             triggerScan = false;
7661         }
7662 
7663         // Since the update mutation may prevent us from matching items after
7664         // it's applied, we need to snapshot affected IDs here
7665         final LongArray updatedIds = new LongArray();
7666         if (triggerInvalidate || triggerScan) {
7667             Trace.beginSection("MP.snapshot");
7668             final LocalCallingIdentity token = clearLocalCallingIdentity();
7669             try (Cursor c = qb.query(helper, new String[] { FileColumns._ID },
7670                     userWhere, userWhereArgs, null, null, null, null, null)) {
7671                 while (c.moveToNext()) {
7672                     updatedIds.add(c.getLong(0));
7673                 }
7674             } finally {
7675                 restoreLocalCallingIdentity(token);
7676                 Trace.endSection();
7677             }
7678         }
7679 
7680         final ContentValues values = new ContentValues(initialValues);
7681         switch (match) {
7682             case AUDIO_MEDIA_ID:
7683             case AUDIO_PLAYLISTS_ID:
7684             case VIDEO_MEDIA_ID:
7685             case IMAGES_MEDIA_ID:
7686             case FILES_ID:
7687             case DOWNLOADS_ID: {
7688                 FileUtils.computeValuesFromData(values, isFuseThread());
7689                 break;
7690             }
7691         }
7692 
7693         if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) {
7694             final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE);
7695             switch (mediaType) {
7696                 case FileColumns.MEDIA_TYPE_AUDIO: {
7697                     computeAudioLocalizedValues(values);
7698                     computeAudioKeyValues(values);
7699                     break;
7700                 }
7701             }
7702         }
7703 
7704         boolean deferScan = false;
7705         if (triggerScan) {
7706             if (SdkLevel.isAtLeastS() &&
7707                     CompatChanges.isChangeEnabled(ENABLE_DEFERRED_SCAN, Binder.getCallingUid())) {
7708                 if (extras.containsKey(QUERY_ARG_DO_ASYNC_SCAN)) {
7709                     throw new IllegalArgumentException("Unsupported argument " +
7710                             QUERY_ARG_DO_ASYNC_SCAN + " used in extras");
7711                 }
7712                 deferScan = extras.getBoolean(QUERY_ARG_DEFER_SCAN, false);
7713                 if (deferScan && initialValues.containsKey(MediaColumns.IS_PENDING) &&
7714                         (initialValues.getAsInteger(MediaColumns.IS_PENDING) == 1)) {
7715                     // if the scan runs in async, ensure that the database row is excluded in
7716                     // default query until the metadata is updated by deferred scan.
7717                     // Apps will still be able to see this database row when queried with
7718                     // QUERY_ARG_MATCH_PENDING=MATCH_INCLUDE
7719                     values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR_PENDING_METADATA);
7720                     qb.allowColumn(FileColumns._MODIFIER);
7721                 }
7722             } else {
7723                 // Allow apps to use QUERY_ARG_DO_ASYNC_SCAN if the device is R or app is targeting
7724                 // targetSDK<=R.
7725                 deferScan = extras.getBoolean(QUERY_ARG_DO_ASYNC_SCAN, false);
7726             }
7727         }
7728 
7729         count = updateAllowingReplace(qb, helper, values, userWhere, userWhereArgs);
7730 
7731         // If the caller tried (and failed) to update metadata, the file on disk
7732         // might have changed, to scan it to collect the latest metadata.
7733         if (triggerInvalidate || triggerScan) {
7734             Trace.beginSection("MP.invalidate");
7735             final LocalCallingIdentity token = clearLocalCallingIdentity();
7736             try {
7737                 for (int i = 0; i < updatedIds.size(); i++) {
7738                     final long updatedId = updatedIds.get(i);
7739                     final Uri updatedUri = Files.getContentUri(volumeName, updatedId);
7740                     helper.postBackground(() -> {
7741                         invalidateThumbnails(updatedUri);
7742                     });
7743 
7744                     if (triggerScan) {
7745                         try (Cursor c = queryForSingleItem(updatedUri,
7746                                 new String[] { FileColumns.DATA }, null, null, null)) {
7747                             final File file = new File(c.getString(0));
7748                             final boolean notifyTranscodeHelper = isUriPublished;
7749                             if (deferScan) {
7750                                 helper.postBackground(() -> {
7751                                     scanFileAsMediaProvider(file, REASON_DEMAND);
7752                                     if (notifyTranscodeHelper) {
7753                                         notifyTranscodeHelperOnUriPublished(updatedUri, file);
7754                                     }
7755                                 });
7756                             } else {
7757                                 helper.postBlocking(() -> {
7758                                     scanFileAsMediaProvider(file, REASON_DEMAND);
7759                                     if (notifyTranscodeHelper) {
7760                                         notifyTranscodeHelperOnUriPublished(updatedUri, file);
7761                                     }
7762                                 });
7763                             }
7764                         } catch (Exception e) {
7765                             Log.w(TAG, "Failed to update metadata for " + updatedUri, e);
7766                         }
7767                     }
7768                 }
7769             } finally {
7770                 restoreLocalCallingIdentity(token);
7771                 Trace.endSection();
7772             }
7773         }
7774 
7775         return count;
7776     }
7777 
7778     private boolean isUpdateAllowedForOwnedPath(@Nullable String srcOwner,
7779             @Nullable String destOwner, @NonNull String srcPath, @NonNull String destPath) {
7780         // 1. Allow if the update is within owned path
7781         // update() from /sdcard/Android/media/com.foo/ABC/image.jpeg to
7782         // /sdcard/Android/media/com.foo/XYZ/image.jpeg - Allowed
7783         if(Objects.equals(srcOwner, destOwner)) {
7784             return true;
7785         }
7786 
7787         // 2. Check if the calling package is a special app which has global access
7788         if (isCallingPackageManager() || (canSystemGalleryAccessTheFile(srcPath) &&
7789             (canSystemGalleryAccessTheFile(destPath)))) {
7790             return true;
7791         }
7792 
7793         // 3. Allow update from srcPath if the source is not a owned path or calling package is the
7794         // owner of the source path or calling package shares the UID with the owner of the source
7795         // path
7796         // update() from /sdcard/DCIM/Foo.jpeg - Allowed
7797         // update() from /sdcard/Android/media/com.foo/image.jpeg - Allowed for
7798         // callingPackage=com.foo, not allowed for callingPackage=com.bar
7799         final boolean isSrcUpdateAllowed = srcOwner == null
7800                 || isCallingIdentitySharedPackageName(srcOwner);
7801 
7802         // 4. Allow update to dstPath if the destination is not a owned path or calling package is
7803         // the owner of the destination path or calling package shares the UID with the owner of the
7804         // destination path
7805         // update() to /sdcard/Pictures/image.jpeg - Allowed
7806         // update() to /sdcard/Android/media/com.foo/image.jpeg - Allowed for
7807         // callingPackage=com.foo, not allowed for callingPackage=com.bar
7808         final boolean isDestUpdateAllowed = destOwner == null
7809                 || isCallingIdentitySharedPackageName(destOwner);
7810 
7811         return isSrcUpdateAllowed && isDestUpdateAllowed;
7812     }
7813 
7814     private void notifyTranscodeHelperOnUriPublished(Uri uri, File file) {
7815         if (!mTranscodeHelper.supportsTranscode(file.getPath())) {
7816             return;
7817         }
7818 
7819         BackgroundThread.getExecutor().execute(() -> {
7820             final LocalCallingIdentity token = clearLocalCallingIdentity();
7821             try {
7822                 mTranscodeHelper.onUriPublished(uri);
7823             } finally {
7824                 restoreLocalCallingIdentity(token);
7825             }
7826         });
7827     }
7828 
7829     private void notifyTranscodeHelperOnFileOpen(String path, String ioPath, int uid,
7830             int transformsReason) {
7831         if (!mTranscodeHelper.supportsTranscode(path)) {
7832             return;
7833         }
7834 
7835         BackgroundThread.getExecutor().execute(() -> {
7836             final LocalCallingIdentity token = clearLocalCallingIdentity();
7837             try {
7838                 mTranscodeHelper.onFileOpen(path, ioPath, uid, transformsReason);
7839             } finally {
7840                 restoreLocalCallingIdentity(token);
7841             }
7842         });
7843     }
7844 
7845     /**
7846      * Update row(s) that match {@code userWhere} in MediaProvider database with {@code values}.
7847      * Treats update as replace for updates with conflicts.
7848      */
7849     private int updateAllowingReplace(@NonNull SQLiteQueryBuilder qb,
7850             @NonNull DatabaseHelper helper, @NonNull ContentValues values, String userWhere,
7851             String[] userWhereArgs) throws SQLiteConstraintException {
7852         return helper.runWithTransaction((db) -> {
7853             try {
7854                 return qb.update(helper, values, userWhere, userWhereArgs);
7855             } catch (SQLiteConstraintException e) {
7856                 // b/155320967 Apps sometimes create a file via file path and then update another
7857                 // explicitly inserted db row to this file. We have to resolve this update with a
7858                 // replace.
7859 
7860                 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
7861                     // We don't support replace for non-legacy apps. Non legacy apps should have
7862                     // clearer interactions with MediaProvider.
7863                     throw e;
7864                 }
7865 
7866                 final String path = values.getAsString(FileColumns.DATA);
7867 
7868                 // We will only handle UNIQUE constraint error for FileColumns.DATA. We will not try
7869                 // update and replace if no file exists for conflicting db row.
7870                 if (path == null || !new File(path).exists()) {
7871                     throw e;
7872                 }
7873 
7874                 final Uri uri = FileUtils.getContentUriForPath(path);
7875                 final boolean allowHidden = isCallingPackageAllowedHidden();
7876                 // The db row which caused UNIQUE constraint error may not match all column values
7877                 // of the given queryBuilder, hence using a generic queryBuilder with Files uri.
7878                 Bundle extras = new Bundle();
7879                 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE);
7880                 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE);
7881                 final SQLiteQueryBuilder qbForReplace = getQueryBuilder(TYPE_DELETE,
7882                         matchUri(uri, allowHidden), uri, extras, null);
7883                 final long rowId = getIdIfPathOwnedByPackages(qbForReplace, helper, path,
7884                         mCallingIdentity.get().getSharedPackagesAsString());
7885 
7886                 if (rowId != -1 && qbForReplace.delete(helper, "_id=?",
7887                         new String[] {Long.toString(rowId)}) == 1) {
7888                     Log.i(TAG, "Retrying database update after deleting conflicting entry");
7889                     return qb.update(helper, values, userWhere, userWhereArgs);
7890                 }
7891                 // Rethrow SQLiteConstraintException if app doesn't own the conflicting db row.
7892                 throw e;
7893             }
7894         });
7895     }
7896 
7897     /**
7898      * Update the internal table of {@link MediaStore.Audio.Playlists.Members}
7899      * by parsing the playlist file on disk and resolving it against scanned
7900      * audio items.
7901      * <p>
7902      * When a playlist references a missing audio item, the associated
7903      * {@link Playlists.Members#PLAY_ORDER} is skipped, leaving a gap to ensure
7904      * that the playlist entry is retained to avoid user data loss.
7905      */
7906     private void resolvePlaylistMembers(@NonNull Uri playlistUri) {
7907         Trace.beginSection("MP.resolvePlaylistMembers");
7908         try {
7909             final DatabaseHelper helper;
7910             try {
7911                 helper = getDatabaseForUri(playlistUri);
7912             } catch (VolumeNotFoundException e) {
7913                 throw e.rethrowAsIllegalArgumentException();
7914             }
7915 
7916             helper.runWithTransaction((db) -> {
7917                 resolvePlaylistMembersInternal(playlistUri, db);
7918                 return null;
7919             });
7920         } finally {
7921             Trace.endSection();
7922         }
7923     }
7924 
7925     private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri,
7926             @NonNull SQLiteDatabase db) {
7927         try {
7928             // Refresh playlist members based on what we parse from disk
7929             final long playlistId = ContentUris.parseId(playlistUri);
7930             final Map<String, Long> membersMap = getAllPlaylistMembers(playlistId);
7931             db.delete("audio_playlists_map", "playlist_id=" + playlistId, null);
7932 
7933             final Path playlistPath = queryForDataFile(playlistUri, null).toPath();
7934             final Playlist playlist = new Playlist();
7935             playlist.read(playlistPath.toFile());
7936 
7937             final List<Path> members = playlist.asList();
7938             for (int i = 0; i < members.size(); i++) {
7939                 try {
7940                     final Path audioPath = playlistPath.getParent().resolve(members.get(i));
7941                     final long audioId = queryForPlaylistMember(audioPath, membersMap);
7942 
7943                     final ContentValues values = new ContentValues();
7944                     values.put(Playlists.Members.PLAY_ORDER, i + 1);
7945                     values.put(Playlists.Members.PLAYLIST_ID, playlistId);
7946                     values.put(Playlists.Members.AUDIO_ID, audioId);
7947                     db.insert("audio_playlists_map", null, values);
7948                 } catch (IOException e) {
7949                     Log.w(TAG, "Failed to resolve playlist member", e);
7950                 }
7951             }
7952         } catch (IOException e) {
7953             Log.w(TAG, "Failed to refresh playlist", e);
7954         }
7955     }
7956 
7957     private Map<String, Long> getAllPlaylistMembers(long playlistId) {
7958         final Map<String, Long> membersMap = new ArrayMap<>();
7959 
7960         final Uri uri = Playlists.Members.getContentUri(MediaStore.VOLUME_EXTERNAL, playlistId);
7961         final String[] projection = new String[] {
7962                 Playlists.Members.DATA,
7963                 Playlists.Members.AUDIO_ID
7964         };
7965         try (Cursor c = query(uri, projection, null, null)) {
7966             if (c == null) {
7967                 Log.e(TAG, "Cursor is null, failed to create cached playlist member info.");
7968                 return membersMap;
7969             }
7970             while (c.moveToNext()) {
7971                 membersMap.put(c.getString(0), c.getLong(1));
7972             }
7973         }
7974         return membersMap;
7975     }
7976 
7977     /**
7978      * Make two attempts to query this playlist member: first based on the exact
7979      * path, and if that fails, fall back to picking a single item matching the
7980      * display name. When there are multiple items with the same display name,
7981      * we can't resolve between them, and leave this member unresolved.
7982      */
7983     private long queryForPlaylistMember(@NonNull Path path, @NonNull Map<String, Long> membersMap)
7984             throws IOException {
7985         final String data = path.toFile().getCanonicalPath();
7986         if (membersMap.containsKey(data)) {
7987             return membersMap.get(data);
7988         }
7989         final Uri audioUri = Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
7990         try (Cursor c = queryForSingleItem(audioUri,
7991                 new String[] { BaseColumns._ID }, MediaColumns.DATA + "=?",
7992                 new String[] { data }, null)) {
7993             return c.getLong(0);
7994         } catch (FileNotFoundException ignored) {
7995         }
7996         try (Cursor c = queryForSingleItem(audioUri,
7997                 new String[] { BaseColumns._ID }, MediaColumns.DISPLAY_NAME + "=?",
7998                 new String[] { path.toFile().getName() }, null)) {
7999             return c.getLong(0);
8000         } catch (FileNotFoundException ignored) {
8001         }
8002         throw new FileNotFoundException();
8003     }
8004 
8005     /**
8006      * Add the given audio item to the given playlist. Defaults to adding at the
8007      * end of the playlist when no {@link Playlists.Members#PLAY_ORDER} is
8008      * defined.
8009      */
8010     private long addPlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values)
8011             throws FallbackException {
8012         final long audioId = values.getAsLong(Audio.Playlists.Members.AUDIO_ID);
8013         final String volumeName = MediaStore.VOLUME_INTERNAL.equals(getVolumeName(playlistUri))
8014                 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL;
8015         final Uri audioUri = Audio.Media.getContentUri(volumeName, audioId);
8016 
8017         Integer playOrder = values.getAsInteger(Playlists.Members.PLAY_ORDER);
8018         playOrder = (playOrder != null) ? (playOrder - 1) : Integer.MAX_VALUE;
8019 
8020         try {
8021             final File playlistFile = queryForDataFile(playlistUri, null);
8022             final File audioFile = queryForDataFile(audioUri, null);
8023 
8024             final Playlist playlist = new Playlist();
8025             playlist.read(playlistFile);
8026             playOrder = playlist.add(playOrder,
8027                     playlistFile.toPath().getParent().relativize(audioFile.toPath()));
8028             playlist.write(playlistFile);
8029             invalidateFuseDentry(playlistFile);
8030 
8031             resolvePlaylistMembers(playlistUri);
8032 
8033             // Callers are interested in the actual ID we generated
8034             final Uri membersUri = Playlists.Members.getContentUri(volumeName,
8035                     ContentUris.parseId(playlistUri));
8036             try (Cursor c = query(membersUri, new String[] { BaseColumns._ID },
8037                     Playlists.Members.PLAY_ORDER + "=" + (playOrder + 1), null, null)) {
8038                 c.moveToFirst();
8039                 return c.getLong(0);
8040             }
8041         } catch (IOException e) {
8042             throw new FallbackException("Failed to update playlist", e,
8043                     android.os.Build.VERSION_CODES.R);
8044         }
8045     }
8046 
8047     private int addPlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues[] initialValues)
8048             throws FallbackException {
8049         final String volumeName = getVolumeName(playlistUri);
8050         final String audioVolumeName =
8051                 MediaStore.VOLUME_INTERNAL.equals(volumeName)
8052                         ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL;
8053 
8054         try {
8055             final File playlistFile = queryForDataFile(playlistUri, null);
8056             final Playlist playlist = new Playlist();
8057             playlist.read(playlistFile);
8058 
8059             for (ContentValues values : initialValues) {
8060                 final long audioId = values.getAsLong(Audio.Playlists.Members.AUDIO_ID);
8061                 final Uri audioUri = Audio.Media.getContentUri(audioVolumeName, audioId);
8062                 final File audioFile = queryForDataFile(audioUri, null);
8063 
8064                 Integer playOrder = values.getAsInteger(Playlists.Members.PLAY_ORDER);
8065                 playOrder = (playOrder != null) ? (playOrder - 1) : Integer.MAX_VALUE;
8066                 playlist.add(playOrder,
8067                         playlistFile.toPath().getParent().relativize(audioFile.toPath()));
8068             }
8069             playlist.write(playlistFile);
8070 
8071             resolvePlaylistMembers(playlistUri);
8072         } catch (IOException e) {
8073             throw new FallbackException("Failed to update playlist", e,
8074                     android.os.Build.VERSION_CODES.R);
8075         }
8076 
8077         return initialValues.length;
8078     }
8079 
8080     /**
8081      * Move an audio item within the given playlist.
8082      */
8083     private int movePlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values,
8084             @NonNull Bundle queryArgs) throws FallbackException {
8085         final int fromIndex = resolvePlaylistIndex(playlistUri, queryArgs);
8086         final int toIndex = values.getAsInteger(Playlists.Members.PLAY_ORDER) - 1;
8087         if (fromIndex == -1) {
8088             throw new FallbackException("Failed to resolve playlist member " + queryArgs,
8089                     android.os.Build.VERSION_CODES.R);
8090         }
8091         try {
8092             final File playlistFile = queryForDataFile(playlistUri, null);
8093 
8094             final Playlist playlist = new Playlist();
8095             playlist.read(playlistFile);
8096             final int finalIndex = playlist.move(fromIndex, toIndex);
8097             playlist.write(playlistFile);
8098             invalidateFuseDentry(playlistFile);
8099 
8100             resolvePlaylistMembers(playlistUri);
8101             return finalIndex;
8102         } catch (IOException e) {
8103             throw new FallbackException("Failed to update playlist", e,
8104                     android.os.Build.VERSION_CODES.R);
8105         }
8106     }
8107 
8108     /**
8109      * Removes an audio item or multiple audio items(if targetSDK<R) from the given playlist.
8110      */
8111     private int removePlaylistMembers(@NonNull Uri playlistUri, @NonNull Bundle queryArgs)
8112             throws FallbackException {
8113         final int[] indexes = resolvePlaylistIndexes(playlistUri, queryArgs);
8114         try {
8115             final File playlistFile = queryForDataFile(playlistUri, null);
8116 
8117             final Playlist playlist = new Playlist();
8118             playlist.read(playlistFile);
8119             final int count;
8120             if (indexes.length == 0) {
8121                 // This means either no playlist members match the query or VolumeNotFoundException
8122                 // was thrown. So we don't have anything to delete.
8123                 count = 0;
8124             } else {
8125                 count = playlist.removeMultiple(indexes);
8126             }
8127             playlist.write(playlistFile);
8128             invalidateFuseDentry(playlistFile);
8129 
8130             resolvePlaylistMembers(playlistUri);
8131             return count;
8132         } catch (IOException e) {
8133             throw new FallbackException("Failed to update playlist", e,
8134                     android.os.Build.VERSION_CODES.R);
8135         }
8136     }
8137 
8138     /**
8139      * Remove an audio item from the given playlist since the playlist file or the audio file is
8140      * already removed.
8141      */
8142     private void removePlaylistMembers(int mediaType, long id) {
8143         final DatabaseHelper helper;
8144         try {
8145             helper = getDatabaseForUri(Audio.Media.EXTERNAL_CONTENT_URI);
8146         } catch (VolumeNotFoundException e) {
8147             Log.w(TAG, e);
8148             return;
8149         }
8150 
8151         helper.runWithTransaction((db) -> {
8152             final String where;
8153             if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
8154                 where = "playlist_id=?";
8155             } else {
8156                 where = "audio_id=?";
8157             }
8158             db.delete("audio_playlists_map", where, new String[] { "" + id });
8159             return null;
8160         });
8161     }
8162 
8163     /**
8164      * Resolve query arguments that are designed to select specific playlist
8165      * items using the playlist's {@link Playlists.Members#PLAY_ORDER}.
8166      *
8167      * @return an array of the indexes that match the query.
8168      */
8169     private int[] resolvePlaylistIndexes(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) {
8170         final Uri membersUri = Playlists.Members.getContentUri(
8171                 getVolumeName(playlistUri), ContentUris.parseId(playlistUri));
8172 
8173         final DatabaseHelper helper;
8174         final SQLiteQueryBuilder qb;
8175         try {
8176             helper = getDatabaseForUri(membersUri);
8177             qb = getQueryBuilder(TYPE_DELETE, AUDIO_PLAYLISTS_ID_MEMBERS,
8178                     membersUri, queryArgs, null);
8179         } catch (VolumeNotFoundException ignored) {
8180             return new int[0];
8181         }
8182 
8183         try (Cursor c = qb.query(helper,
8184                 new String[] { Playlists.Members.PLAY_ORDER }, queryArgs, null)) {
8185             if ((c.getCount() >= 1) && c.moveToFirst()) {
8186                 int size = c.getCount();
8187                 int[] res = new int[size];
8188                 for (int i = 0; i < size; ++i) {
8189                     res[i] = c.getInt(0) - 1;
8190                     c.moveToNext();
8191                 }
8192                 return res;
8193             } else {
8194                 // Cursor size is 0
8195                 return new int[0];
8196             }
8197         }
8198     }
8199 
8200     /**
8201      * Resolve query arguments that are designed to select a specific playlist
8202      * item using its {@link Playlists.Members#PLAY_ORDER}.
8203      *
8204      * @return if there's only 1 item that matches the query, returns its index. Returns -1
8205      * otherwise.
8206      */
8207     private int resolvePlaylistIndex(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) {
8208         int[] indexes = resolvePlaylistIndexes(playlistUri, queryArgs);
8209         if (indexes.length == 1) {
8210             return indexes[0];
8211         }
8212         return -1;
8213     }
8214 
8215     private boolean isPickerUri(Uri uri) {
8216         final int match = matchUri(uri, /* allowHidden */ isCallingPackageAllowedHidden());
8217         return match == PICKER_ID;
8218     }
8219 
8220     @Override
8221     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
8222         return openFileCommon(uri, mode, /*signal*/ null, /*opts*/ null);
8223     }
8224 
8225     @Override
8226     public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
8227             throws FileNotFoundException {
8228         return openFileCommon(uri, mode, signal, /*opts*/ null);
8229     }
8230 
8231     private ParcelFileDescriptor openFileCommon(Uri uri, String mode, CancellationSignal signal,
8232             @Nullable Bundle opts)
8233             throws FileNotFoundException {
8234         opts = opts == null ? new Bundle() : opts;
8235         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
8236         opts.remove(QUERY_ARG_REDACTED_URI);
8237         if (isRedactedUri(uri)) {
8238             opts.putParcelable(QUERY_ARG_REDACTED_URI, uri);
8239             uri = getUriForRedactedUri(uri);
8240         }
8241         uri = safeUncanonicalize(uri);
8242 
8243         if (isPickerUri(uri)) {
8244             final int callingPid = mCallingIdentity.get().pid;
8245             final int callingUid = mCallingIdentity.get().uid;
8246             return mPickerUriResolver.openFile(uri, mode, signal, callingPid, callingUid);
8247         }
8248 
8249         final boolean allowHidden = isCallingPackageAllowedHidden();
8250         final int match = matchUri(uri, allowHidden);
8251         final String volumeName = getVolumeName(uri);
8252 
8253         // Handle some legacy cases where we need to redirect thumbnails
8254         try {
8255             switch (match) {
8256                 case AUDIO_ALBUMART_ID: {
8257                     final long albumId = Long.parseLong(uri.getPathSegments().get(3));
8258                     final Uri targetUri = ContentUris
8259                             .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId);
8260                     return ensureThumbnail(targetUri, signal);
8261                 }
8262                 case AUDIO_ALBUMART_FILE_ID: {
8263                     final long audioId = Long.parseLong(uri.getPathSegments().get(3));
8264                     final Uri targetUri = ContentUris
8265                             .withAppendedId(Audio.Media.getContentUri(volumeName), audioId);
8266                     return ensureThumbnail(targetUri, signal);
8267                 }
8268                 case VIDEO_MEDIA_ID_THUMBNAIL: {
8269                     final long videoId = Long.parseLong(uri.getPathSegments().get(3));
8270                     final Uri targetUri = ContentUris
8271                             .withAppendedId(Video.Media.getContentUri(volumeName), videoId);
8272                     return ensureThumbnail(targetUri, signal);
8273                 }
8274                 case IMAGES_MEDIA_ID_THUMBNAIL: {
8275                     final long imageId = Long.parseLong(uri.getPathSegments().get(3));
8276                     final Uri targetUri = ContentUris
8277                             .withAppendedId(Images.Media.getContentUri(volumeName), imageId);
8278                     return ensureThumbnail(targetUri, signal);
8279                 }
8280                 case CLI: {
8281                     // Command Line Interface "file" - content://media/cli - may be opened only by
8282                     // Shell (or Root).
8283                     if (!isCallingPackageShell()) {
8284                         throw new SecurityException("Only shell (or root) is allowed to open "
8285                                 + "MediaProvider's CLI file (" + uri + ')');
8286                     }
8287 
8288                     // We expect the uri's query to hold a single parameter - "cmd" - which contains
8289                     // the command name followed by the arguments (if any), all joined with '+'
8290                     // symbols:
8291                     // ?cmd=command[+arg1+[arg2+[arg3...]]]
8292                     //
8293                     // For example:
8294                     // (1) ?cmd=version
8295                     // (2) ?cmd=set-cloud-provider=com.my.cloud.provider.authority
8296                     //
8297                     // We retrieve the command name and the argument (if any) with
8298                     // uri.getQueryParameter("cmd") call, which will replace all '+' delimiters with
8299                     // spaces.
8300 
8301                     final String[] cmdAndArgs = uri.getQueryParameter("cmd").split("\\s+");
8302                     Log.d(TAG, "MediaProvider CLI command: " + Arrays.toString(cmdAndArgs));
8303                     try {
8304                         // Create a UNIX pipe.
8305                         final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
8306                         // Pass the write end - pipe[1] - to our shell command.
8307                         final var cmd = new MediaProviderShellCommand(getContext(),
8308                                 mConfigStore,
8309                                 mPickerSyncController,
8310                                 /* out */ pipe[1]);
8311                         cmd.exec(cmdAndArgs);
8312                         // Return the read end - pipe[0] - to the caller.
8313                         return pipe[0];
8314                     } catch (IOException e) {
8315                         Log.e(TAG, "Could not create a pipe", e);
8316                         return null;
8317                     }
8318                 }
8319             }
8320         } finally {
8321             // We have to log separately here because openFileAndEnforcePathPermissionsHelper calls
8322             // a public MediaProvider API and so logs the access there.
8323             PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
8324         }
8325 
8326         return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal, opts);
8327     }
8328 
8329     @Override
8330     public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
8331             throws FileNotFoundException {
8332         return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, null);
8333     }
8334 
8335     @Override
8336     public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
8337             CancellationSignal signal) throws FileNotFoundException {
8338         return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, signal);
8339     }
8340 
8341     private AssetFileDescriptor openTypedAssetFileCommon(Uri uri, String mimeTypeFilter,
8342             Bundle opts, CancellationSignal signal) throws FileNotFoundException {
8343         final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE)
8344                 && StringUtils.startsWithIgnoreCase(mimeTypeFilter, "image/");
8345         String mode = "r";
8346 
8347         // If request is not for thumbnail and arising from MediaProvider, then check for EXTRA_MODE
8348         if (opts != null && !wantsThumb && isCallingPackageSelf()) {
8349             mode = opts.getString(MediaStore.EXTRA_MODE, "r");
8350         } else if (opts != null) {
8351             opts.remove(MediaStore.EXTRA_MODE);
8352         }
8353 
8354         if (opts != null && opts.containsKey(MediaStore.EXTRA_FILE_DESCRIPTOR)) {
8355             // This is called as part of MediaStore#getOriginalMediaFormatFileDescriptor
8356             // We don't need to use the |uri| because the input fd already identifies the file and
8357             // we actually don't have a valid URI, we are going to identify the file via the fd.
8358             // While identifying the file, we also perform the following security checks.
8359             // 1. Find the FUSE file with the associated inode
8360             // 2. Verify that the binder caller opened it
8361             // 3. Verify the access level the fd is opened with (r/w)
8362             // 4. Open the original (non-transcoded) file *with* redaction enabled and the access
8363             // level from #3
8364             // 5. Return the fd from #4 to the app or throw an exception if any of the conditions
8365             // are not met
8366             try {
8367                 return getOriginalMediaFormatFileDescriptor(opts);
8368             } finally {
8369                 // Clearing the Bundle closes the underlying Parcel, ensuring that the input fd
8370                 // owned by the Parcel is closed immediately and not at the next GC.
8371                 // This works around a change in behavior introduced by:
8372                 // aosp/Icfe8880cad00c3cd2afcbe4b92400ad4579e680e
8373                 opts.clear();
8374             }
8375         }
8376 
8377         // This is needed for thumbnail resolution as it doesn't go through openFileCommon
8378         if (isPickerUri(uri)) {
8379             final int callingPid = mCallingIdentity.get().pid;
8380             final int callingUid = mCallingIdentity.get().uid;
8381             return mPickerUriResolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal,
8382                     callingPid, callingUid);
8383         }
8384 
8385         // TODO: enforce that caller has access to this uri
8386 
8387         // Offer thumbnail of media, when requested
8388         if (wantsThumb) {
8389             final ParcelFileDescriptor pfd = ensureThumbnail(uri, signal);
8390             return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
8391         }
8392 
8393         // Worst case, return the underlying file
8394         return new AssetFileDescriptor(openFileCommon(uri, mode, signal, opts), 0,
8395                 AssetFileDescriptor.UNKNOWN_LENGTH);
8396     }
8397 
8398     private ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
8399             throws FileNotFoundException {
8400         final boolean allowHidden = isCallingPackageAllowedHidden();
8401         final int match = matchUri(uri, allowHidden);
8402 
8403         Trace.beginSection("MP.ensureThumbnail");
8404         final LocalCallingIdentity token = clearLocalCallingIdentity();
8405         try {
8406             switch (match) {
8407                 case AUDIO_ALBUMS_ID: {
8408                     final String volumeName = MediaStore.getVolumeName(uri);
8409                     final Uri baseUri = MediaStore.Audio.Media.getContentUri(volumeName);
8410                     final long albumId = ContentUris.parseId(uri);
8411                     try (Cursor c = query(baseUri, new String[] { MediaStore.Audio.Media._ID },
8412                             MediaStore.Audio.Media.ALBUM_ID + "=" + albumId, null, null, signal)) {
8413                         if (c.moveToFirst()) {
8414                             final long audioId = c.getLong(0);
8415                             final Uri targetUri = ContentUris.withAppendedId(baseUri, audioId);
8416                             return mAudioThumbnailer.ensureThumbnail(targetUri, signal);
8417                         } else {
8418                             throw new FileNotFoundException("No media for album " + uri);
8419                         }
8420                     }
8421                 }
8422                 case AUDIO_MEDIA_ID:
8423                     return mAudioThumbnailer.ensureThumbnail(uri, signal);
8424                 case VIDEO_MEDIA_ID:
8425                     return mVideoThumbnailer.ensureThumbnail(uri, signal);
8426                 case IMAGES_MEDIA_ID:
8427                     return mImageThumbnailer.ensureThumbnail(uri, signal);
8428                 case FILES_ID:
8429                 case DOWNLOADS_ID: {
8430                     // When item is referenced in a generic way, resolve to actual type
8431                     final int mediaType = MimeUtils.resolveMediaType(getType(uri));
8432                     switch (mediaType) {
8433                         case FileColumns.MEDIA_TYPE_AUDIO:
8434                             return mAudioThumbnailer.ensureThumbnail(uri, signal);
8435                         case FileColumns.MEDIA_TYPE_VIDEO:
8436                             return mVideoThumbnailer.ensureThumbnail(uri, signal);
8437                         case FileColumns.MEDIA_TYPE_IMAGE:
8438                             return mImageThumbnailer.ensureThumbnail(uri, signal);
8439                         default:
8440                             throw new FileNotFoundException();
8441                     }
8442                 }
8443                 default:
8444                     throw new FileNotFoundException();
8445             }
8446         } catch (IOException e) {
8447             Log.w(TAG, e);
8448             throw new FileNotFoundException(e.getMessage());
8449         } finally {
8450             restoreLocalCallingIdentity(token);
8451             Trace.endSection();
8452         }
8453     }
8454 
8455     /**
8456      * Update the metadata columns for the image residing at given {@link Uri}
8457      * by reading data from the underlying image.
8458      */
8459     private void updateImageMetadata(ContentValues values, File file) {
8460         final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options();
8461         bitmapOpts.inJustDecodeBounds = true;
8462         BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOpts);
8463 
8464         values.put(MediaColumns.WIDTH, bitmapOpts.outWidth);
8465         values.put(MediaColumns.HEIGHT, bitmapOpts.outHeight);
8466     }
8467 
8468     private void handleInsertedRowForFuse(long rowId) {
8469         if (isFuseThread()) {
8470             // Removes restored row ID saved list.
8471             mCallingIdentity.get().removeDeletedRowId(rowId);
8472         }
8473     }
8474 
8475     private void handleUpdatedRowForFuse(@NonNull String oldPath, @NonNull String ownerPackage,
8476             long oldRowId, long newRowId) {
8477         if (oldRowId == newRowId) {
8478             // Update didn't delete or add row ID. We don't need to save row ID or remove saved
8479             // deleted ID.
8480             return;
8481         }
8482 
8483         handleDeletedRowForFuse(oldPath, ownerPackage, oldRowId);
8484         handleInsertedRowForFuse(newRowId);
8485     }
8486 
8487     private void handleDeletedRowForFuse(@NonNull String path, @NonNull String ownerPackage,
8488             long rowId) {
8489         if (!isFuseThread()) {
8490             return;
8491         }
8492 
8493         // Invalidate saved owned ID's of the previous owner of the deleted path, this prevents old
8494         // owner from gaining access to newly created file with restored row ID.
8495         if (!ownerPackage.equals("null") && !ownerPackage.equals(getCallingPackageOrSelf())) {
8496             invalidateLocalCallingIdentityCache(ownerPackage, "owned_database_row_deleted:"
8497                     + path);
8498         }
8499         // Saves row ID corresponding to deleted path. Saved row ID will be restored on subsequent
8500         // create or rename.
8501         mCallingIdentity.get().addDeletedRowId(path, rowId);
8502     }
8503 
8504     private void handleOwnerPackageNameChange(@NonNull String oldPath,
8505             @NonNull String oldOwnerPackage, @NonNull String newOwnerPackage) {
8506         if (Objects.equals(oldOwnerPackage, newOwnerPackage)) {
8507             return;
8508         }
8509         // Invalidate saved owned ID's of the previous owner of the renamed path, this prevents old
8510         // owner from gaining access to replaced file.
8511         invalidateLocalCallingIdentityCache(oldOwnerPackage, "owner_package_changed:" + oldPath);
8512     }
8513 
8514     /**
8515      * Return the {@link MediaColumns#DATA} field for the given {@code Uri}.
8516      */
8517     File queryForDataFile(Uri uri, CancellationSignal signal)
8518             throws FileNotFoundException {
8519         return queryForDataFile(uri, null, null, signal);
8520     }
8521 
8522     /**
8523      * Return the {@link MediaColumns#DATA} field for the given {@code Uri}.
8524      */
8525     File queryForDataFile(Uri uri, String selection, String[] selectionArgs,
8526             CancellationSignal signal) throws FileNotFoundException {
8527         try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns.DATA },
8528                 selection, selectionArgs, signal)) {
8529             final String data = cursor.getString(0);
8530             if (TextUtils.isEmpty(data)) {
8531                 throw new FileNotFoundException("Missing path for " + uri);
8532             } else {
8533                 return new File(data);
8534             }
8535         }
8536     }
8537 
8538     /**
8539      * Return the {@link Uri} for the given {@code File}.
8540      */
8541     Uri queryForMediaUri(File file, CancellationSignal signal) throws FileNotFoundException {
8542         final String volumeName = FileUtils.getVolumeName(getContext(), file);
8543         final Uri uri = Files.getContentUri(volumeName);
8544         try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns._ID },
8545                 MediaColumns.DATA + "=?", new String[] { file.getAbsolutePath() }, signal)) {
8546             return ContentUris.withAppendedId(uri, cursor.getLong(0));
8547         }
8548     }
8549 
8550     /**
8551      * Query the given {@link Uri} as MediaProvider, expecting only a single item to be found.
8552      *
8553      * @throws FileNotFoundException if no items were found, or multiple items
8554      *             were found, or there was trouble reading the data.
8555      */
8556     Cursor queryForSingleItemAsMediaProvider(Uri uri, String[] projection, String selection,
8557             String[] selectionArgs, CancellationSignal signal)
8558             throws FileNotFoundException {
8559         final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
8560         try {
8561             return queryForSingleItem(uri, projection, selection, selectionArgs, signal);
8562         } finally {
8563             restoreLocalCallingIdentity(tokenInner);
8564         }
8565     }
8566 
8567     /**
8568      * Query the given {@link Uri}, expecting only a single item to be found.
8569      *
8570      * @throws FileNotFoundException if no items were found, or multiple items
8571      *             were found, or there was trouble reading the data.
8572      */
8573     Cursor queryForSingleItem(Uri uri, String[] projection, String selection,
8574             String[] selectionArgs, CancellationSignal signal) throws FileNotFoundException {
8575         Cursor c = null;
8576         try {
8577             c = query(uri, projection,
8578                     DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null),
8579                     signal, true);
8580         } catch (IllegalArgumentException  e) {
8581             throw new FileNotFoundException("Volume not found for " + uri);
8582         }
8583         if (c == null) {
8584             throw new FileNotFoundException("Missing cursor for " + uri);
8585         } else if (c.getCount() < 1) {
8586             FileUtils.closeQuietly(c);
8587             throw new FileNotFoundException("No item at " + uri);
8588         } else if (c.getCount() > 1) {
8589             FileUtils.closeQuietly(c);
8590             throw new FileNotFoundException("Multiple items at " + uri);
8591         }
8592 
8593         if (c.moveToFirst()) {
8594             return c;
8595         } else {
8596             FileUtils.closeQuietly(c);
8597             throw new FileNotFoundException("Failed to read row from " + uri);
8598         }
8599     }
8600 
8601     /**
8602      * Compares {@code itemOwner} with package name of {@link LocalCallingIdentity} and throws
8603      * {@link IllegalStateException} if it doesn't match.
8604      * Make sure to set calling identity properly before calling.
8605      */
8606     private void requireOwnershipForItem(@Nullable String itemOwner, Uri item) {
8607         final boolean hasOwner = (itemOwner != null);
8608         final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), itemOwner);
8609         if (hasOwner && !callerIsOwner) {
8610             throw new IllegalStateException(
8611                     "Only owner is able to interact with pending/trashed item " + item);
8612         }
8613     }
8614 
8615     private ParcelFileDescriptor openWithFuse(String filePath, int uid, int mediaCapabilitiesUid,
8616             int modeBits, boolean shouldRedact, boolean shouldTranscode, int transcodeReason)
8617             throws FileNotFoundException {
8618         Log.d(TAG, "Open with FUSE. FilePath: " + filePath
8619                 + ". Uid: " + uid
8620                 + ". Media Capabilities Uid: " + mediaCapabilitiesUid
8621                 + ". ShouldRedact: " + shouldRedact
8622                 + ". ShouldTranscode: " + shouldTranscode);
8623 
8624         int tid = android.os.Process.myTid();
8625         synchronized (mPendingOpenInfo) {
8626             mPendingOpenInfo.put(tid,
8627                     new PendingOpenInfo(uid, mediaCapabilitiesUid, shouldRedact, transcodeReason));
8628         }
8629 
8630         try {
8631             return FileUtils.openSafely(toFuseFile(new File(filePath)), modeBits);
8632         } finally {
8633             synchronized (mPendingOpenInfo) {
8634                 mPendingOpenInfo.remove(tid);
8635             }
8636         }
8637     }
8638 
8639     /**
8640      * @return {@link FuseDaemon} corresponding to a given file
8641      */
8642     @NonNull
8643     public static FuseDaemon getFuseDaemonForFile(@NonNull File file, VolumeCache volumeCache)
8644             throws FileNotFoundException {
8645         final FuseDaemon daemon = ExternalStorageServiceImpl.getFuseDaemon(
8646                 volumeCache.getVolumeId(file));
8647         if (daemon == null) {
8648             throw new FileNotFoundException("Missing FUSE daemon for " + file);
8649         } else {
8650             return daemon;
8651         }
8652     }
8653 
8654     @NonNull
8655     public static FuseDaemon getFuseDaemonForFileWithWait(@NonNull File file,
8656             VolumeCache volumeCache, long waitTimeInMilliseconds) throws FileNotFoundException {
8657         FuseDaemon fuseDaemon = null;
8658         long time = 0;
8659         while (time < waitTimeInMilliseconds) {
8660             fuseDaemon = ExternalStorageServiceImpl.getFuseDaemon(
8661                     volumeCache.getVolumeId(file));
8662             if (fuseDaemon != null) {
8663                 break;
8664             }
8665             SystemClock.sleep(POLLING_TIME_IN_MILLIS);
8666             time += POLLING_TIME_IN_MILLIS;
8667         }
8668 
8669         if (fuseDaemon == null) {
8670             throw new FileNotFoundException("Missing FUSE daemon for " + file);
8671         } else {
8672             return fuseDaemon;
8673         }
8674     }
8675 
8676     private void invalidateFuseDentry(@NonNull File file) {
8677         invalidateFuseDentry(file.getAbsolutePath());
8678     }
8679 
8680     private void invalidateFuseDentry(@NonNull String path) {
8681         try {
8682             final FuseDaemon daemon = getFuseDaemonForFile(new File(path), mVolumeCache);
8683             if (isFuseThread()) {
8684                 // If we are on a FUSE thread, we don't need to invalidate,
8685                 // (and *must* not, otherwise we'd crash) because the invalidation
8686                 // is already reflected in the lower filesystem
8687                 return;
8688             } else {
8689                 daemon.invalidateFuseDentryCache(path);
8690             }
8691         } catch (FileNotFoundException e) {
8692             Log.w(TAG, "Failed to invalidate FUSE dentry", e);
8693         }
8694     }
8695 
8696     /**
8697      * Replacement for {@link #openFileHelper(Uri, String)} which enforces any
8698      * permissions applicable to the path before returning.
8699      *
8700      * <p>This function should never be called from the fuse thread since it tries to open
8701      * a "/mnt/user" path.
8702      */
8703     private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match,
8704             String mode, CancellationSignal signal, @NonNull Bundle opts)
8705             throws FileNotFoundException {
8706         int modeBits = ParcelFileDescriptor.parseMode(mode);
8707         boolean forWrite = (modeBits & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0;
8708         final Uri redactedUri = opts.getParcelable(QUERY_ARG_REDACTED_URI);
8709         if (forWrite) {
8710             if (redactedUri != null) {
8711                 throw new UnsupportedOperationException(
8712                         "Write is not supported on " + redactedUri.toString());
8713             }
8714             // Upgrade 'w' only to 'rw'. This allows us acquire a WR_LOCK when calling
8715             // #shouldOpenWithFuse
8716             modeBits |= ParcelFileDescriptor.MODE_READ_WRITE;
8717         }
8718 
8719         final boolean hasOwnerPackageName = hasOwnerPackageName(uri);
8720         final String[] projection = new String[] {
8721                 MediaColumns.DATA,
8722                 hasOwnerPackageName ? MediaColumns.OWNER_PACKAGE_NAME : "NULL",
8723                 hasOwnerPackageName ? MediaColumns.IS_PENDING : "0",
8724         };
8725 
8726         final File file;
8727         final String ownerPackageName;
8728         final boolean isPending;
8729         final LocalCallingIdentity token = clearLocalCallingIdentity();
8730         try (Cursor c = queryForSingleItem(uri, projection, null, null, signal)) {
8731             final String data = c.getString(0);
8732             if (TextUtils.isEmpty(data)) {
8733                 throw new FileNotFoundException("Missing path for " + uri);
8734             } else {
8735                 file = new File(data).getCanonicalFile();
8736             }
8737             ownerPackageName = c.getString(1);
8738             isPending = c.getInt(2) != 0;
8739         } catch (IOException e) {
8740             throw new FileNotFoundException(e.toString());
8741         } finally {
8742             restoreLocalCallingIdentity(token);
8743         }
8744 
8745         if (redactedUri == null) {
8746             checkAccess(uri, Bundle.EMPTY, file, forWrite);
8747         } else {
8748             checkAccess(redactedUri, Bundle.EMPTY, file, false);
8749         }
8750 
8751         // We don't check ownership for files with IS_PENDING set by FUSE
8752         if (isPending && !isPendingFromFuse(file)) {
8753             requireOwnershipForItem(ownerPackageName, uri);
8754         }
8755 
8756         // Figure out if we need to redact contents
8757         final boolean redactionNeeded = isRedactionNeededForOpenViaContentResolver(redactedUri,
8758                 ownerPackageName, file);
8759         final RedactionInfo redactionInfo;
8760         try {
8761             redactionInfo = redactionNeeded ? getRedactionRanges(file)
8762                     : new RedactionInfo(new long[0], new long[0]);
8763         } catch (IOException e) {
8764             throw new IllegalStateException(e);
8765         }
8766 
8767         // Yell if caller requires original, since we can't give it to them
8768         // unless they have access granted above
8769         if (redactionNeeded && MediaStore.getRequireOriginal(uri)) {
8770             throw new UnsupportedOperationException(
8771                     "Caller must hold ACCESS_MEDIA_LOCATION permission to access original");
8772         }
8773 
8774         // Kick off metadata update when writing is finished
8775         final OnCloseListener listener = (e) -> {
8776             // We always update metadata to reflect the state on disk, even when
8777             // the remote writer tried claiming an exception
8778             invalidateThumbnails(uri);
8779 
8780             // Invalidate so subsequent stat(2) on the upper fs is eventually consistent
8781             invalidateFuseDentry(file);
8782             try {
8783                 switch (match) {
8784                     case IMAGES_THUMBNAILS_ID:
8785                     case VIDEO_THUMBNAILS_ID:
8786                         final ContentValues values = new ContentValues();
8787                         updateImageMetadata(values, file);
8788                         update(uri, values, null, null);
8789                         break;
8790                     default:
8791                         scanFileAsMediaProvider(file, REASON_DEMAND);
8792                         break;
8793                 }
8794             } catch (Exception e2) {
8795                 Log.w(TAG, "Failed to update metadata for " + uri, e2);
8796             }
8797         };
8798 
8799         try {
8800             // First, handle any redaction that is needed for caller
8801             final ParcelFileDescriptor pfd;
8802             final String filePath = file.getPath();
8803             final int uid = Binder.getCallingUid();
8804             final int transcodeReason = mTranscodeHelper.shouldTranscode(filePath, uid, opts);
8805             final boolean shouldTranscode = transcodeReason > 0;
8806             int mediaCapabilitiesUid = opts.getInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID);
8807             if (!shouldTranscode || mediaCapabilitiesUid < Process.FIRST_APPLICATION_UID) {
8808                 // Although 0 is a valid UID, it's not a valid app uid.
8809                 // So, we use it to signify that mediaCapabilitiesUid is not set.
8810                 mediaCapabilitiesUid = 0;
8811             }
8812             if (redactionInfo.redactionRanges.length > 0) {
8813                 // If fuse is enabled, we can provide an fd that points to the fuse
8814                 // file system and handle redaction in the fuse handler when the caller reads.
8815                 pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits,
8816                         true /* shouldRedact */, shouldTranscode, transcodeReason);
8817             } else if (shouldTranscode) {
8818                 pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits,
8819                         false /* shouldRedact */, shouldTranscode, transcodeReason);
8820             } else {
8821                 FuseDaemon daemon = null;
8822                 try {
8823                     daemon = getFuseDaemonForFile(file, mVolumeCache);
8824                 } catch (FileNotFoundException ignored) {
8825                 }
8826                 ParcelFileDescriptor lowerFsFd = FileUtils.openSafely(file, modeBits);
8827                 // Always acquire a readLock. This allows us make multiple opens via lower
8828                 // filesystem
8829                 boolean shouldOpenWithFuse = daemon != null
8830                         && daemon.shouldOpenWithFuse(filePath, true /* forRead */,
8831                         lowerFsFd.getFd());
8832 
8833                 if (shouldOpenWithFuse) {
8834                     // If the file is already opened on the FUSE mount with VFS caching enabled
8835                     // we return an upper filesystem fd (via FUSE) to avoid file corruption
8836                     // resulting from cache inconsistencies between the upper and lower
8837                     // filesystem caches
8838                     pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits,
8839                             false /* shouldRedact */, shouldTranscode, transcodeReason);
8840                     try {
8841                         lowerFsFd.close();
8842                     } catch (IOException e) {
8843                         Log.w(TAG, "Failed to close lower filesystem fd " + file.getPath(), e);
8844                     }
8845                 } else {
8846                     Log.i(TAG, "Open with lower FS for " + filePath + ". Uid: " + uid);
8847                     if (forWrite) {
8848                         // When opening for write on the lower filesystem, invalidate the VFS dentry
8849                         // so subsequent open/getattr calls will return correctly.
8850                         //
8851                         // A 'dirty' dentry with write back cache enabled can cause the kernel to
8852                         // ignore file attributes or even see stale page cache data when the lower
8853                         // filesystem has been modified outside of the FUSE driver
8854                         invalidateFuseDentry(file);
8855                     }
8856 
8857                     pfd = lowerFsFd;
8858                 }
8859             }
8860 
8861             // Second, wrap in any listener that we've requested
8862             if (!isPending && forWrite) {
8863                 return ParcelFileDescriptor.wrap(pfd, BackgroundThread.getHandler(), listener);
8864             } else {
8865                 return pfd;
8866             }
8867         } catch (IOException e) {
8868             if (e instanceof FileNotFoundException) {
8869                 throw (FileNotFoundException) e;
8870             } else {
8871                 throw new IllegalStateException(e);
8872             }
8873         }
8874     }
8875 
8876     private boolean isRedactionNeededForOpenViaContentResolver(Uri redactedUri,
8877             String ownerPackageName, File file) {
8878         // Redacted Uris should always redact information
8879         if (redactedUri != null) {
8880             return true;
8881         }
8882 
8883         final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName);
8884         if (callerIsOwner) {
8885             return false;
8886         }
8887 
8888         // To be consistent with FUSE redaction checks we allow similar access for File Manager
8889         // and System Gallery apps.
8890         if (isCallingPackageManager() || canSystemGalleryAccessTheFile(file.getPath())) {
8891             return false;
8892         }
8893 
8894         return isRedactionNeeded();
8895     }
8896 
8897     private void deleteAndInvalidate(@NonNull Path path) {
8898         deleteAndInvalidate(path.toFile());
8899     }
8900 
8901     private void deleteAndInvalidate(@NonNull File file) {
8902         file.delete();
8903         invalidateFuseDentry(file);
8904     }
8905 
8906     private void deleteIfAllowed(Uri uri, Bundle extras, String path) {
8907         try {
8908             final File file = new File(path).getCanonicalFile();
8909             checkAccess(uri, extras, file, true);
8910             deleteAndInvalidate(file);
8911         } catch (Exception e) {
8912             Log.e(TAG, "Couldn't delete " + path, e);
8913         }
8914     }
8915 
8916     @Deprecated
8917     private boolean isPending(Uri uri) {
8918         final int match = matchUri(uri, true);
8919         switch (match) {
8920             case AUDIO_MEDIA_ID:
8921             case VIDEO_MEDIA_ID:
8922             case IMAGES_MEDIA_ID:
8923                 try (Cursor c = queryForSingleItem(uri,
8924                         new String[] { MediaColumns.IS_PENDING }, null, null, null)) {
8925                     return (c.getInt(0) != 0);
8926                 } catch (FileNotFoundException e) {
8927                     throw new IllegalStateException(e);
8928                 }
8929             default:
8930                 return false;
8931         }
8932     }
8933 
8934     @Deprecated
8935     private boolean isRedactionNeeded(Uri uri) {
8936         return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED);
8937     }
8938 
8939     private boolean isRedactionNeeded() {
8940         return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED);
8941     }
8942 
8943     private boolean isCallingPackageRequestingLegacy() {
8944         return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_GRANTED);
8945     }
8946 
8947     private boolean shouldBypassDatabase(int uid) {
8948         if (uid != android.os.Process.SHELL_UID && isCallingPackageManager()) {
8949             return mCallingIdentity.get().shouldBypassDatabase(false /*isSystemGallery*/);
8950         } else if (isCallingPackageSystemGallery()) {
8951             if (isCallingPackageLegacyWrite()) {
8952                 // We bypass db operations for legacy system galleries with W_E_S (see b/167307393).
8953                 // Tracking a longer term solution in b/168784136.
8954                 return true;
8955             } else if (!SdkLevel.isAtLeastS()) {
8956                 // We don't parse manifest flags for SdkLevel<=R yet. Hence, we don't bypass
8957                 // database updates for SystemGallery targeting R or above on R OS.
8958                 return false;
8959             }
8960             return mCallingIdentity.get().shouldBypassDatabase(true /*isSystemGallery*/);
8961         }
8962         return false;
8963     }
8964 
8965     private static int getFileMediaType(String path) {
8966         final File file = new File(path);
8967         final String mimeType = MimeUtils.resolveMimeType(file);
8968         return MimeUtils.resolveMediaType(mimeType);
8969     }
8970 
8971     private boolean canSystemGalleryAccessTheFile(String filePath) {
8972 
8973         if (!isCallingPackageSystemGallery()) {
8974             return false;
8975         }
8976 
8977         final int mediaType = getFileMediaType(filePath);
8978 
8979         return mediaType ==  FileColumns.MEDIA_TYPE_IMAGE ||
8980             mediaType ==  FileColumns.MEDIA_TYPE_VIDEO;
8981     }
8982 
8983     /**
8984      * Returns true if:
8985      * <ul>
8986      * <li>the calling identity is an app targeting Q or older versions AND is requesting legacy
8987      * storage and has the corresponding legacy access (read/write) permissions
8988      * <li>the calling identity holds {@code MANAGE_EXTERNAL_STORAGE}
8989      * <li>the calling identity owns or has access to the filePath (eg /Android/data/com.foo)
8990      * <li>the calling identity has permission to write images and the given file is an image file
8991      * <li>the calling identity has permission to write video and the given file is an video file
8992      * </ul>
8993      */
8994     private boolean shouldBypassFuseRestrictions(boolean forWrite, String filePath) {
8995         boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite()
8996                 : isCallingPackageLegacyRead();
8997         if (isRequestingLegacyStorage) {
8998             return true;
8999         }
9000 
9001         if (isCallingPackageManager()) {
9002             return true;
9003         }
9004 
9005         // Check if the caller has access to private app directories.
9006         if (isUidAllowedAccessToDataOrObbPathForFuse(mCallingIdentity.get().uid, filePath)) {
9007             return true;
9008         }
9009 
9010         // Apps with write access to images and/or videos can bypass our restrictions if all of the
9011         // the files they're accessing are of the compatible media type.
9012         return canSystemGalleryAccessTheFile(filePath);
9013     }
9014 
9015     /**
9016      * Returns true if the passed in path is an application-private data directory
9017      * (such as Android/data/com.foo or Android/obb/com.foo) that does not belong to the caller and
9018      * the caller does not have special access.
9019      */
9020     private boolean isPrivatePackagePathNotAccessibleByCaller(String path) {
9021         // Files under the apps own private directory
9022         final String appSpecificDir = extractPathOwnerPackageName(path);
9023 
9024         if (appSpecificDir == null) {
9025             return false;
9026         }
9027 
9028         // Android/media is not considered private, because it contains media that is explicitly
9029         // scanned and shared by other apps
9030         if (isExternalMediaDirectory(path)) {
9031             return false;
9032         }
9033         return !isUidAllowedAccessToDataOrObbPathForFuse(mCallingIdentity.get().uid, path);
9034     }
9035 
9036     private boolean shouldBypassDatabaseAndSetDirtyForFuse(int uid, String path) {
9037         if (shouldBypassDatabase(uid)) {
9038             synchronized (mNonHiddenPaths) {
9039                 File file = new File(path);
9040                 String key = file.getParent();
9041                 boolean maybeHidden = !mNonHiddenPaths.containsKey(key);
9042 
9043                 if (maybeHidden) {
9044                     File topNoMediaDir = FileUtils.getTopLevelNoMedia(new File(path));
9045                     if (topNoMediaDir == null) {
9046                         mNonHiddenPaths.put(key, 0);
9047                     } else {
9048                         mMediaScanner.onDirectoryDirty(topNoMediaDir);
9049                     }
9050                 }
9051             }
9052             return true;
9053         }
9054         return false;
9055     }
9056 
9057     /**
9058      * Set of Exif tags that should be considered for redaction.
9059      */
9060     private static final String[] REDACTED_EXIF_TAGS = new String[] {
9061             ExifInterface.TAG_GPS_ALTITUDE,
9062             ExifInterface.TAG_GPS_ALTITUDE_REF,
9063             ExifInterface.TAG_GPS_AREA_INFORMATION,
9064             ExifInterface.TAG_GPS_DOP,
9065             ExifInterface.TAG_GPS_DATESTAMP,
9066             ExifInterface.TAG_GPS_DEST_BEARING,
9067             ExifInterface.TAG_GPS_DEST_BEARING_REF,
9068             ExifInterface.TAG_GPS_DEST_DISTANCE,
9069             ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
9070             ExifInterface.TAG_GPS_DEST_LATITUDE,
9071             ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
9072             ExifInterface.TAG_GPS_DEST_LONGITUDE,
9073             ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
9074             ExifInterface.TAG_GPS_DIFFERENTIAL,
9075             ExifInterface.TAG_GPS_IMG_DIRECTION,
9076             ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
9077             ExifInterface.TAG_GPS_LATITUDE,
9078             ExifInterface.TAG_GPS_LATITUDE_REF,
9079             ExifInterface.TAG_GPS_LONGITUDE,
9080             ExifInterface.TAG_GPS_LONGITUDE_REF,
9081             ExifInterface.TAG_GPS_MAP_DATUM,
9082             ExifInterface.TAG_GPS_MEASURE_MODE,
9083             ExifInterface.TAG_GPS_PROCESSING_METHOD,
9084             ExifInterface.TAG_GPS_SATELLITES,
9085             ExifInterface.TAG_GPS_SPEED,
9086             ExifInterface.TAG_GPS_SPEED_REF,
9087             ExifInterface.TAG_GPS_STATUS,
9088             ExifInterface.TAG_GPS_TIMESTAMP,
9089             ExifInterface.TAG_GPS_TRACK,
9090             ExifInterface.TAG_GPS_TRACK_REF,
9091             ExifInterface.TAG_GPS_VERSION_ID,
9092     };
9093 
9094     /**
9095      * Set of ISO boxes that should be considered for redaction.
9096      */
9097     private static final int[] REDACTED_ISO_BOXES = new int[] {
9098             IsoInterface.BOX_LOCI,
9099             IsoInterface.BOX_XYZ,
9100             IsoInterface.BOX_GPS,
9101             IsoInterface.BOX_GPS0,
9102     };
9103 
9104     public static final Set<String> sRedactedExifTags = new ArraySet<>(
9105             Arrays.asList(REDACTED_EXIF_TAGS));
9106 
9107     private static final class RedactionInfo {
9108         public final long[] redactionRanges;
9109         public final long[] freeOffsets;
9110 
9111         public RedactionInfo() {
9112             this.redactionRanges = new long[0];
9113             this.freeOffsets = new long[0];
9114         }
9115 
9116         public RedactionInfo(long[] redactionRanges, long[] freeOffsets) {
9117             this.redactionRanges = redactionRanges;
9118             this.freeOffsets = freeOffsets;
9119         }
9120     }
9121 
9122     private static class LRUCache<K, V> extends LinkedHashMap<K, V> {
9123         private final int mMaxSize;
9124 
9125         public LRUCache(int maxSize) {
9126             this.mMaxSize = maxSize;
9127         }
9128 
9129         @Override
9130         protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
9131             return size() > mMaxSize;
9132         }
9133     }
9134 
9135     private static final class PendingOpenInfo {
9136         public final int uid;
9137         public final int mediaCapabilitiesUid;
9138         public final boolean shouldRedact;
9139         public final int transcodeReason;
9140 
9141         public PendingOpenInfo(int uid, int mediaCapabilitiesUid, boolean shouldRedact,
9142                 int transcodeReason) {
9143             this.uid = uid;
9144             this.mediaCapabilitiesUid = mediaCapabilitiesUid;
9145             this.shouldRedact = shouldRedact;
9146             this.transcodeReason = transcodeReason;
9147         }
9148     }
9149 
9150     /**
9151      * Calculates the ranges that need to be redacted for the given file and user that wants to
9152      * access the file.
9153      * Note: This method assumes that the caller of this function has already done permission checks
9154      * for the uid to access this path.
9155      *
9156      * @param uid UID of the package wanting to access the file
9157      * @param path File path
9158      * @param tid thread id making IO on the FUSE filesystem
9159      * @return Ranges that should be redacted.
9160      *
9161      * @throws IOException if an error occurs while calculating the redaction ranges
9162      */
9163     @NonNull
9164     private long[] getRedactionRangesForFuse(String path, String ioPath, int original_uid, int uid,
9165             int tid, boolean forceRedaction) throws IOException {
9166         // |ioPath| might refer to a transcoded file path (which is not indexed in the db)
9167         // |path| will always refer to a valid _data column
9168         // We use |ioPath| for the filesystem access because in the case of transcoding,
9169         // we want to get redaction ranges from the transcoded file and *not* the original file
9170         final File file = new File(ioPath);
9171 
9172         if (forceRedaction) {
9173             return getRedactionRanges(file).redactionRanges;
9174         }
9175 
9176         // When calculating redaction ranges initiated from MediaProvider, the redaction policy
9177         // is slightly different from the FUSE initiated opens redaction policy. targetSdk=29 from
9178         // MediaProvider requires redaction, but targetSdk=29 apps from FUSE don't require redaction
9179         // Hence, we check the mPendingOpenInfo object (populated when opens are initiated from
9180         // MediaProvider) if there's a pending open from MediaProvider with matching tid and uid and
9181         // use the shouldRedact decision there if there's one.
9182         synchronized (mPendingOpenInfo) {
9183             PendingOpenInfo info = mPendingOpenInfo.get(tid);
9184             if (info != null && info.uid == original_uid) {
9185                 boolean shouldRedact = info.shouldRedact;
9186                 if (shouldRedact) {
9187                     return getRedactionRanges(file).redactionRanges;
9188                 } else {
9189                     return new long[0];
9190                 }
9191             }
9192         }
9193 
9194         final LocalCallingIdentity token =
9195                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
9196         try {
9197             if (!isRedactionNeeded()
9198                     || shouldBypassFuseRestrictions(/* forWrite */ false, path)) {
9199                 return new long[0];
9200             }
9201 
9202             final Uri contentUri = FileUtils.getContentUriForPath(path);
9203             final String[] projection = new String[]{
9204                     MediaColumns.OWNER_PACKAGE_NAME, MediaColumns._ID , FileColumns.MEDIA_TYPE};
9205             final String selection = MediaColumns.DATA + "=?";
9206             final String[] selectionArgs = new String[]{path};
9207             final String ownerPackageName;
9208             final int id;
9209             final int mediaType;
9210             // Query as MediaProvider as non-RES apps will result in FileNotFoundException.
9211             // Note: The caller uid already has passed permission checks to access this file.
9212             try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection,
9213                     selection, selectionArgs, null)) {
9214                 c.moveToFirst();
9215                 ownerPackageName = c.getString(0);
9216                 id = c.getInt(1);
9217                 mediaType = c.getInt(2);
9218             } catch (FileNotFoundException e) {
9219                 // Ideally, this shouldn't happen unless the file was deleted after we checked its
9220                 // existence and before we get to the redaction logic here. In this case we throw
9221                 // and fail the operation and FuseDaemon should handle this and fail the whole open
9222                 // operation gracefully.
9223                 throw new FileNotFoundException(
9224                         path + " not found while calculating redaction ranges: " + e.getMessage());
9225             }
9226 
9227             final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(),
9228                     ownerPackageName);
9229 
9230             // Do not redact if the caller is the owner
9231             if (callerIsOwner) {
9232                 return new long[0];
9233             }
9234 
9235             // Do not redact if the caller has write uri permission granted on the file.
9236             final Uri fileUri = ContentUris.withAppendedId(contentUri, id);
9237             boolean callerHasWriteUriPermission = getContext().checkUriPermission(
9238                     fileUri, mCallingIdentity.get().pid, mCallingIdentity.get().uid,
9239                     Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PERMISSION_GRANTED;
9240             if (callerHasWriteUriPermission) {
9241                 return new long[0];
9242             }
9243             // Check if the caller has write access to other uri formats for the same file.
9244             callerHasWriteUriPermission = getOtherUriGrantsForPath(path, mediaType,
9245                     Long.toString(id), /* forWrite */ true) != null;
9246             if (callerHasWriteUriPermission) {
9247                 return new long[0];
9248             }
9249 
9250             return getRedactionRanges(file).redactionRanges;
9251         } finally {
9252             restoreLocalCallingIdentity(token);
9253         }
9254     }
9255 
9256     /**
9257      * Calculates the ranges containing sensitive metadata that should be redacted if the caller
9258      * doesn't have the required permissions.
9259      *
9260      * @param file file to be redacted
9261      * @return the ranges to be redacted in a RedactionInfo object, could be empty redaction ranges
9262      * if there's sensitive metadata
9263      * @throws IOException if an IOException happens while calculating the redaction ranges
9264      */
9265     @VisibleForTesting
9266     public static RedactionInfo getRedactionRanges(File file) throws IOException {
9267         try (FileInputStream is = new FileInputStream(file)) {
9268             return getRedactionRanges(is, MimeUtils.resolveMimeType(file));
9269         } catch (FileNotFoundException ignored) {
9270             // If file not found, then there's nothing to redact
9271             return new RedactionInfo();
9272         } catch (IOException e) {
9273             throw new IOException("Failed to redact " + file, e);
9274         }
9275     }
9276 
9277     /**
9278      * Calculates the ranges containing sensitive metadata that should be redacted if the caller
9279      * doesn't have the required permissions.
9280      *
9281      * @param fis {@link FileInputStream} to be redacted
9282      * @return the ranges to be redacted in a RedactionInfo object, could be empty redaction ranges
9283      * if there's sensitive metadata
9284      * @throws IOException if an IOException happens while calculating the redaction ranges
9285      */
9286     @VisibleForTesting
9287     public static RedactionInfo getRedactionRanges(FileInputStream fis, String mimeType)
9288             throws IOException {
9289         final LongArray res = new LongArray();
9290         final LongArray freeOffsets = new LongArray();
9291 
9292         Trace.beginSection("MP.getRedactionRanges");
9293         try {
9294             if (ExifInterface.isSupportedMimeType(mimeType)) {
9295                 final ExifInterface exif = new ExifInterface(fis.getFD());
9296                 for (String tag : REDACTED_EXIF_TAGS) {
9297                     final long[] range = exif.getAttributeRange(tag);
9298                     if (range != null) {
9299                         res.add(range[0]);
9300                         res.add(range[0] + range[1]);
9301                     }
9302                 }
9303                 // Redact xmp where present
9304                 final XmpInterface exifXmp = XmpInterface.fromContainer(exif);
9305                 res.addAll(exifXmp.getRedactionRanges());
9306             }
9307 
9308             if (IsoInterface.isSupportedMimeType(mimeType)) {
9309                 final IsoInterface iso = IsoInterface.fromFileDescriptor(fis.getFD());
9310                 for (int box : REDACTED_ISO_BOXES) {
9311                     final long[] ranges = iso.getBoxRanges(box);
9312                     for (int i = 0; i < ranges.length; i += 2) {
9313                         long boxTypeOffset = ranges[i] - 4;
9314                         freeOffsets.add(boxTypeOffset);
9315                         res.add(boxTypeOffset);
9316                         res.add(ranges[i + 1]);
9317                     }
9318                 }
9319                 // Redact xmp where present
9320                 final XmpInterface isoXmp = XmpInterface.fromContainer(iso);
9321                 res.addAll(isoXmp.getRedactionRanges());
9322             }
9323 
9324             return new RedactionInfo(res.toArray(), freeOffsets.toArray());
9325         } finally {
9326             Trace.endSection();
9327         }
9328     }
9329 
9330     /**
9331      * @return {@code true} if {@code file} is pending from FUSE, {@code false} otherwise.
9332      * Files pending from FUSE will not have pending file pattern.
9333      */
9334     private static boolean isPendingFromFuse(@NonNull File file) {
9335         final Matcher matcher =
9336                 FileUtils.PATTERN_EXPIRES_FILE.matcher(extractDisplayName(file.getName()));
9337         return !matcher.matches();
9338     }
9339 
9340     private FileAccessAttributes queryForFileAttributes(final String path)
9341             throws FileNotFoundException {
9342         Trace.beginSection("MP.queryFileAttr");
9343         final Uri contentUri = FileUtils.getContentUriForPath(path);
9344         final String[] projection = new String[]{
9345                 MediaColumns._ID,
9346                 MediaColumns.OWNER_PACKAGE_NAME,
9347                 MediaColumns.IS_PENDING,
9348                 FileColumns.MEDIA_TYPE,
9349                 MediaColumns.IS_TRASHED
9350         };
9351         final String selection = MediaColumns.DATA + "=?";
9352         final String[] selectionArgs = new String[]{path};
9353         FileAccessAttributes fileAccessAttributes;
9354         try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection,
9355                 selection,
9356                 selectionArgs, null)) {
9357             fileAccessAttributes = FileAccessAttributes.fromCursor(c);
9358         }
9359         Trace.endSection();
9360         return fileAccessAttributes;
9361     }
9362 
9363     private void checkIfFileOpenIsPermitted(String path,
9364             FileAccessAttributes fileAccessAttributes, String redactedUriId,
9365             boolean forWrite) throws FileNotFoundException {
9366         final File file = new File(path);
9367         Uri fileUri = MediaStore.Files.getContentUri(extractVolumeName(path),
9368                 fileAccessAttributes.getId());
9369         // We don't check ownership for files with IS_PENDING set by FUSE
9370         // Please note that even if ownerPackageName is null, the check below will throw an
9371         // IllegalStateException
9372         if (fileAccessAttributes.isTrashed() || (fileAccessAttributes.isPending()
9373                 && !isPendingFromFuse(new File(path)))) {
9374             requireOwnershipForItem(fileAccessAttributes.getOwnerPackageName(), fileUri);
9375         }
9376 
9377         // Check that path looks consistent before uri checks
9378         if (!FileUtils.contains(Environment.getStorageDirectory(), file)) {
9379             checkWorldReadAccess(file.getAbsolutePath());
9380         }
9381 
9382         try {
9383             // checkAccess throws FileNotFoundException only from checkWorldReadAccess(),
9384             // which we already check above. Hence, handling only SecurityException.
9385             if (redactedUriId != null) {
9386                 fileUri = ContentUris.removeId(fileUri).buildUpon().appendPath(
9387                         redactedUriId).build();
9388             }
9389             checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
9390         } catch (SecurityException e) {
9391             // Check for other Uri formats only when the single uri check flow fails.
9392             // Throw the previous exception if the multi-uri checks failed.
9393             final String uriId = redactedUriId == null
9394                     ? Long.toString(fileAccessAttributes.getId()) : redactedUriId;
9395             if (getOtherUriGrantsForPath(path, fileAccessAttributes.getMediaType(),
9396                     uriId, forWrite) == null) {
9397                 throw e;
9398             }
9399         }
9400     }
9401 
9402 
9403     /**
9404      * Checks if the app identified by the given UID is allowed to open the given file for the given
9405      * access mode.
9406      *
9407      * @param path the path of the file to be opened
9408      * @param uid UID of the app requesting to open the file
9409      * @param forWrite specifies if the file is to be opened for write
9410      * @return {@link FileOpenResult} with {@code status} {@code 0} upon success and
9411      * {@link FileOpenResult} with {@code status} {@link OsConstants#EACCES} if the operation is
9412      * illegal or not permitted for the given {@code uid} or if the calling package is a legacy app
9413      * that doesn't have right storage permission.
9414      *
9415      * Called from JNI in jni/MediaProviderWrapper.cpp
9416      */
9417     @Keep
9418     public FileOpenResult onFileOpenForFuse(String path, String ioPath, int uid, int tid,
9419             int transformsReason, boolean forWrite, boolean redact, boolean logTransformsMetrics) {
9420         final LocalCallingIdentity token =
9421                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
9422 
9423         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
9424 
9425         boolean isSuccess = false;
9426 
9427         final int originalUid = getBinderUidForFuse(uid, tid);
9428         final int callingUserId = uidToUserId(uid);
9429         int mediaCapabilitiesUid = 0;
9430         final PendingOpenInfo pendingOpenInfo;
9431         synchronized (mPendingOpenInfo) {
9432             pendingOpenInfo = mPendingOpenInfo.get(tid);
9433         }
9434 
9435         if (pendingOpenInfo != null && pendingOpenInfo.uid == originalUid) {
9436             mediaCapabilitiesUid = pendingOpenInfo.mediaCapabilitiesUid;
9437         }
9438 
9439         try {
9440             boolean forceRedaction = false;
9441             String redactedUriId = null;
9442             if (isSyntheticPath(path, callingUserId)) {
9443                 if (forWrite) {
9444                     // Synthetic URIs are not allowed to update EXIF headers.
9445                     return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
9446                             mediaCapabilitiesUid, new long[0]);
9447                 }
9448 
9449                 if (isRedactedPath(path, callingUserId)) {
9450                     redactedUriId = extractFileName(path);
9451 
9452                     // If path is redacted Uris' path, ioPath must be the real path, ioPath must
9453                     // haven been updated to the real path during onFileLookupForFuse.
9454                     path = ioPath;
9455 
9456                     // Irrespective of the permissions we want to redact in this case.
9457                     redact = true;
9458                     forceRedaction = true;
9459                 } else if (isPickerPath(path, callingUserId)) {
9460                     return handlePickerFileOpen(path, originalUid);
9461                 } else {
9462                     // we don't support any other transformations under .transforms/synthetic dir
9463                     return new FileOpenResult(OsConstants.ENOENT /* status */, originalUid,
9464                             mediaCapabilitiesUid, new long[0]);
9465                 }
9466             }
9467 
9468             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
9469                 Log.e(TAG, "Can't open a file in another app's external directory!");
9470                 return new FileOpenResult(OsConstants.ENOENT, originalUid, mediaCapabilitiesUid,
9471                         new long[0]);
9472             }
9473 
9474             if (shouldBypassFuseRestrictions(forWrite, path)) {
9475                 isSuccess = true;
9476                 return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid,
9477                         redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid,
9478                                 forceRedaction) : new long[0]);
9479             }
9480             // Legacy apps that made is this far don't have the right storage permission and hence
9481             // are not allowed to access anything other than their external app directory
9482             if (isCallingPackageRequestingLegacy()) {
9483                 return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
9484                         mediaCapabilitiesUid, new long[0]);
9485             }
9486             // TODO: Fetch owner id from Android/media directory and check if caller is owner
9487             FileAccessAttributes fileAttributes = null;
9488             if (XAttrUtils.ENABLE_XATTR_METADATA_FOR_FUSE) {
9489                 Optional<FileAccessAttributes> fileAttributesThroughXattr =
9490                         XAttrUtils.getFileAttributesFromXAttr(path,
9491                                 XAttrUtils.FILE_ACCESS_XATTR_KEY);
9492                 if (fileAttributesThroughXattr.isPresent()) {
9493                     fileAttributes = fileAttributesThroughXattr.get();
9494                 }
9495             }
9496 
9497             // FileAttributes will be null if the xattr call failed or the flag to enable xattr
9498             // metadata support is not set
9499             if (fileAttributes == null)  {
9500                 fileAttributes = queryForFileAttributes(path);
9501             }
9502             checkIfFileOpenIsPermitted(path, fileAttributes, redactedUriId, forWrite);
9503             isSuccess = true;
9504             return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid,
9505                     redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid,
9506                             forceRedaction) : new long[0]);
9507         } catch (IOException e) {
9508             // We are here because
9509             // * There is no db row corresponding to the requested path, which is more unlikely.
9510             // * getRedactionRangesForFuse couldn't fetch the redaction info correctly
9511             // In all of these cases, it means that app doesn't have access permission to the file.
9512             Log.e(TAG, "Couldn't find file: " + path, e);
9513             return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
9514                     mediaCapabilitiesUid, new long[0]);
9515         } catch (IllegalStateException | SecurityException e) {
9516             Log.e(TAG, "Permission to access file: " + path + " is denied");
9517             return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
9518                     mediaCapabilitiesUid, new long[0]);
9519         } finally {
9520             if (isSuccess && logTransformsMetrics) {
9521                 notifyTranscodeHelperOnFileOpen(path, ioPath, originalUid, transformsReason);
9522             }
9523             restoreLocalCallingIdentity(token);
9524         }
9525     }
9526 
9527     @Nullable
9528     private Uri getOtherUriGrantsForPath(String path, boolean forWrite) {
9529         final Uri contentUri = FileUtils.getContentUriForPath(path);
9530         final String[] projection = new String[]{
9531                 MediaColumns._ID,
9532                 FileColumns.MEDIA_TYPE};
9533         final String selection = MediaColumns.DATA + "=?";
9534         final String[] selectionArgs = new String[]{path};
9535         final String id;
9536         final int mediaType;
9537         try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection, selection,
9538                 selectionArgs, null)) {
9539             id = c.getString(0);
9540             mediaType = c.getInt(1);
9541             return getOtherUriGrantsForPath(path, mediaType, id, forWrite);
9542         } catch (FileNotFoundException ignored) {
9543         }
9544         return null;
9545     }
9546 
9547     @Nullable
9548     private Uri getOtherUriGrantsForPath(String path, int mediaType, String id, boolean forWrite) {
9549         List<Uri> otherUris = new ArrayList<>();
9550         final Uri mediaUri = getMediaUriForFuse(extractVolumeName(path), mediaType, id);
9551         otherUris.add(mediaUri);
9552         final Uri externalMediaUri = getMediaUriForFuse(MediaStore.VOLUME_EXTERNAL, mediaType, id);
9553         otherUris.add(externalMediaUri);
9554         return getPermissionGrantedUri(otherUris, forWrite);
9555     }
9556 
9557     @NonNull
9558     private Uri getMediaUriForFuse(@NonNull String volumeName, int mediaType, String id) {
9559         Uri uri = MediaStore.Files.getContentUri(volumeName);
9560         switch (mediaType) {
9561             case FileColumns.MEDIA_TYPE_IMAGE:
9562                 uri = MediaStore.Images.Media.getContentUri(volumeName);
9563                 break;
9564             case FileColumns.MEDIA_TYPE_VIDEO:
9565                 uri = MediaStore.Video.Media.getContentUri(volumeName);
9566                 break;
9567             case FileColumns.MEDIA_TYPE_AUDIO:
9568                 uri = MediaStore.Audio.Media.getContentUri(volumeName);
9569                 break;
9570             case FileColumns.MEDIA_TYPE_PLAYLIST:
9571                 uri = MediaStore.Audio.Playlists.getContentUri(volumeName);
9572                 break;
9573         }
9574 
9575         return uri.buildUpon().appendPath(id).build();
9576     }
9577 
9578     /**
9579      * Returns {@code true} if {@link #mCallingIdentity#getSharedPackageNamesList(String)} contains
9580      * the given package name, {@code false} otherwise.
9581      * <p> Assumes that {@code mCallingIdentity} has been properly set to reflect the calling
9582      * package.
9583      */
9584     private boolean isCallingIdentitySharedPackageName(@NonNull String packageName) {
9585         for (String sharedPkgName : mCallingIdentity.get().getSharedPackageNamesArray()) {
9586             if (packageName.toLowerCase(Locale.ROOT)
9587                     .equals(sharedPkgName.toLowerCase(Locale.ROOT))) {
9588                 return true;
9589             }
9590         }
9591         return false;
9592     }
9593 
9594     /**
9595      * @throws IllegalStateException if path is invalid or doesn't match a volume.
9596      */
9597     @NonNull
9598     private Uri getContentUriForFile(@NonNull String filePath, @NonNull String mimeType) {
9599         final String volName;
9600         try {
9601             volName = FileUtils.getVolumeName(getContext(), new File(filePath));
9602         } catch (FileNotFoundException e) {
9603             throw new IllegalStateException("Couldn't get volume name for " + filePath);
9604         }
9605         Uri uri = Files.getContentUri(volName);
9606         String topLevelDir = extractTopLevelDir(filePath);
9607         if (topLevelDir == null) {
9608             // If the file path doesn't match the external storage directory, we use the files URI
9609             // as default and let #insert enforce the restrictions
9610             return uri;
9611         }
9612         topLevelDir = topLevelDir.toLowerCase(Locale.ROOT);
9613 
9614         switch (topLevelDir) {
9615             case DIRECTORY_PODCASTS_LOWER_CASE:
9616             case DIRECTORY_RINGTONES_LOWER_CASE:
9617             case DIRECTORY_ALARMS_LOWER_CASE:
9618             case DIRECTORY_NOTIFICATIONS_LOWER_CASE:
9619             case DIRECTORY_AUDIOBOOKS_LOWER_CASE:
9620             case DIRECTORY_RECORDINGS_LOWER_CASE:
9621                 uri = Audio.Media.getContentUri(volName);
9622                 break;
9623             case DIRECTORY_MUSIC_LOWER_CASE:
9624                 if (MimeUtils.isPlaylistMimeType(mimeType)) {
9625                     uri = Audio.Playlists.getContentUri(volName);
9626                 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) {
9627                     // Send Files uri for media type subtitle
9628                     uri = Audio.Media.getContentUri(volName);
9629                 }
9630                 break;
9631             case DIRECTORY_MOVIES_LOWER_CASE:
9632                 if (MimeUtils.isPlaylistMimeType(mimeType)) {
9633                     uri = Audio.Playlists.getContentUri(volName);
9634                 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) {
9635                     // Send Files uri for media type subtitle
9636                     uri = Video.Media.getContentUri(volName);
9637                 }
9638                 break;
9639             case DIRECTORY_DCIM_LOWER_CASE:
9640             case DIRECTORY_PICTURES_LOWER_CASE:
9641                 if (MimeUtils.isImageMimeType(mimeType)) {
9642                     uri = Images.Media.getContentUri(volName);
9643                 } else {
9644                     uri = Video.Media.getContentUri(volName);
9645                 }
9646                 break;
9647             case DIRECTORY_DOWNLOADS_LOWER_CASE:
9648             case DIRECTORY_DOCUMENTS_LOWER_CASE:
9649                 break;
9650             default:
9651                 Log.w(TAG, "Forgot to handle a top level directory in getContentUriForFile?");
9652         }
9653         return uri;
9654     }
9655 
9656     private boolean containsIgnoreCase(@Nullable List<String> stringsList, @Nullable String item) {
9657         if (item == null || stringsList == null) return false;
9658 
9659         for (String current : stringsList) {
9660             if (item.equalsIgnoreCase(current)) return true;
9661         }
9662         return false;
9663     }
9664 
9665     private boolean fileExists(@NonNull String absolutePath) {
9666         // We don't care about specific columns in the match,
9667         // we just want to check IF there's a match
9668         final String[] projection = {};
9669         final String selection = FileColumns.DATA + " = ?";
9670         final String[] selectionArgs = {absolutePath};
9671         final Uri uri = FileUtils.getContentUriForPath(absolutePath);
9672 
9673         final LocalCallingIdentity token = clearLocalCallingIdentity();
9674         try {
9675             try (final Cursor c = query(uri, projection, selection, selectionArgs, null)) {
9676                 // Shouldn't return null
9677                 return c.getCount() > 0;
9678             }
9679         } finally {
9680             clearLocalCallingIdentity(token);
9681         }
9682     }
9683 
9684     private Uri insertFileForFuse(@NonNull String path, @NonNull Uri uri, @NonNull String mimeType,
9685             boolean useData) {
9686         ContentValues values = new ContentValues();
9687         values.put(FileColumns.OWNER_PACKAGE_NAME, getCallingPackageOrSelf());
9688         values.put(MediaColumns.MIME_TYPE, mimeType);
9689         values.put(FileColumns.IS_PENDING, 1);
9690 
9691         if (useData) {
9692             values.put(FileColumns.DATA, path);
9693         } else {
9694             values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
9695             values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
9696             values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
9697         }
9698         return insert(uri, values, Bundle.EMPTY);
9699     }
9700 
9701     /**
9702      * Enforces file creation restrictions (see return values) for the given file on behalf of the
9703      * app with the given {@code uid}. If the file is added to the shared storage, creates a
9704      * database entry for it.
9705      * <p> Does NOT create file.
9706      *
9707      * @param path the path of the file
9708      * @param uid UID of the app requesting to create the file
9709      * @return In case of success, 0. If the operation is illegal or not permitted, returns the
9710      * appropriate {@code errno} value:
9711      * <ul>
9712      * <li>{@link OsConstants#ENOENT} if the app tries to create file in other app's external dir
9713      * <li>{@link OsConstants#EEXIST} if the file already exists
9714      * <li>{@link OsConstants#EPERM} if the file type doesn't match the relative path, or if the
9715      * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission.
9716      * <li>{@link OsConstants#EIO} in case of any other I/O exception
9717      * </ul>
9718      *
9719      * @throws IllegalStateException if given path is invalid.
9720      *
9721      * Called from JNI in jni/MediaProviderWrapper.cpp
9722      */
9723     @Keep
9724     public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) {
9725         final LocalCallingIdentity token =
9726                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
9727         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
9728 
9729         try {
9730             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
9731                 Log.e(TAG, "Can't create a file in another app's external directory");
9732                 return OsConstants.ENOENT;
9733             }
9734 
9735             if (!path.equals(getAbsoluteSanitizedPath(path))) {
9736                 Log.e(TAG, "File name contains invalid characters");
9737                 return OsConstants.EPERM;
9738             }
9739 
9740             if (shouldBypassDatabaseAndSetDirtyForFuse(uid, path)) {
9741                 if (path.endsWith("/.nomedia")) {
9742                     File parent = new File(path).getParentFile();
9743                     synchronized (mNonHiddenPaths) {
9744                         mNonHiddenPaths.keySet().removeIf(
9745                                 k -> FileUtils.contains(parent, new File(k)));
9746                     }
9747                 }
9748                 return 0;
9749             }
9750 
9751             final String mimeType = MimeUtils.resolveMimeType(new File(path));
9752 
9753             if (shouldBypassFuseRestrictions(/* forWrite */ true, path)) {
9754                 final boolean callerRequestingLegacy = isCallingPackageRequestingLegacy();
9755                 if (!fileExists(path)) {
9756                     // If app has already inserted the db row, inserting the row again might set
9757                     // IS_PENDING=1. We shouldn't overwrite existing entry as part of FUSE
9758                     // operation, hence, insert the db row only when it doesn't exist.
9759                     try {
9760                         insertFileForFuse(path, FileUtils.getContentUriForPath(path),
9761                                 mimeType, /* useData */ callerRequestingLegacy);
9762                     } catch (Exception ignored) {
9763                     }
9764                 } else {
9765                     // Upon creating a file via FUSE, if a row matching the path already exists
9766                     // but a file doesn't exist on the filesystem, we transfer ownership to the
9767                     // app attempting to create the file. If we don't update ownership, then the
9768                     // app that inserted the original row may be able to observe the contents of
9769                     // written file even though they don't hold the right permissions to do so.
9770                     if (callerRequestingLegacy) {
9771                         final String owner = getCallingPackageOrSelf();
9772                         if (owner != null && !updateOwnerForPath(path, owner)) {
9773                             return OsConstants.EPERM;
9774                         }
9775                     }
9776                 }
9777 
9778                 return 0;
9779             }
9780 
9781             // Legacy apps that made is this far don't have the right storage permission and hence
9782             // are not allowed to access anything other than their external app directory
9783             if (isCallingPackageRequestingLegacy()) {
9784                 return OsConstants.EPERM;
9785             }
9786 
9787             if (fileExists(path)) {
9788                 // If the file already exists in the db, we shouldn't allow the file creation.
9789                 return OsConstants.EEXIST;
9790             }
9791 
9792             final Uri contentUri = getContentUriForFile(path, mimeType);
9793             final Uri item = insertFileForFuse(path, contentUri, mimeType, /* useData */ false);
9794             if (item == null) {
9795                 return OsConstants.EPERM;
9796             }
9797             return 0;
9798         } catch (IllegalArgumentException e) {
9799             Log.e(TAG, "insertFileIfNecessary failed", e);
9800             return OsConstants.EPERM;
9801         } finally {
9802             restoreLocalCallingIdentity(token);
9803         }
9804     }
9805 
9806     private boolean updateOwnerForPath(@NonNull String path, @NonNull String newOwner) {
9807         final DatabaseHelper helper;
9808         try {
9809             helper = getDatabaseForUri(FileUtils.getContentUriForPath(path));
9810         } catch (VolumeNotFoundException e) {
9811             // Cannot happen, as this is a path that we already resolved.
9812             throw new AssertionError("Path must already be resolved", e);
9813         }
9814 
9815         ContentValues values = new ContentValues(1);
9816         values.put(FileColumns.OWNER_PACKAGE_NAME, newOwner);
9817 
9818         return helper.runWithoutTransaction(
9819                 (db) -> db.update("files", values, "_data=?", new String[] { path })) == 1;
9820     }
9821 
9822     private int deleteFileUnchecked(@NonNull String path,
9823             LocalCallingIdentity localCallingIdentity) {
9824         final File toDelete = new File(path);
9825         if (toDelete.delete()) {
9826             localCallingIdentity.incrementDeletedFileCountBypassingDatabase();
9827             return 0;
9828         } else {
9829             return OsConstants.ENOENT;
9830         }
9831     }
9832 
9833     /**
9834      * Deletes file with the given {@code path} on behalf of the app with the given {@code uid}.
9835      * <p>Before deleting, checks if app has permissions to delete this file.
9836      *
9837      * @param path the path of the file
9838      * @param uid UID of the app requesting to delete the file
9839      * @return 0 upon success.
9840      * In case of error, return the appropriate negated {@code errno} value:
9841      * <ul>
9842      * <li>{@link OsConstants#ENOENT} if the file does not exist or if the app tries to delete file
9843      * in another app's external dir
9844      * <li>{@link OsConstants#EPERM} a security exception was thrown by {@link #delete}, or if the
9845      * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission.
9846      * </ul>
9847      *
9848      * Called from JNI in jni/MediaProviderWrapper.cpp
9849      */
9850     @Keep
9851     public int deleteFileForFuse(@NonNull String path, int uid) throws IOException {
9852         final LocalCallingIdentity localCallingIdentity = getCachedCallingIdentityForFuse(uid);
9853         final LocalCallingIdentity token = clearLocalCallingIdentity(localCallingIdentity);
9854         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
9855 
9856         try {
9857             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
9858                 Log.e(TAG, "Can't delete a file in another app's external directory!");
9859                 return OsConstants.ENOENT;
9860             }
9861 
9862             if (shouldBypassDatabaseAndSetDirtyForFuse(uid, path)) {
9863                 return deleteFileUnchecked(path, localCallingIdentity);
9864             }
9865 
9866             final boolean shouldBypass = shouldBypassFuseRestrictions(/*forWrite*/ true, path);
9867 
9868             // Legacy apps that made is this far don't have the right storage permission and hence
9869             // are not allowed to access anything other than their external app directory
9870             if (!shouldBypass && isCallingPackageRequestingLegacy()) {
9871                 return OsConstants.EPERM;
9872             }
9873 
9874             final Uri contentUri = FileUtils.getContentUriForPath(path);
9875             final String where = FileColumns.DATA + " = ?";
9876             final String[] whereArgs = {path};
9877 
9878             if (delete(contentUri, where, whereArgs) == 0) {
9879                 if (shouldBypass) {
9880                     return deleteFileUnchecked(path, localCallingIdentity);
9881                 }
9882                 return OsConstants.ENOENT;
9883             } else {
9884                 // success - 1 file was deleted
9885                 return 0;
9886             }
9887 
9888         } catch (SecurityException e) {
9889             Log.e(TAG, "File deletion not allowed", e);
9890             return OsConstants.EPERM;
9891         } finally {
9892             restoreLocalCallingIdentity(token);
9893         }
9894     }
9895 
9896     // These need to stay in sync with MediaProviderWrapper.cpp's DirectoryAccessRequestType enum
9897     @IntDef(flag = true, prefix = { "DIRECTORY_ACCESS_FOR_" }, value = {
9898             DIRECTORY_ACCESS_FOR_READ,
9899             DIRECTORY_ACCESS_FOR_WRITE,
9900             DIRECTORY_ACCESS_FOR_CREATE,
9901             DIRECTORY_ACCESS_FOR_DELETE,
9902     })
9903     @Retention(RetentionPolicy.SOURCE)
9904     @VisibleForTesting
9905     @interface DirectoryAccessType {}
9906 
9907     @VisibleForTesting
9908     static final int DIRECTORY_ACCESS_FOR_READ = 1;
9909 
9910     @VisibleForTesting
9911     static final int DIRECTORY_ACCESS_FOR_WRITE = 2;
9912 
9913     @VisibleForTesting
9914     static final int DIRECTORY_ACCESS_FOR_CREATE = 3;
9915 
9916     @VisibleForTesting
9917     static final int DIRECTORY_ACCESS_FOR_DELETE = 4;
9918 
9919     /**
9920      * Checks whether the app with the given UID is allowed to access the directory denoted by the
9921      * given path.
9922      *
9923      * @param path directory's path
9924      * @param uid UID of the requesting app
9925      * @param accessType type of access being requested - eg {@link
9926      * MediaProvider#DIRECTORY_ACCESS_FOR_READ}
9927      * @return 0 if it's allowed to access the directory, {@link OsConstants#ENOENT} for attempts
9928      * to access a private package path in Android/data or Android/obb the caller doesn't have
9929      * access to, and otherwise {@link OsConstants#EACCES} if the calling package is a legacy app
9930      * that doesn't have READ_EXTERNAL_STORAGE permission or for other invalid attempts to access
9931      * Android/data or Android/obb dirs.
9932      *
9933      * Called from JNI in jni/MediaProviderWrapper.cpp
9934      */
9935     @Keep
9936     public int isDirAccessAllowedForFuse(@NonNull String path, int uid,
9937             @DirectoryAccessType int accessType) {
9938         Preconditions.checkArgumentInRange(accessType, 1, DIRECTORY_ACCESS_FOR_DELETE,
9939                 "accessType");
9940 
9941         final boolean forRead = accessType == DIRECTORY_ACCESS_FOR_READ;
9942         final LocalCallingIdentity token =
9943                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
9944         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
9945         try {
9946             if ("/storage/emulated".equals(path)) {
9947                 return OsConstants.EPERM;
9948             }
9949             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
9950                 Log.e(TAG, "Can't access another app's external directory!");
9951                 return OsConstants.ENOENT;
9952             }
9953 
9954             if (shouldBypassFuseRestrictions(/* forWrite= */ !forRead, path)) {
9955                 return 0;
9956             }
9957 
9958             // Do not allow apps that reach this point to access Android/data or Android/obb dirs.
9959             // Creation should be via getContext().getExternalFilesDir() etc methods.
9960             // Reads and writes on primary volumes should be via mount views of lowerfs for apps
9961             // that get special access to these directories.
9962             // Reads and writes on secondary volumes would be provided via an early return from
9963             // shouldBypassFuseRestrictions above (again just for apps with special access).
9964             if (isDataOrObbPath(path)) {
9965                 return OsConstants.EACCES;
9966             }
9967 
9968             // Legacy apps that made is this far don't have the right storage permission and hence
9969             // are not allowed to access anything other than their external app directory
9970             if (isCallingPackageRequestingLegacy()) {
9971                 return OsConstants.EACCES;
9972             }
9973             // This is a non-legacy app. Rest of the directories are generally writable
9974             // except for non-default top-level directories.
9975             if (!forRead) {
9976                 final String[] relativePath = sanitizePath(extractRelativePath(path));
9977                 if (relativePath.length == 0) {
9978                     Log.e(TAG,
9979                             "Directory update not allowed on invalid relative path for " + path);
9980                     return OsConstants.EPERM;
9981                 }
9982                 final boolean isTopLevelDir =
9983                         relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
9984                 if (isTopLevelDir) {
9985                     // We don't allow deletion of any top-level folders
9986                     if (accessType == DIRECTORY_ACCESS_FOR_DELETE) {
9987                         Log.e(TAG, "Deleting top level directories are not allowed!");
9988                         return OsConstants.EACCES;
9989                     }
9990 
9991                     // We allow creating or writing to default top-level folders, but we don't
9992                     // allow creation or writing to non-default top-level folders.
9993                     if ((accessType == DIRECTORY_ACCESS_FOR_CREATE
9994                             || accessType == DIRECTORY_ACCESS_FOR_WRITE)
9995                             && FileUtils.isDefaultDirectoryName(extractDisplayName(path))) {
9996                         return 0;
9997                     }
9998 
9999                     Log.e(TAG,
10000                             "Creating or writing to a non-default top level directory is not "
10001                                     + "allowed!");
10002                     return OsConstants.EACCES;
10003                 }
10004             }
10005 
10006             return 0;
10007         } finally {
10008             restoreLocalCallingIdentity(token);
10009         }
10010     }
10011 
10012     @Keep
10013     public boolean isUidAllowedAccessToDataOrObbPathForFuse(int uid, String path) {
10014         final LocalCallingIdentity token =
10015                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
10016         try {
10017             return isCallingIdentityAllowedAccessToDataOrObbPath(
10018                     extractRelativePathWithDisplayName(path));
10019         } finally {
10020             restoreLocalCallingIdentity(token);
10021         }
10022     }
10023 
10024     private boolean isCallingIdentityAllowedAccessToDataOrObbPath(String relativePath) {
10025         // Files under the apps own private directory
10026         final String appSpecificDir = extractOwnerPackageNameFromRelativePath(relativePath);
10027 
10028         if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) {
10029             return true;
10030         }
10031         // This is a private-package relativePath; return true if accessible by the caller
10032         return isCallingIdentityAllowedSpecialPrivatePathAccess(relativePath);
10033     }
10034 
10035     /**
10036      * @return true iff the caller has installer privileges which gives write access to obb dirs.
10037      *
10038      * @deprecated This method should only be called for Android R. For Android S+, please use
10039      * {@link StorageManager#getExternalStorageMountMode} to check if the caller has
10040      * {@link StorageManager#MOUNT_MODE_EXTERNAL_INSTALLER} access.
10041      *
10042      * Note: WRITE_EXTERNAL_STORAGE permission should ideally not be requested by non-legacy apps.
10043      * But to be consistent with {@link StorageManager} check for Installer apps access for primary
10044      * volumes in Android R, we do not add non-legacy apps check here as well.
10045      */
10046     @Deprecated
10047     private boolean isCallingIdentityAllowedInstallerAccess() {
10048         final boolean hasWrite = mCallingIdentity.get().
10049                 hasPermission(PERMISSION_WRITE_EXTERNAL_STORAGE);
10050 
10051         if (!hasWrite) {
10052             return false;
10053         }
10054 
10055         // We're only willing to give out installer access if they also hold
10056         // runtime permission; this is a firm CDD requirement
10057         final boolean hasInstall = mCallingIdentity.get().
10058                 hasPermission(PERMISSION_INSTALL_PACKAGES);
10059 
10060         if (hasInstall) {
10061             return true;
10062         }
10063         // OPSTR_REQUEST_INSTALL_PACKAGES is granted/denied per package but vold can't
10064         // update mountpoints of a specific package. So, check the appop for all packages
10065         // sharing the uid and allow same level of storage access for all packages even if
10066         // one of the packages has the appop granted.
10067         // To maintain consistency of access in primary volume and secondary volumes use the same
10068         // logic as we do for Zygote.MOUNT_EXTERNAL_INSTALLER view.
10069         return mCallingIdentity.get().hasPermission(APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID);
10070     }
10071 
10072     private String getExternalStorageProviderAuthority() {
10073         if (SdkLevel.isAtLeastS()) {
10074             return getExternalStorageProviderAuthorityFromDocumentsContract();
10075         }
10076         return MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
10077     }
10078 
10079     @RequiresApi(Build.VERSION_CODES.S)
10080     private String getExternalStorageProviderAuthorityFromDocumentsContract() {
10081         return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
10082     }
10083 
10084     private String getDownloadsProviderAuthority() {
10085         if (SdkLevel.isAtLeastS()) {
10086             return getDownloadsProviderAuthorityFromDocumentsContract();
10087         }
10088         return DOWNLOADS_PROVIDER_AUTHORITY;
10089     }
10090 
10091     @RequiresApi(Build.VERSION_CODES.S)
10092     private String getDownloadsProviderAuthorityFromDocumentsContract() {
10093         return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
10094     }
10095 
10096     private boolean isCallingIdentityDownloadProvider() {
10097         return getCallingUidOrSelf() == mDownloadsAuthorityAppId;
10098     }
10099 
10100     private boolean isCallingIdentityExternalStorageProvider() {
10101         return getCallingUidOrSelf() == mExternalStorageAuthorityAppId;
10102     }
10103 
10104     private boolean isCallingIdentityMtp() {
10105         return mCallingIdentity.get().hasPermission(PERMISSION_ACCESS_MTP);
10106     }
10107 
10108     /**
10109      * The following apps have access to all private-app directories on secondary volumes:
10110      *    * ExternalStorageProvider
10111      *    * DownloadProvider
10112      *    * Signature apps with ACCESS_MTP permission granted
10113      *      (Note: For Android R we also allow privileged apps with ACCESS_MTP to access all
10114      *      private-app directories, this additional access is removed for Android S+).
10115      *
10116      * Installer apps can only access private-app directories on Android/obb.
10117      *
10118      * @param relativePath the relative path of the file to access
10119      */
10120     private boolean isCallingIdentityAllowedSpecialPrivatePathAccess(String relativePath) {
10121         if (SdkLevel.isAtLeastS()) {
10122             return isMountModeAllowedPrivatePathAccess(getCallingUidOrSelf(), getCallingPackage(),
10123                     relativePath);
10124         } else {
10125             if (isCallingIdentityDownloadProvider() ||
10126                     isCallingIdentityExternalStorageProvider() || isCallingIdentityMtp()) {
10127                 return true;
10128             }
10129             return (isObbOrChildRelativePath(relativePath) &&
10130                     isCallingIdentityAllowedInstallerAccess());
10131         }
10132     }
10133 
10134     @RequiresApi(Build.VERSION_CODES.S)
10135     private boolean isMountModeAllowedPrivatePathAccess(int uid, String packageName,
10136             String relativePath) {
10137         // This is required as only MediaProvider (package with WRITE_MEDIA_STORAGE) can access
10138         // mount modes.
10139         final CallingIdentity token = clearCallingIdentity();
10140         try {
10141             final int mountMode = mStorageManager.getExternalStorageMountMode(uid, packageName);
10142             switch (mountMode) {
10143                 case StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE:
10144                 case StorageManager.MOUNT_MODE_EXTERNAL_PASS_THROUGH:
10145                     return true;
10146                 case StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER:
10147                     return isObbOrChildRelativePath(relativePath);
10148             }
10149         } catch (Exception e) {
10150             Log.w(TAG, "Caller does not have the permissions to access mount modes: ", e);
10151         } finally {
10152             restoreCallingIdentity(token);
10153         }
10154         return false;
10155     }
10156 
10157     private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
10158         // System internals can work with all media
10159         if (isCallingPackageSelf() || isCallingPackageShell()) {
10160             return true;
10161         }
10162 
10163         // Apps that have permission to manage external storage can work with all files
10164         if (isCallingPackageManager()) {
10165             return true;
10166         }
10167 
10168         // Check if caller is known to be owner of this item, to speed up
10169         // performance of our permission checks
10170         final int table = matchUri(uri, true);
10171         switch (table) {
10172             case AUDIO_MEDIA_ID:
10173             case VIDEO_MEDIA_ID:
10174             case IMAGES_MEDIA_ID:
10175             case FILES_ID:
10176             case DOWNLOADS_ID:
10177                 final long id = ContentUris.parseId(uri);
10178                 if (mCallingIdentity.get().isOwned(id)) {
10179                     return true;
10180                 }
10181                 break;
10182             default:
10183                 // continue below
10184         }
10185 
10186         // Check whether the uri is a specific table or not. Don't allow the global access to these
10187         // table uris
10188         switch (table) {
10189             case AUDIO_MEDIA:
10190             case IMAGES_MEDIA:
10191             case VIDEO_MEDIA:
10192             case DOWNLOADS:
10193             case FILES:
10194             case AUDIO_ALBUMS:
10195             case AUDIO_ARTISTS:
10196             case AUDIO_GENRES:
10197             case AUDIO_PLAYLISTS:
10198                 return false;
10199             default:
10200                 // continue below
10201         }
10202 
10203         // Outstanding grant means they get access
10204         return isUriPermissionGranted(uri, forWrite);
10205     }
10206 
10207     /**
10208      * Returns any uri that is granted from the set of Uris passed.
10209      */
10210     @Nullable
10211     private Uri getPermissionGrantedUri(@NonNull List<Uri> uris, boolean forWrite) {
10212         for (Uri uri : uris) {
10213             if (isUriPermissionGranted(uri, forWrite)) {
10214                 return uri;
10215             }
10216         }
10217         return null;
10218     }
10219 
10220     private boolean isUriPermissionGranted(Uri uri, boolean forWrite) {
10221         final int modeFlags = forWrite
10222                 ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION
10223                 : Intent.FLAG_GRANT_READ_URI_PERMISSION;
10224         int uriPermission = getContext().checkUriPermission(uri, mCallingIdentity.get().pid,
10225                 mCallingIdentity.get().uid, modeFlags);
10226         return uriPermission == PERMISSION_GRANTED;
10227     }
10228 
10229     @VisibleForTesting
10230     public boolean isFuseThread() {
10231         return FuseDaemon.native_is_fuse_thread();
10232     }
10233 
10234 
10235     /**
10236      * Enforce that caller has access to the given {@link Uri}.
10237      *
10238      * @throws SecurityException if access isn't allowed.
10239      */
10240     private void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras,
10241             boolean forWrite) {
10242         Trace.beginSection("MP.enforceCallingPermission");
10243         try {
10244             enforceCallingPermissionInternal(uri, extras, forWrite);
10245         } finally {
10246             Trace.endSection();
10247         }
10248     }
10249 
10250     private void enforceCallingPermission(@NonNull Collection<Uri> uris, boolean forWrite) {
10251         for (Uri uri : uris) {
10252             enforceCallingPermission(uri, Bundle.EMPTY, forWrite);
10253         }
10254     }
10255 
10256     private void enforceCallingPermissionInternal(@NonNull Uri uri, @NonNull Bundle extras,
10257             boolean forWrite) {
10258         Objects.requireNonNull(uri);
10259         Objects.requireNonNull(extras);
10260 
10261         // Try a simple global check first before falling back to performing a
10262         // simple query to probe for access.
10263         if (checkCallingPermissionGlobal(uri, forWrite)) {
10264             // Access allowed, yay!
10265             return;
10266         }
10267 
10268         // For redacted URI proceed with its corresponding URI as query builder doesn't support
10269         // redacted URIs for fetching a database row
10270         // NOTE: The grants (if any) must have been on redacted URI hence global check requires
10271         // redacted URI
10272         Uri redactedUri = null;
10273         if (isRedactedUri(uri)) {
10274             redactedUri = uri;
10275             uri = getUriForRedactedUri(uri);
10276         }
10277 
10278         final DatabaseHelper helper;
10279         try {
10280             helper = getDatabaseForUri(uri);
10281         } catch (VolumeNotFoundException e) {
10282             throw e.rethrowAsIllegalArgumentException();
10283         }
10284 
10285         final boolean allowHidden = isCallingPackageAllowedHidden();
10286         final int table = matchUri(uri, allowHidden);
10287 
10288         final String selection = extras.getString(QUERY_ARG_SQL_SELECTION);
10289         final String[] selectionArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);
10290 
10291         // First, check to see if caller has direct write access
10292         if (forWrite) {
10293             final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, null);
10294             qb.allowColumn(SQLiteQueryBuilder.ROWID_COLUMN);
10295             try (Cursor c = qb.query(helper, new String[] { SQLiteQueryBuilder.ROWID_COLUMN },
10296                     selection, selectionArgs, null, null, null, null, null)) {
10297                 if (c.moveToFirst()) {
10298                     // Direct write access granted, yay!
10299                     return;
10300                 }
10301             }
10302         }
10303 
10304         // We only allow the user to grant access to specific media items in
10305         // strongly typed collections; never to broad collections
10306         boolean allowUserGrant = false;
10307         final int matchUri = matchUri(uri, true);
10308         switch (matchUri) {
10309             case IMAGES_MEDIA_ID:
10310             case AUDIO_MEDIA_ID:
10311             case VIDEO_MEDIA_ID:
10312                 allowUserGrant = true;
10313                 break;
10314         }
10315 
10316         // Second, check to see if caller has direct read access
10317         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null);
10318         qb.allowColumn(SQLiteQueryBuilder.ROWID_COLUMN);
10319         try (Cursor c = qb.query(helper, new String[] { SQLiteQueryBuilder.ROWID_COLUMN },
10320                 selection, selectionArgs, null, null, null, null, null)) {
10321             if (c.moveToFirst()) {
10322                 if (!forWrite) {
10323                     // Direct read access granted, yay!
10324                     return;
10325                 } else if (allowUserGrant) {
10326                     // Caller has read access, but they wanted to write, and
10327                     // they'll need to get the user to grant that access
10328                     final Context context = getContext();
10329                     final Collection<Uri> uris = Collections.singletonList(uri);
10330                     final PendingIntent intent = MediaStore
10331                             .createWriteRequest(ContentResolver.wrap(this), uris);
10332 
10333                     final Icon icon = getCollectionIcon(uri);
10334                     final RemoteAction action = new RemoteAction(icon,
10335                             context.getText(R.string.permission_required_action),
10336                             context.getText(R.string.permission_required_action),
10337                             intent);
10338 
10339                     throw new RecoverableSecurityException(new SecurityException(
10340                             getCallingPackageOrSelf() + " has no access to " + uri),
10341                             context.getText(R.string.permission_required), action);
10342                 }
10343             }
10344         }
10345 
10346         if (redactedUri != null) uri = redactedUri;
10347         throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri);
10348     }
10349 
10350     private Icon getCollectionIcon(Uri uri) {
10351         final PackageManager pm = getContext().getPackageManager();
10352         final String type = uri.getPathSegments().get(1);
10353         final String groupName;
10354         switch (type) {
10355             default: groupName = android.Manifest.permission_group.STORAGE; break;
10356         }
10357         try {
10358             final PermissionGroupInfo perm = pm.getPermissionGroupInfo(groupName, 0);
10359             return Icon.createWithResource(perm.packageName, perm.icon);
10360         } catch (NameNotFoundException e) {
10361             throw new RuntimeException(e);
10362         }
10363     }
10364 
10365     private void checkAccess(@NonNull Uri uri, @NonNull Bundle extras, @NonNull File file,
10366             boolean isWrite) throws FileNotFoundException {
10367         // First, does caller have the needed row-level access?
10368         enforceCallingPermission(uri, extras, isWrite);
10369 
10370         // Second, does the path look consistent?
10371         if (!FileUtils.contains(Environment.getStorageDirectory(), file)) {
10372             checkWorldReadAccess(file.getAbsolutePath());
10373         }
10374     }
10375 
10376     /**
10377      * Check whether the path is a world-readable file
10378      */
10379     @VisibleForTesting
10380     public static void checkWorldReadAccess(String path) throws FileNotFoundException {
10381         // Path has already been canonicalized, and we relax the check to look
10382         // at groups to support runtime storage permissions.
10383         final int accessBits = path.startsWith("/storage/") ? OsConstants.S_IRGRP
10384                 : OsConstants.S_IROTH;
10385         try {
10386             StructStat stat = Os.stat(path);
10387             if (OsConstants.S_ISREG(stat.st_mode) &&
10388                 ((stat.st_mode & accessBits) == accessBits)) {
10389                 checkLeadingPathComponentsWorldExecutable(path);
10390                 return;
10391             }
10392         } catch (ErrnoException e) {
10393             // couldn't stat the file, either it doesn't exist or isn't
10394             // accessible to us
10395         }
10396 
10397         throw new FileNotFoundException("Can't access " + path);
10398     }
10399 
10400     private static void checkLeadingPathComponentsWorldExecutable(String filePath)
10401             throws FileNotFoundException {
10402         File parent = new File(filePath).getParentFile();
10403 
10404         // Path has already been canonicalized, and we relax the check to look
10405         // at groups to support runtime storage permissions.
10406         final int accessBits = filePath.startsWith("/storage/") ? OsConstants.S_IXGRP
10407                 : OsConstants.S_IXOTH;
10408 
10409         while (parent != null) {
10410             if (! parent.exists()) {
10411                 // parent dir doesn't exist, give up
10412                 throw new FileNotFoundException("access denied");
10413             }
10414             try {
10415                 StructStat stat = Os.stat(parent.getPath());
10416                 if ((stat.st_mode & accessBits) != accessBits) {
10417                     // the parent dir doesn't have the appropriate access
10418                     throw new FileNotFoundException("Can't access " + filePath);
10419                 }
10420             } catch (ErrnoException e1) {
10421                 // couldn't stat() parent
10422                 throw new FileNotFoundException("Can't access " + filePath);
10423             }
10424             parent = parent.getParentFile();
10425         }
10426     }
10427 
10428     @VisibleForTesting
10429     static class FallbackException extends Exception {
10430         private final int mThrowSdkVersion;
10431 
10432         public FallbackException(String message, int throwSdkVersion) {
10433             super(message);
10434             mThrowSdkVersion = throwSdkVersion;
10435         }
10436 
10437         public FallbackException(String message, Throwable cause, int throwSdkVersion) {
10438             super(message, cause);
10439             mThrowSdkVersion = throwSdkVersion;
10440         }
10441 
10442         @Override
10443         public String getMessage() {
10444             if (getCause() != null) {
10445                 return super.getMessage() + ": " + getCause().getMessage();
10446             } else {
10447                 return super.getMessage();
10448             }
10449         }
10450 
10451         public IllegalArgumentException rethrowAsIllegalArgumentException() {
10452             throw new IllegalArgumentException(getMessage());
10453         }
10454 
10455         public Cursor translateForQuery(int targetSdkVersion) {
10456             if (targetSdkVersion >= mThrowSdkVersion) {
10457                 throw new IllegalArgumentException(getMessage());
10458             } else {
10459                 Log.w(TAG, getMessage());
10460                 return null;
10461             }
10462         }
10463 
10464         public Uri translateForInsert(int targetSdkVersion) {
10465             if (targetSdkVersion >= mThrowSdkVersion) {
10466                 throw new IllegalArgumentException(getMessage());
10467             } else {
10468                 Log.w(TAG, getMessage());
10469                 return null;
10470             }
10471         }
10472 
10473         public int translateForBulkInsert(int targetSdkVersion) {
10474             if (targetSdkVersion >= mThrowSdkVersion) {
10475                 throw new IllegalArgumentException(getMessage());
10476             } else {
10477                 Log.w(TAG, getMessage());
10478                 return 0;
10479             }
10480         }
10481 
10482         public int translateForUpdateDelete(int targetSdkVersion) {
10483             if (targetSdkVersion >= mThrowSdkVersion) {
10484                 throw new IllegalArgumentException(getMessage());
10485             } else {
10486                 Log.w(TAG, getMessage());
10487                 return 0;
10488             }
10489         }
10490     }
10491 
10492     @VisibleForTesting
10493     static class VolumeNotFoundException extends FallbackException {
10494         public VolumeNotFoundException(String volumeName) {
10495             super("Volume " + volumeName + " not found", Build.VERSION_CODES.Q);
10496         }
10497     }
10498 
10499     @VisibleForTesting
10500     static class VolumeArgumentException extends FallbackException {
10501         public VolumeArgumentException(File actual, Collection<File> allowed) {
10502             super("Requested path " + actual + " doesn't appear under " + allowed,
10503                     Build.VERSION_CODES.Q);
10504         }
10505     }
10506 
10507     public List<String> getSupportedTranscodingRelativePaths() {
10508         return mTranscodeHelper.getSupportedRelativePaths();
10509     }
10510 
10511     public List<String> getSupportedUncachedRelativePaths() {
10512         return StringUtils.verifySupportedUncachedRelativePaths(
10513                        StringUtils.getStringArrayConfig(getContext(),
10514                                R.array.config_supported_uncached_relative_paths));
10515     }
10516 
10517     /**
10518      * Creating a new method for Transcoding to avoid any merge conflicts.
10519      * TODO(b/170465810): Remove this when the code is refactored.
10520      */
10521     @NonNull DatabaseHelper getDatabaseForUriForTranscoding(Uri uri)
10522             throws VolumeNotFoundException {
10523         return getDatabaseForUri(uri);
10524     }
10525 
10526     @NonNull
10527     private DatabaseHelper getDatabaseForUri(Uri uri) throws VolumeNotFoundException {
10528         final String volumeName = resolveVolumeName(uri);
10529         synchronized (mAttachedVolumes) {
10530             boolean volumeAttached = false;
10531             UserHandle user = mCallingIdentity.get().getUser();
10532             for (MediaVolume vol : mAttachedVolumes) {
10533                 if (vol.getName().equals(volumeName)
10534                         && (vol.isVisibleToUser(user) || vol.isPublicVolume()) ) {
10535                     volumeAttached = true;
10536                     break;
10537                 }
10538             }
10539             if (!volumeAttached) {
10540                 // Dump some more debug info
10541                 Log.e(TAG, "Volume " + volumeName + " not found, calling identity: "
10542                         + user + ", attached volumes: " + mAttachedVolumes);
10543                 throw new VolumeNotFoundException(volumeName);
10544             }
10545         }
10546         if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
10547             return mInternalDatabase;
10548         } else {
10549             return mExternalDatabase;
10550         }
10551     }
10552 
10553     static boolean isMediaDatabaseName(String name) {
10554         if (INTERNAL_DATABASE_NAME.equals(name)) {
10555             return true;
10556         }
10557         if (EXTERNAL_DATABASE_NAME.equals(name)) {
10558             return true;
10559         }
10560         return name.startsWith("external-") && name.endsWith(".db");
10561     }
10562 
10563     @NonNull
10564     private Uri getBaseContentUri(@NonNull String volumeName) {
10565         return MediaStore.AUTHORITY_URI.buildUpon().appendPath(volumeName).build();
10566     }
10567 
10568     public Uri attachVolume(MediaVolume volume, boolean validate) {
10569         if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
10570             throw new SecurityException(
10571                     "Opening and closing databases not allowed.");
10572         }
10573 
10574         final String volumeName = volume.getName();
10575 
10576         // Quick check for shady volume names
10577         MediaStore.checkArgumentVolumeName(volumeName);
10578 
10579         // Quick check that volume actually exists
10580         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName) && validate) {
10581             try {
10582                 getVolumePath(volumeName);
10583             } catch (IOException e) {
10584                 throw new IllegalArgumentException(
10585                         "Volume " + volume + " currently unavailable", e);
10586             }
10587         }
10588 
10589         synchronized (mAttachedVolumes) {
10590             mAttachedVolumes.add(volume);
10591         }
10592 
10593         mDatabaseBackupAndRecovery.setupVolumeDbBackupAndRecovery(volume.getName(),
10594                 volume.getPath());
10595 
10596         final ContentResolver resolver = getContext().getContentResolver();
10597         final Uri uri = getBaseContentUri(volumeName);
10598         // TODO(b/182396009) we probably also want to notify clone profile (and vice versa)
10599         resolver.notifyChange(getBaseContentUri(volumeName), null);
10600 
10601         if (LOGV) Log.v(TAG, "Attached volume: " + volume);
10602         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
10603             // Also notify on synthetic view of all devices
10604             resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
10605 
10606             ForegroundThread.getExecutor().execute(() -> {
10607                 mExternalDatabase.runWithTransaction((db) -> {
10608                     ensureDefaultFolders(volume, db);
10609                     ensureThumbnailsValid(volume, db);
10610                     return null;
10611                 });
10612 
10613                 // We just finished the database operation above, we know that
10614                 // it's ready to answer queries, so notify our DocumentProvider
10615                 // so it can answer queries without risking ANR
10616                 MediaDocumentsProvider.onMediaStoreReady(getContext());
10617             });
10618         }
10619         return uri;
10620     }
10621 
10622     private void detachVolume(Uri uri) {
10623         final String volumeName = MediaStore.getVolumeName(uri);
10624         try {
10625             detachVolume(getVolume(volumeName));
10626         } catch (FileNotFoundException e) {
10627             Log.e(TAG, "Couldn't find volume for URI " + uri, e) ;
10628         }
10629     }
10630 
10631     public boolean isVolumeAttached(MediaVolume volume) {
10632         synchronized (mAttachedVolumes) {
10633             return mAttachedVolumes.contains(volume);
10634         }
10635     }
10636 
10637     public void detachVolume(MediaVolume volume) {
10638         if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
10639             throw new SecurityException(
10640                     "Opening and closing databases not allowed.");
10641         }
10642 
10643         final String volumeName = volume.getName();
10644 
10645         // Quick check for shady volume names
10646         MediaStore.checkArgumentVolumeName(volumeName);
10647 
10648         if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
10649             throw new UnsupportedOperationException(
10650                     "Deleting the internal volume is not allowed");
10651         }
10652 
10653         // Signal any scanning to shut down
10654         mMediaScanner.onDetachVolume(volume);
10655 
10656         synchronized (mAttachedVolumes) {
10657             mAttachedVolumes.remove(volume);
10658         }
10659 
10660         final ContentResolver resolver = getContext().getContentResolver();
10661         resolver.notifyChange(getBaseContentUri(volumeName), null);
10662 
10663         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
10664             // Also notify on synthetic view of all devices
10665             resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
10666         }
10667 
10668         if (LOGV) Log.v(TAG, "Detached volume: " + volumeName);
10669     }
10670 
10671     @GuardedBy("mAttachedVolumes")
10672     private final ArraySet<MediaVolume> mAttachedVolumes = new ArraySet<>();
10673     @GuardedBy("mCustomCollators")
10674     private final ArraySet<String> mCustomCollators = new ArraySet<>();
10675 
10676     private MediaScanner mMediaScanner;
10677 
10678     private ProjectionHelper mProjectionHelper;
10679     private DatabaseHelper mInternalDatabase;
10680     private DatabaseHelper mExternalDatabase;
10681     private PickerDbFacade mPickerDbFacade;
10682     private ExternalDbFacade mExternalDbFacade;
10683     private PickerDataLayer mPickerDataLayer;
10684     private ConfigStore mConfigStore;
10685     private PickerSyncController mPickerSyncController;
10686     private TranscodeHelper mTranscodeHelper;
10687     private MediaGrants mMediaGrants;
10688     private DatabaseBackupAndRecovery mDatabaseBackupAndRecovery;
10689 
10690     // name of the volume currently being scanned by the media scanner (or null)
10691     private String mMediaScannerVolume;
10692 
10693 
10694     private static final HashSet<Integer> REDACTED_URI_SUPPORTED_TYPES = new HashSet<>(
10695             Arrays.asList(AUDIO_MEDIA_ID, IMAGES_MEDIA_ID, VIDEO_MEDIA_ID, FILES_ID, DOWNLOADS_ID));
10696 
10697     private LocalUriMatcher mUriMatcher;
10698 
10699     private int matchUri(Uri uri, boolean allowHidden) {
10700         return mUriMatcher.matchUri(uri, allowHidden);
10701     }
10702 
10703 
10704 
10705     /**
10706      * Set of columns that can be safely mutated by external callers; all other
10707      * columns are treated as read-only, since they reflect what the media
10708      * scanner found on disk, and any mutations would be overwritten the next
10709      * time the media was scanned.
10710      */
10711     private static final ArraySet<String> sMutableColumns = new ArraySet<>();
10712 
10713     static {
10714         sMutableColumns.add(MediaStore.MediaColumns.DATA);
10715         sMutableColumns.add(MediaStore.MediaColumns.RELATIVE_PATH);
10716         sMutableColumns.add(MediaStore.MediaColumns.DISPLAY_NAME);
10717         sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING);
10718         sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED);
10719         sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE);
10720         sMutableColumns.add(MediaStore.MediaColumns.OWNER_PACKAGE_NAME);
10721 
10722         sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK);
10723 
10724         sMutableColumns.add(MediaStore.Video.VideoColumns.TAGS);
10725         sMutableColumns.add(MediaStore.Video.VideoColumns.CATEGORY);
10726         sMutableColumns.add(MediaStore.Video.VideoColumns.BOOKMARK);
10727 
10728         sMutableColumns.add(MediaStore.Audio.Playlists.NAME);
10729         sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID);
10730         sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
10731 
10732         sMutableColumns.add(MediaStore.DownloadColumns.DOWNLOAD_URI);
10733         sMutableColumns.add(MediaStore.DownloadColumns.REFERER_URI);
10734 
10735         sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE);
10736         sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE);
10737     }
10738 
10739     /**
10740      * Set of columns that affect placement of files on disk.
10741      */
10742     private static final ArraySet<String> sPlacementColumns = new ArraySet<>();
10743 
10744     static {
10745         sPlacementColumns.add(MediaStore.MediaColumns.DATA);
10746         sPlacementColumns.add(MediaStore.MediaColumns.RELATIVE_PATH);
10747         sPlacementColumns.add(MediaStore.MediaColumns.DISPLAY_NAME);
10748         sPlacementColumns.add(MediaStore.MediaColumns.MIME_TYPE);
10749         sPlacementColumns.add(MediaStore.MediaColumns.IS_PENDING);
10750         sPlacementColumns.add(MediaStore.MediaColumns.IS_TRASHED);
10751         sPlacementColumns.add(MediaStore.MediaColumns.DATE_EXPIRES);
10752     }
10753 
10754     /**
10755      * List of abusive custom columns that we're willing to allow via
10756      * {@link SQLiteQueryBuilder#setProjectionGreylist(List)}.
10757      */
10758     static final ArrayList<Pattern> sGreylist = new ArrayList<>();
10759 
10760     private static void addGreylistPattern(String pattern) {
10761         sGreylist.add(Pattern.compile(" *" + pattern + " *"));
10762     }
10763 
10764     static {
10765         final String maybeAs = "( (as )?[_a-z0-9]+)?";
10766         addGreylistPattern("(?i)[_a-z0-9]+" + maybeAs);
10767         addGreylistPattern("audio\\._id AS _id");
10768         addGreylistPattern(
10769                 "(?i)(min|max|sum|avg|total|count|cast)\\(([_a-z0-9]+"
10770                         + maybeAs
10771                         + "|\\*)\\)"
10772                         + maybeAs);
10773         addGreylistPattern(
10774                 "case when case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added"
10775                     + " \\* \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then"
10776                     + " date_added when \\(date_added >= \\d+ and date_added < \\d+\\) then"
10777                     + " date_added / \\d+ else \\d+ end > case when \\(date_modified >= \\d+ and"
10778                     + " date_modified < \\d+\\) then date_modified \\* \\d+ when \\(date_modified"
10779                     + " >= \\d+ and date_modified < \\d+\\) then date_modified when"
10780                     + " \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified /"
10781                     + " \\d+ else \\d+ end then case when \\(date_added >= \\d+ and date_added <"
10782                     + " \\d+\\) then date_added \\* \\d+ when \\(date_added >= \\d+ and date_added"
10783                     + " < \\d+\\) then date_added when \\(date_added >= \\d+ and date_added <"
10784                     + " \\d+\\) then date_added / \\d+ else \\d+ end else case when"
10785                     + " \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified \\*"
10786                     + " \\d+ when \\(date_modified >= \\d+ and date_modified < \\d+\\) then"
10787                     + " date_modified when \\(date_modified >= \\d+ and date_modified < \\d+\\)"
10788                     + " then date_modified / \\d+ else \\d+ end end as corrected_added_modified");
10789         addGreylistPattern(
10790                 "MAX\\(case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\*"
10791                     + " \\d+ when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when"
10792                     + " \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else"
10793                     + " \\d+ end\\)");
10794         addGreylistPattern(
10795                 "MAX\\(case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added \\*"
10796                     + " \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added"
10797                     + " when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added / \\d+"
10798                     + " else \\d+ end\\)");
10799         addGreylistPattern(
10800                 "MAX\\(case when \\(date_modified >= \\d+ and date_modified < \\d+\\) then"
10801                     + " date_modified \\* \\d+ when \\(date_modified >= \\d+ and date_modified <"
10802                     + " \\d+\\) then date_modified when \\(date_modified >= \\d+ and date_modified"
10803                     + " < \\d+\\) then date_modified / \\d+ else \\d+ end\\)");
10804         addGreylistPattern("\"content://media/[a-z]+/audio/media\"");
10805         addGreylistPattern(
10806                 "substr\\(_data, length\\(_data\\)-length\\(_display_name\\), 1\\) as"
10807                     + " filename_prevchar");
10808         addGreylistPattern("\\*" + maybeAs);
10809         addGreylistPattern(
10810                 "case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\* \\d+"
10811                     + " when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when"
10812                     + " \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else"
10813                     + " \\d+ end");
10814     }
10815 
10816     public ArrayMap<String, String> getProjectionMap(Class<?>... clazzes) {
10817         return mProjectionHelper.getProjectionMap(clazzes);
10818     }
10819 
10820     static <T> boolean containsAny(Set<T> a, Set<T> b) {
10821         for (T i : b) {
10822             if (a.contains(i)) {
10823                 return true;
10824             }
10825         }
10826         return false;
10827     }
10828 
10829     @VisibleForTesting
10830     @Nullable
10831     static Uri computeCommonPrefix(@NonNull List<Uri> uris) {
10832         if (uris.isEmpty()) return null;
10833 
10834         final Uri base = uris.get(0);
10835         final List<String> basePath = new ArrayList<>(base.getPathSegments());
10836         for (int i = 1; i < uris.size(); i++) {
10837             final List<String> probePath = uris.get(i).getPathSegments();
10838             for (int j = 0; j < basePath.size() && j < probePath.size(); j++) {
10839                 if (!Objects.equals(basePath.get(j), probePath.get(j))) {
10840                     // Trim away all remaining common elements
10841                     while (basePath.size() > j) {
10842                         basePath.remove(j);
10843                     }
10844                 }
10845             }
10846 
10847             final int probeSize = probePath.size();
10848             while (basePath.size() > probeSize) {
10849                 basePath.remove(probeSize);
10850             }
10851         }
10852 
10853         final Uri.Builder builder = base.buildUpon().path(null);
10854         for (String s : basePath) {
10855             builder.appendPath(s);
10856         }
10857         return builder.build();
10858     }
10859 
10860     public ExternalDbFacade getExternalDbFacade() {
10861         return mExternalDbFacade;
10862     }
10863 
10864     public PickerSyncController getPickerSyncController() {
10865         return mPickerSyncController;
10866     }
10867 
10868     private boolean isCallingPackageSystemGallery() {
10869         if (mCallingIdentity.get().hasPermission(PERMISSION_IS_SYSTEM_GALLERY)) {
10870             if (isCallingPackageRequestingLegacy()) {
10871                 return isCallingPackageLegacyWrite();
10872             }
10873             return true;
10874         }
10875         return false;
10876     }
10877 
10878     private int getCallingUidOrSelf() {
10879         return mCallingIdentity.get().uid;
10880     }
10881 
10882     @Deprecated
10883     private String getCallingPackageOrSelf() {
10884         return mCallingIdentity.get().getPackageName();
10885     }
10886 
10887     @Deprecated
10888     @VisibleForTesting
10889     public int getCallingPackageTargetSdkVersion() {
10890         return mCallingIdentity.get().getTargetSdkVersion();
10891     }
10892 
10893     @Deprecated
10894     private boolean isCallingPackageAllowedHidden() {
10895         return isCallingPackageSelf();
10896     }
10897 
10898     @Deprecated
10899     private boolean isCallingPackageSelf() {
10900         return mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF);
10901     }
10902 
10903     @Deprecated
10904     private boolean isCallingPackageShell() {
10905         return mCallingIdentity.get().hasPermission(PERMISSION_IS_SHELL);
10906     }
10907 
10908     @Deprecated
10909     private boolean isCallingPackageManager() {
10910         return mCallingIdentity.get().hasPermission(PERMISSION_IS_MANAGER);
10911     }
10912 
10913     @Deprecated
10914     private boolean isCallingPackageDelegator() {
10915         return mCallingIdentity.get().hasPermission(PERMISSION_IS_DELEGATOR);
10916     }
10917 
10918     @Deprecated
10919     private boolean isCallingPackageLegacyRead() {
10920         return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_READ);
10921     }
10922 
10923     @Deprecated
10924     private boolean isCallingPackageLegacyWrite() {
10925         return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE);
10926     }
10927 
10928     @Override
10929     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
10930         writer.println("mThumbSize=" + mThumbSize);
10931         synchronized (mAttachedVolumes) {
10932             writer.println("mAttachedVolumes=" + mAttachedVolumes);
10933         }
10934         writer.println();
10935 
10936         mVolumeCache.dump(writer);
10937         writer.println();
10938 
10939         mUserCache.dump(writer);
10940         writer.println();
10941 
10942         mTranscodeHelper.dump(writer);
10943         writer.println();
10944 
10945         dumpAccessLogs(writer);
10946         writer.println();
10947 
10948         Logging.dumpPersistent(writer);
10949     }
10950 
10951     private void dumpAccessLogs(PrintWriter writer) {
10952         synchronized (mCachedCallingIdentityForFuse) {
10953             for (int key = 0; key <= mCachedCallingIdentityForFuse.size(); key++) {
10954                 if (mCachedCallingIdentityForFuse.contains(key)) {
10955                     mCachedCallingIdentityForFuse.get(key).dump(writer);
10956                 }
10957             }
10958         }
10959     }
10960 
10961     /**
10962      * Called once - from {@link #onCreate()}.
10963      */
10964     @NonNull
10965     private ConfigStore createConfigStore() {
10966         // Tests may want override provideConfigStore() in order to inject a mock object.
10967         ConfigStore configStore = provideConfigStore();
10968         if (configStore == null) {
10969             // Tests did not provide an alternative implementation: create our regular "production"
10970             // ConfigStore.
10971             configStore = MediaApplication.getConfigStore();
10972         }
10973         return configStore;
10974     }
10975 
10976     /**
10977      * <b>FOT TESTING PURPOSES ONLY</b>
10978      * <p>
10979      * Allows injecting alternative {@link ConfigStore} implementation.
10980      */
10981     @VisibleForTesting
10982     @Nullable
10983     protected ConfigStore provideConfigStore() {
10984         return null;
10985     }
10986 
10987     protected VolumeCache getVolumeCache() {
10988         return mVolumeCache;
10989     }
10990 
10991     protected DatabaseBackupAndRecovery createDatabaseBackupAndRecovery() {
10992         return new DatabaseBackupAndRecovery(mConfigStore, mVolumeCache);
10993     }
10994 }
10995