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.app.AppOpsManager.permissionToOp;
21 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
22 import static android.app.PendingIntent.FLAG_IMMUTABLE;
23 import static android.app.PendingIntent.FLAG_ONE_SHOT;
24 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION;
25 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS;
26 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
27 import static android.provider.MediaStore.MATCH_DEFAULT;
28 import static android.provider.MediaStore.MATCH_EXCLUDE;
29 import static android.provider.MediaStore.MATCH_INCLUDE;
30 import static android.provider.MediaStore.MATCH_ONLY;
31 import static android.provider.MediaStore.QUERY_ARG_MATCH_FAVORITE;
32 import static android.provider.MediaStore.QUERY_ARG_MATCH_PENDING;
33 import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED;
34 import static android.provider.MediaStore.QUERY_ARG_RELATED_URI;
35 import static android.provider.MediaStore.getVolumeName;
36 
37 import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME;
38 import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
39 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_DELEGATOR;
40 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED;
41 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_READ;
42 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE;
43 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_MANAGER;
44 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED;
45 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SELF;
46 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SHELL;
47 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_AUDIO;
48 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES;
49 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO;
50 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_AUDIO;
51 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_IMAGES;
52 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_VIDEO;
53 import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND;
54 import static com.android.providers.media.scan.MediaScanner.REASON_IDLE;
55 import static com.android.providers.media.util.DatabaseUtils.bindList;
56 import static com.android.providers.media.util.FileUtils.PATTERN_PENDING_FILEPATH_FOR_SQL;
57 import static com.android.providers.media.util.FileUtils.extractDisplayName;
58 import static com.android.providers.media.util.FileUtils.extractFileName;
59 import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName;
60 import static com.android.providers.media.util.FileUtils.extractRelativePath;
61 import static com.android.providers.media.util.FileUtils.extractRelativePathForDirectory;
62 import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
63 import static com.android.providers.media.util.FileUtils.extractVolumeName;
64 import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath;
65 import static com.android.providers.media.util.FileUtils.isDataOrObbPath;
66 import static com.android.providers.media.util.FileUtils.isDownload;
67 import static com.android.providers.media.util.FileUtils.sanitizePath;
68 import static com.android.providers.media.util.Logging.LOGV;
69 import static com.android.providers.media.util.Logging.TAG;
70 
71 import android.app.AppOpsManager;
72 import android.app.AppOpsManager.OnOpActiveChangedListener;
73 import android.app.AppOpsManager.OnOpChangedListener;
74 import android.app.DownloadManager;
75 import android.app.PendingIntent;
76 import android.app.RecoverableSecurityException;
77 import android.app.RemoteAction;
78 import android.content.BroadcastReceiver;
79 import android.content.ClipData;
80 import android.content.ClipDescription;
81 import android.content.ContentProvider;
82 import android.content.ContentProviderClient;
83 import android.content.ContentProviderOperation;
84 import android.content.ContentProviderResult;
85 import android.content.ContentResolver;
86 import android.content.ContentUris;
87 import android.content.ContentValues;
88 import android.content.Context;
89 import android.content.Intent;
90 import android.content.IntentFilter;
91 import android.content.OperationApplicationException;
92 import android.content.SharedPreferences;
93 import android.content.UriMatcher;
94 import android.content.pm.ApplicationInfo;
95 import android.content.pm.PackageInstaller.SessionInfo;
96 import android.content.pm.PackageManager;
97 import android.content.pm.PackageManager.NameNotFoundException;
98 import android.content.pm.PermissionGroupInfo;
99 import android.content.pm.ProviderInfo;
100 import android.content.res.AssetFileDescriptor;
101 import android.content.res.Configuration;
102 import android.content.res.Resources;
103 import android.database.Cursor;
104 import android.database.MatrixCursor;
105 import android.database.sqlite.SQLiteConstraintException;
106 import android.database.sqlite.SQLiteDatabase;
107 import android.graphics.Bitmap;
108 import android.graphics.BitmapFactory;
109 import android.graphics.drawable.Icon;
110 import android.icu.util.ULocale;
111 import android.media.ExifInterface;
112 import android.media.ThumbnailUtils;
113 import android.mtp.MtpConstants;
114 import android.net.Uri;
115 import android.os.Binder;
116 import android.os.Binder.ProxyTransactListener;
117 import android.os.Build;
118 import android.os.Bundle;
119 import android.os.CancellationSignal;
120 import android.os.Environment;
121 import android.os.IBinder;
122 import android.os.ParcelFileDescriptor;
123 import android.os.ParcelFileDescriptor.OnCloseListener;
124 import android.os.RemoteException;
125 import android.os.SystemClock;
126 import android.os.SystemProperties;
127 import android.os.Trace;
128 import android.os.UserHandle;
129 import android.os.storage.StorageManager;
130 import android.os.storage.StorageManager.StorageVolumeCallback;
131 import android.os.storage.StorageVolume;
132 import android.preference.PreferenceManager;
133 import android.provider.BaseColumns;
134 import android.provider.Column;
135 import android.provider.MediaStore;
136 import android.provider.MediaStore.Audio;
137 import android.provider.MediaStore.Audio.AudioColumns;
138 import android.provider.MediaStore.Audio.Playlists;
139 import android.provider.MediaStore.Downloads;
140 import android.provider.MediaStore.Files;
141 import android.provider.MediaStore.Files.FileColumns;
142 import android.provider.MediaStore.Images;
143 import android.provider.MediaStore.Images.ImageColumns;
144 import android.provider.MediaStore.MediaColumns;
145 import android.provider.MediaStore.Video;
146 import android.system.ErrnoException;
147 import android.system.Os;
148 import android.system.OsConstants;
149 import android.system.StructStat;
150 import android.text.TextUtils;
151 import android.text.format.DateUtils;
152 import android.util.ArrayMap;
153 import android.util.ArraySet;
154 import android.util.DisplayMetrics;
155 import android.util.Log;
156 import android.util.LongSparseArray;
157 import android.util.Size;
158 import android.util.SparseArray;
159 import android.webkit.MimeTypeMap;
160 
161 import androidx.annotation.GuardedBy;
162 import androidx.annotation.Keep;
163 import androidx.annotation.NonNull;
164 import androidx.annotation.Nullable;
165 import androidx.annotation.VisibleForTesting;
166 
167 import com.android.providers.media.DatabaseHelper.OnFilesChangeListener;
168 import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener;
169 import com.android.providers.media.fuse.ExternalStorageServiceImpl;
170 import com.android.providers.media.fuse.FuseDaemon;
171 import com.android.providers.media.playlist.Playlist;
172 import com.android.providers.media.scan.MediaScanner;
173 import com.android.providers.media.scan.ModernMediaScanner;
174 import com.android.providers.media.util.BackgroundThread;
175 import com.android.providers.media.util.CachedSupplier;
176 import com.android.providers.media.util.DatabaseUtils;
177 import com.android.providers.media.util.FileUtils;
178 import com.android.providers.media.util.ForegroundThread;
179 import com.android.providers.media.util.IsoInterface;
180 import com.android.providers.media.util.Logging;
181 import com.android.providers.media.util.LongArray;
182 import com.android.providers.media.util.Metrics;
183 import com.android.providers.media.util.MimeUtils;
184 import com.android.providers.media.util.PermissionUtils;
185 import com.android.providers.media.util.RedactingFileDescriptor;
186 import com.android.providers.media.util.SQLiteQueryBuilder;
187 import com.android.providers.media.util.XmpInterface;
188 
189 import com.google.common.hash.Hashing;
190 
191 import java.io.File;
192 import java.io.FileDescriptor;
193 import java.io.FileInputStream;
194 import java.io.FileNotFoundException;
195 import java.io.FileOutputStream;
196 import java.io.IOException;
197 import java.io.OutputStream;
198 import java.io.PrintWriter;
199 import java.nio.charset.StandardCharsets;
200 import java.nio.file.Path;
201 import java.util.ArrayList;
202 import java.util.Arrays;
203 import java.util.Collection;
204 import java.util.List;
205 import java.util.Locale;
206 import java.util.Map;
207 import java.util.Objects;
208 import java.util.Optional;
209 import java.util.Set;
210 import java.util.function.Consumer;
211 import java.util.function.Supplier;
212 import java.util.function.UnaryOperator;
213 import java.util.regex.Matcher;
214 import java.util.regex.Pattern;
215 
216 /**
217  * Media content provider. See {@link android.provider.MediaStore} for details.
218  * Separate databases are kept for each external storage card we see (using the
219  * card's ID as an index).  The content visible at content://media/external/...
220  * changes with the card.
221  */
222 public class MediaProvider extends ContentProvider {
223     /**
224      * Regex of a selection string that matches a specific ID.
225      */
226     static final Pattern PATTERN_SELECTION_ID = Pattern.compile(
227             "(?:image_id|video_id)\\s*=\\s*(\\d+)");
228 
229     /**
230      * Property that indicates whether fuse is enabled.
231      */
232     private static final String PROP_FUSE = "persist.sys.fuse";
233 
234     /**
235      * These directory names aren't declared in Environment as final variables, and so we need to
236      * have the same values in separate final variables in order to have them considered constant
237      * expressions.
238      */
239     private static final String DIRECTORY_MUSIC = "Music";
240     private static final String DIRECTORY_PODCASTS = "Podcasts";
241     private static final String DIRECTORY_RINGTONES = "Ringtones";
242     private static final String DIRECTORY_ALARMS = "Alarms";
243     private static final String DIRECTORY_NOTIFICATIONS = "Notifications";
244     private static final String DIRECTORY_PICTURES = "Pictures";
245     private static final String DIRECTORY_MOVIES = "Movies";
246     private static final String DIRECTORY_DOWNLOADS = "Download";
247     private static final String DIRECTORY_DCIM = "DCIM";
248     private static final String DIRECTORY_DOCUMENTS = "Documents";
249     private static final String DIRECTORY_AUDIOBOOKS = "Audiobooks";
250     private static final String DIRECTORY_ANDROID = "Android";
251 
252     private static final String DIRECTORY_MEDIA = "media";
253     private static final String DIRECTORY_THUMBNAILS = ".thumbnails";
254 
255     /**
256      * Hard-coded filename where the current value of
257      * {@link DatabaseHelper#getOrCreateUuid} is persisted on a physical SD card
258      * to help identify stale thumbnail collections.
259      */
260     private static final String FILE_DATABASE_UUID = ".database_uuid";
261 
262     /**
263      * Specify what default directories the caller gets full access to. By default, the caller
264      * shouldn't get full access to any default dirs.
265      * But for example, we do an exception for System Gallery apps and allow them full access to:
266      * DCIM, Pictures, Movies.
267      */
268     private static final String INCLUDED_DEFAULT_DIRECTORIES =
269             "android:included-default-directories";
270 
271     /**
272      * Value indicating that operations should include database rows matching the criteria defined
273      * by this key only when calling package has write permission to the database row or column is
274      * {@column MediaColumns#IS_PENDING} and is set by FUSE.
275      * <p>
276      * Note that items <em>not</em> matching the criteria will also be included, and as part of this
277      * match no additional write permission checks are carried out for those items.
278      */
279     private static final int MATCH_VISIBLE_FOR_FILEPATH = 32;
280 
281     /**
282      * Where clause to match pending files from FUSE. Pending files from FUSE will not have
283      * PATTERN_PENDING_FILEPATH_FOR_SQL pattern.
284      */
285     private static final String MATCH_PENDING_FROM_FUSE = String.format("lower(%s) NOT REGEXP '%s'",
286             MediaColumns.DATA, PATTERN_PENDING_FILEPATH_FOR_SQL);
287 
288     /**
289      * Set of {@link Cursor} columns that refer to raw filesystem paths.
290      */
291     private static final ArrayMap<String, Object> sDataColumns = new ArrayMap<>();
292 
293     {
sDataColumns.put(MediaStore.MediaColumns.DATA, null)294         sDataColumns.put(MediaStore.MediaColumns.DATA, null);
sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null)295         sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null);
sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null)296         sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null);
sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null)297         sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null);
sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null)298         sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null);
299     }
300 
301     private static final Object sCacheLock = new Object();
302 
303     @GuardedBy("sCacheLock")
304     private static final Set<String> sCachedExternalVolumeNames = new ArraySet<>();
305     @GuardedBy("sCacheLock")
306     private static final Map<String, File> sCachedVolumePaths = new ArrayMap<>();
307     @GuardedBy("sCacheLock")
308     private static final Map<String, Collection<File>> sCachedVolumeScanPaths = new ArrayMap<>();
309     @GuardedBy("sCacheLock")
310     private static final ArrayMap<File, String> sCachedVolumePathToId = new ArrayMap<>();
311 
312     @GuardedBy("mShouldRedactThreadIds")
313     private final LongArray mShouldRedactThreadIds = new LongArray();
314 
updateVolumes()315     public void updateVolumes() {
316         synchronized (sCacheLock) {
317             sCachedExternalVolumeNames.clear();
318             sCachedExternalVolumeNames.addAll(MediaStore.getExternalVolumeNames(getContext()));
319             Log.v(TAG, "Updated external volumes to: " + sCachedExternalVolumeNames.toString());
320 
321             sCachedVolumePaths.clear();
322             sCachedVolumeScanPaths.clear();
323             sCachedVolumePathToId.clear();
324             try {
325                 sCachedVolumeScanPaths.put(MediaStore.VOLUME_INTERNAL,
326                         FileUtils.getVolumeScanPaths(getContext(), MediaStore.VOLUME_INTERNAL));
327             } catch (FileNotFoundException e) {
328                 Log.wtf(TAG, "Failed to update volume " + MediaStore.VOLUME_INTERNAL, e);
329             }
330 
331             for (String volumeName : sCachedExternalVolumeNames) {
332                 try {
333                     final Uri uri = MediaStore.Files.getContentUri(volumeName);
334                     final StorageVolume volume = mStorageManager.getStorageVolume(uri);
335                     sCachedVolumePaths.put(volumeName, volume.getDirectory());
336                     sCachedVolumeScanPaths.put(volumeName,
337                             FileUtils.getVolumeScanPaths(getContext(), volumeName));
338                     sCachedVolumePathToId.put(volume.getDirectory(), volume.getId());
339                 } catch (IllegalStateException | FileNotFoundException e) {
340                     Log.wtf(TAG, "Failed to update volume " + volumeName, e);
341                 }
342             }
343         }
344 
345         // Update filters to reflect mounted volumes so users don't get
346         // confused by metadata from ejected volumes
347         ForegroundThread.getExecutor().execute(() -> {
348             mExternalDatabase.setFilterVolumeNames(getExternalVolumeNames());
349         });
350     }
351 
getVolumePath(@onNull String volumeName)352     public @NonNull File getVolumePath(@NonNull String volumeName) throws FileNotFoundException {
353         // Ugly hack to keep unit tests passing, where we don't always have a
354         // Context to discover volumes with
355         if (getContext() == null) {
356             return Environment.getExternalStorageDirectory();
357         }
358 
359         synchronized (sCacheLock) {
360             if (sCachedVolumePaths.containsKey(volumeName)) {
361                 return sCachedVolumePaths.get(volumeName);
362             }
363 
364             // Nothing found above; let's ask directly and cache the answer
365             final File res = FileUtils.getVolumePath(getContext(), volumeName);
366             sCachedVolumePaths.put(volumeName, res);
367             return res;
368         }
369     }
370 
getVolumeId(@onNull File file)371     public @NonNull String getVolumeId(@NonNull File file) throws FileNotFoundException {
372         synchronized (sCacheLock) {
373             for (int i = 0; i < sCachedVolumePathToId.size(); i++) {
374                 if (FileUtils.contains(sCachedVolumePathToId.keyAt(i), file)) {
375                     return sCachedVolumePathToId.valueAt(i);
376                 }
377             }
378 
379             // Nothing found above; let's ask directly and cache the answer
380             final StorageVolume volume = mStorageManager.getStorageVolume(file);
381             if (volume == null) {
382                 throw new FileNotFoundException("Missing volume for " + file);
383             }
384             sCachedVolumePathToId.put(volume.getDirectory(), volume.getId());
385             return volume.getId();
386         }
387     }
388 
getExternalVolumeNames()389     public @NonNull Set<String> getExternalVolumeNames() {
390         synchronized (sCacheLock) {
391             return new ArraySet<>(sCachedExternalVolumeNames);
392         }
393     }
394 
getVolumeScanPaths(String volumeName)395     public @NonNull Collection<File> getVolumeScanPaths(String volumeName)
396             throws FileNotFoundException {
397         synchronized (sCacheLock) {
398             if (sCachedVolumeScanPaths.containsKey(volumeName)) {
399                 return new ArrayList<>(sCachedVolumeScanPaths.get(volumeName));
400             }
401 
402             // Nothing found above; let's ask directly and cache the answer
403             final Collection<File> res = FileUtils.getVolumeScanPaths(getContext(), volumeName);
404             sCachedVolumeScanPaths.put(volumeName, res);
405             return res;
406         }
407     }
408 
409     private StorageManager mStorageManager;
410     private AppOpsManager mAppOpsManager;
411     private PackageManager mPackageManager;
412 
413     private Size mThumbSize;
414 
415     /**
416      * Map from UID to cached {@link LocalCallingIdentity}. Values are only
417      * maintained in this map while the UID is actively working with a
418      * performance-critical component, such as camera.
419      */
420     @GuardedBy("mCachedCallingIdentity")
421     private final SparseArray<LocalCallingIdentity> mCachedCallingIdentity = new SparseArray<>();
422 
423     private final OnOpActiveChangedListener mActiveListener = (code, uid, packageName, active) -> {
424         synchronized (mCachedCallingIdentity) {
425             if (active) {
426                 // TODO moltmann: Set correct featureId
427                 mCachedCallingIdentity.put(uid,
428                         LocalCallingIdentity.fromExternal(getContext(), uid, packageName,
429                                 null));
430             } else {
431                 mCachedCallingIdentity.remove(uid);
432             }
433         }
434     };
435 
436     /**
437      * Map from UID to cached {@link LocalCallingIdentity}. Values are only
438      * maintained in this map until there's any change in the appops needed or packages
439      * used in the {@link LocalCallingIdentity}.
440      */
441     @GuardedBy("mCachedCallingIdentityForFuse")
442     private final SparseArray<LocalCallingIdentity> mCachedCallingIdentityForFuse =
443             new SparseArray<>();
444 
445     private OnOpChangedListener mModeListener =
446             (op, packageName) -> invalidateLocalCallingIdentityCache(packageName, "op " + op);
447 
448     /**
449      * Retrieves a cached calling identity or creates a new one. Also, always sets the app-op
450      * description for the calling identity.
451      */
getCachedCallingIdentityForFuse(int uid)452     private LocalCallingIdentity getCachedCallingIdentityForFuse(int uid) {
453         synchronized (mCachedCallingIdentityForFuse) {
454             PermissionUtils.setOpDescription("via FUSE");
455             LocalCallingIdentity ident = mCachedCallingIdentityForFuse.get(uid);
456             if (ident == null) {
457                ident = LocalCallingIdentity.fromExternal(getContext(), uid);
458                mCachedCallingIdentityForFuse.put(uid, ident);
459             }
460             return ident;
461         }
462     }
463 
464     /**
465      * Calling identity state about on the current thread. Populated on demand,
466      * and invalidated by {@link #onCallingPackageChanged()} when each remote
467      * call is finished.
468      */
469     private final ThreadLocal<LocalCallingIdentity> mCallingIdentity = ThreadLocal
470             .withInitial(() -> {
471                 PermissionUtils.setOpDescription("via MediaProvider");
472                 synchronized (mCachedCallingIdentity) {
473                     final LocalCallingIdentity cached = mCachedCallingIdentity
474                             .get(Binder.getCallingUid());
475                     return (cached != null) ? cached
476                             : LocalCallingIdentity.fromBinder(getContext(), this);
477                 }
478             });
479 
480     /**
481      * We simply propagate the UID that is being tracked by
482      * {@link LocalCallingIdentity}, which means we accurately blame both
483      * incoming Binder calls and FUSE calls.
484      */
485     private final ProxyTransactListener mTransactListener = new ProxyTransactListener() {
486         @Override
487         public Object onTransactStarted(IBinder binder, int transactionCode) {
488             if (LOGV) Trace.beginSection(Thread.currentThread().getStackTrace()[5].getMethodName());
489             return Binder.setCallingWorkSourceUid(mCallingIdentity.get().uid);
490         }
491 
492         @Override
493         public void onTransactEnded(Object session) {
494             final long token = (long) session;
495             Binder.restoreCallingWorkSource(token);
496             if (LOGV) Trace.endSection();
497         }
498     };
499 
500     // In memory cache of path<->id mappings, to speed up inserts during media scan
501     @GuardedBy("mDirectoryCache")
502     private final ArrayMap<String, Long> mDirectoryCache = new ArrayMap<>();
503 
504     private static final String[] sDataOnlyColumn = new String[] {
505         FileColumns.DATA
506     };
507 
508     private static final String ID_NOT_PARENT_CLAUSE =
509             "_id NOT IN (SELECT parent FROM files WHERE parent IS NOT NULL)";
510 
511     private static final String CANONICAL = "canonical";
512 
513     private BroadcastReceiver mPackageReceiver = new BroadcastReceiver() {
514         @Override
515         public void onReceive(Context context, Intent intent) {
516             switch (intent.getAction()) {
517                 case Intent.ACTION_PACKAGE_REMOVED:
518                 case Intent.ACTION_PACKAGE_ADDED:
519                     Uri uri = intent.getData();
520                     String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
521                     if (pkg != null) {
522                         invalidateLocalCallingIdentityCache(pkg, "package " + intent.getAction());
523                     } else {
524                         Log.w(TAG, "Failed to retrieve package from intent: " + intent.getAction());
525                     }
526                     break;
527             }
528         }
529     };
530 
invalidateLocalCallingIdentityCache(String packageName, String reason)531     private void invalidateLocalCallingIdentityCache(String packageName, String reason) {
532         synchronized (mCachedCallingIdentityForFuse) {
533             try {
534                 Log.i(TAG, "Invalidating LocalCallingIdentity cache for package " + packageName
535                         + ". Reason: " + reason);
536                 mCachedCallingIdentityForFuse.remove(
537                         getContext().getPackageManager().getPackageUid(packageName, 0));
538             } catch (NameNotFoundException ignored) {
539             }
540         }
541     }
542 
updateQuotaTypeForUri(@onNull Uri uri, int mediaType)543     private final void updateQuotaTypeForUri(@NonNull Uri uri, int mediaType) {
544         Trace.beginSection("updateQuotaTypeForUri");
545         File file;
546         try {
547             file = queryForDataFile(uri, null);
548             if (!file.exists()) {
549                 // This can happen if an item is inserted in MediaStore before it is created
550                 return;
551             }
552 
553             if (mediaType == FileColumns.MEDIA_TYPE_NONE) {
554                 // This might be because the file is hidden; but we still want to
555                 // attribute its quota to the correct type, so get the type from
556                 // the extension instead.
557                 mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
558             }
559 
560             updateQuotaTypeForFileInternal(file, mediaType);
561         } catch (FileNotFoundException e) {
562             // Ignore
563             return;
564         } finally {
565             Trace.endSection();
566         }
567     }
568 
updateQuotaTypeForFileInternal(File file, int mediaType)569     private final void updateQuotaTypeForFileInternal(File file, int mediaType) {
570         try {
571             switch (mediaType) {
572                 case FileColumns.MEDIA_TYPE_AUDIO:
573                     mStorageManager.updateExternalStorageFileQuotaType(file,
574                             StorageManager.QUOTA_TYPE_MEDIA_AUDIO);
575                     break;
576                 case FileColumns.MEDIA_TYPE_VIDEO:
577                     mStorageManager.updateExternalStorageFileQuotaType(file,
578                             StorageManager.QUOTA_TYPE_MEDIA_VIDEO);
579                     break;
580                 case FileColumns.MEDIA_TYPE_IMAGE:
581                     mStorageManager.updateExternalStorageFileQuotaType(file,
582                             StorageManager.QUOTA_TYPE_MEDIA_IMAGE);
583                     break;
584                 default:
585                     mStorageManager.updateExternalStorageFileQuotaType(file,
586                             StorageManager.QUOTA_TYPE_MEDIA_NONE);
587                     break;
588             }
589         } catch (IOException e) {
590             Log.w(TAG, "Failed to update quota type for " + file.getPath(), e);
591         }
592     }
593 
594     /**
595      * Since these operations are in the critical path of apps working with
596      * media, we only collect the {@link Uri} that need to be notified, and all
597      * other side-effect operations are delegated to {@link BackgroundThread} so
598      * that we return as quickly as possible.
599      */
600     private final OnFilesChangeListener mFilesListener = new OnFilesChangeListener() {
601         @Override
602         public void onInsert(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
603                 int mediaType, boolean isDownload) {
604             handleInsertedRowForFuse(id);
605             acceptWithExpansion(helper::notifyInsert, volumeName, id, mediaType, isDownload);
606 
607             helper.postBackground(() -> {
608                 if (helper.isExternal()) {
609                     // Update the quota type on the filesystem
610                     Uri fileUri = MediaStore.Files.getContentUri(volumeName, id);
611                     updateQuotaTypeForUri(fileUri, mediaType);
612                 }
613 
614                 // Tell our SAF provider so it knows when views are no longer empty
615                 MediaDocumentsProvider.onMediaStoreInsert(getContext(), volumeName, mediaType, id);
616             });
617         }
618 
619         @Override
620         public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
621                 long oldId, int oldMediaType, boolean oldIsDownload,
622                 long newId, int newMediaType, boolean newIsDownload,
623                 String oldOwnerPackage, String newOwnerPackage, String oldPath) {
624             final boolean isDownload = oldIsDownload || newIsDownload;
625             final Uri fileUri = MediaStore.Files.getContentUri(volumeName, oldId);
626             handleUpdatedRowForFuse(oldPath, oldOwnerPackage, oldId, newId);
627             handleOwnerPackageNameChange(oldPath, oldOwnerPackage, newOwnerPackage);
628             acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, oldMediaType, isDownload);
629 
630             helper.postBackground(() -> {
631                 if (helper.isExternal()) {
632                     // Update the quota type on the filesystem
633                     updateQuotaTypeForUri(fileUri, newMediaType);
634                 }
635             });
636 
637             if (newMediaType != oldMediaType) {
638                 acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, newMediaType,
639                         isDownload);
640 
641                 helper.postBackground(() -> {
642                     // Invalidate any thumbnails when the media type changes
643                     invalidateThumbnails(fileUri);
644                 });
645             }
646         }
647 
648         @Override
649         public void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
650                 int mediaType, boolean isDownload, String ownerPackageName, String path) {
651             handleDeletedRowForFuse(path, ownerPackageName, id);
652             acceptWithExpansion(helper::notifyDelete, volumeName, id, mediaType, isDownload);
653 
654             helper.postBackground(() -> {
655                 // Item no longer exists, so revoke all access to it
656                 Trace.beginSection("revokeUriPermission");
657                 try {
658                     acceptWithExpansion((uri) -> {
659                         getContext().revokeUriPermission(uri, ~0);
660                     }, volumeName, id, mediaType, isDownload);
661                 } finally {
662                     Trace.endSection();
663                 }
664 
665                 // Invalidate any thumbnails now that media is gone
666                 invalidateThumbnails(MediaStore.Files.getContentUri(volumeName, id));
667 
668                 // Tell our SAF provider so it can revoke too
669                 MediaDocumentsProvider.onMediaStoreDelete(getContext(), volumeName, mediaType, id);
670             });
671         }
672     };
673 
674     private final UnaryOperator<String> mIdGenerator = path -> {
675         final long rowId = mCallingIdentity.get().getDeletedRowId(path);
676         if (rowId != -1 && isFuseThread()) {
677             return String.valueOf(rowId);
678         }
679         return null;
680     };
681 
682     private final OnLegacyMigrationListener mMigrationListener = new OnLegacyMigrationListener() {
683         @Override
684         public void onStarted(ContentProviderClient client, String volumeName) {
685             MediaStore.startLegacyMigration(ContentResolver.wrap(client), volumeName);
686         }
687 
688         @Override
689         public void onProgress(ContentProviderClient client, String volumeName,
690                 long progress, long total) {
691             // TODO: notify blocked threads of progress once we can change APIs
692         }
693 
694         @Override
695         public void onFinished(ContentProviderClient client, String volumeName) {
696             MediaStore.finishLegacyMigration(ContentResolver.wrap(client), volumeName);
697         }
698     };
699 
700     /**
701      * Apply {@link Consumer#accept} to the given item.
702      * <p>
703      * Since media items can be exposed through multiple collections or views,
704      * this method expands the single item being accepted to also accept all
705      * relevant views.
706      */
acceptWithExpansion(@onNull Consumer<Uri> consumer, @NonNull String volumeName, long id, int mediaType, boolean isDownload)707     private void acceptWithExpansion(@NonNull Consumer<Uri> consumer, @NonNull String volumeName,
708             long id, int mediaType, boolean isDownload) {
709         switch (mediaType) {
710             case FileColumns.MEDIA_TYPE_AUDIO:
711                 consumer.accept(MediaStore.Audio.Media.getContentUri(volumeName, id));
712 
713                 // Any changing audio items mean we probably need to invalidate all
714                 // indexed views built from that media
715                 consumer.accept(Audio.Genres.getContentUri(volumeName));
716                 consumer.accept(Audio.Playlists.getContentUri(volumeName));
717                 consumer.accept(Audio.Artists.getContentUri(volumeName));
718                 consumer.accept(Audio.Albums.getContentUri(volumeName));
719                 break;
720 
721             case FileColumns.MEDIA_TYPE_VIDEO:
722                 consumer.accept(MediaStore.Video.Media.getContentUri(volumeName, id));
723                 break;
724 
725             case FileColumns.MEDIA_TYPE_IMAGE:
726                 consumer.accept(MediaStore.Images.Media.getContentUri(volumeName, id));
727                 break;
728         }
729 
730         // Also notify through any generic views
731         consumer.accept(MediaStore.Files.getContentUri(volumeName, id));
732         if (isDownload) {
733             consumer.accept(MediaStore.Downloads.getContentUri(volumeName, id));
734         }
735 
736         // Rinse and repeat through any synthetic views
737         switch (volumeName) {
738             case MediaStore.VOLUME_INTERNAL:
739             case MediaStore.VOLUME_EXTERNAL:
740                 // Already a top-level view, no need to expand
741                 break;
742             default:
743                 acceptWithExpansion(consumer, MediaStore.VOLUME_EXTERNAL,
744                         id, mediaType, isDownload);
745                 break;
746         }
747     }
748 
749     private static final String[] sDefaultFolderNames = {
750             Environment.DIRECTORY_MUSIC,
751             Environment.DIRECTORY_PODCASTS,
752             Environment.DIRECTORY_RINGTONES,
753             Environment.DIRECTORY_ALARMS,
754             Environment.DIRECTORY_NOTIFICATIONS,
755             Environment.DIRECTORY_PICTURES,
756             Environment.DIRECTORY_MOVIES,
757             Environment.DIRECTORY_DOWNLOADS,
758             Environment.DIRECTORY_DCIM,
759             Environment.DIRECTORY_AUDIOBOOKS,
760             Environment.DIRECTORY_DOCUMENTS,
761     };
762 
isDefaultDirectoryName(@ullable String dirName)763     private static boolean isDefaultDirectoryName(@Nullable String dirName) {
764         for (String defaultDirName : sDefaultFolderNames) {
765             if (defaultDirName.equals(dirName)) {
766                 return true;
767             }
768         }
769         return false;
770     }
771 
772     /**
773      * Ensure that default folders are created on mounted primary storage
774      * devices. We only do this once per volume so we don't annoy the user if
775      * deleted manually.
776      */
ensureDefaultFolders(@onNull String volumeName, @NonNull SQLiteDatabase db)777     private void ensureDefaultFolders(@NonNull String volumeName, @NonNull SQLiteDatabase db) {
778         try {
779             final File path = getVolumePath(volumeName);
780             final StorageVolume vol = mStorageManager.getStorageVolume(path);
781             final String key;
782             if (vol == null) {
783                 Log.w(TAG, "Failed to ensure default folders for " + volumeName);
784                 return;
785             }
786 
787             if (vol.isPrimary()) {
788                 key = "created_default_folders";
789             } else {
790                 key = "created_default_folders_" + vol.getMediaStoreVolumeName();
791             }
792 
793             final SharedPreferences prefs = PreferenceManager
794                     .getDefaultSharedPreferences(getContext());
795             if (prefs.getInt(key, 0) == 0) {
796                 for (String folderName : sDefaultFolderNames) {
797                     final File folder = new File(vol.getDirectory(), folderName);
798                     if (!folder.exists()) {
799                         folder.mkdirs();
800                         insertDirectory(db, folder.getAbsolutePath());
801                     }
802                 }
803 
804                 SharedPreferences.Editor editor = prefs.edit();
805                 editor.putInt(key, 1);
806                 editor.commit();
807             }
808         } catch (IOException e) {
809             Log.w(TAG, "Failed to ensure default folders for " + volumeName, e);
810         }
811     }
812 
813     /**
814      * Ensure that any thumbnail collections on the given storage volume can be
815      * used with the given {@link DatabaseHelper}. If the
816      * {@link DatabaseHelper#getOrCreateUuid} doesn't match the UUID found on
817      * disk, then all thumbnails will be considered stable and will be deleted.
818      */
ensureThumbnailsValid(@onNull String volumeName, @NonNull SQLiteDatabase db)819     private void ensureThumbnailsValid(@NonNull String volumeName, @NonNull SQLiteDatabase db) {
820         final String uuidFromDatabase = DatabaseHelper.getOrCreateUuid(db);
821         try {
822             for (File dir : getThumbnailDirectories(volumeName)) {
823                 if (!dir.exists()) {
824                     dir.mkdirs();
825                 }
826 
827                 final File file = new File(dir, FILE_DATABASE_UUID);
828                 final Optional<String> uuidFromDisk = FileUtils.readString(file);
829 
830                 final boolean updateUuid;
831                 if (!uuidFromDisk.isPresent()) {
832                     // For newly inserted volumes or upgrading of existing volumes,
833                     // assume that our current UUID is valid
834                     updateUuid = true;
835                 } else if (!Objects.equals(uuidFromDatabase, uuidFromDisk.get())) {
836                     // The UUID of database disagrees with the one on disk,
837                     // which means we can't trust any thumbnails
838                     Log.d(TAG, "Invalidating all thumbnails under " + dir);
839                     FileUtils.walkFileTreeContents(dir.toPath(), this::deleteAndInvalidate);
840                     updateUuid = true;
841                 } else {
842                     updateUuid = false;
843                 }
844 
845                 if (updateUuid) {
846                     FileUtils.writeString(file, Optional.of(uuidFromDatabase));
847                 }
848             }
849         } catch (IOException e) {
850             Log.w(TAG, "Failed to ensure thumbnails valid for " + volumeName, e);
851         }
852     }
853 
854     @Override
attachInfo(Context context, ProviderInfo info)855     public void attachInfo(Context context, ProviderInfo info) {
856         Log.v(TAG, "Attached " + info.authority + " from " + info.applicationInfo.packageName);
857 
858         mUriMatcher = new LocalUriMatcher(info.authority);
859 
860         super.attachInfo(context, info);
861     }
862 
863     @Override
onCreate()864     public boolean onCreate() {
865         final Context context = getContext();
866 
867         // Shift call statistics back to the original caller
868         Binder.setProxyTransactListener(mTransactListener);
869 
870         mStorageManager = context.getSystemService(StorageManager.class);
871         mAppOpsManager = context.getSystemService(AppOpsManager.class);
872         mPackageManager = context.getPackageManager();
873 
874         // Reasonable thumbnail size is half of the smallest screen edge width
875         final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
876         final int thumbSize = Math.min(metrics.widthPixels, metrics.heightPixels) / 2;
877         mThumbSize = new Size(thumbSize, thumbSize);
878 
879         mMediaScanner = new ModernMediaScanner(context);
880 
881         mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME,
882                 true, false, false, Column.class,
883                 Metrics::logSchemaChange, mFilesListener, mMigrationListener, mIdGenerator);
884         mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME,
885                 false, false, false, Column.class,
886                 Metrics::logSchemaChange, mFilesListener, mMigrationListener, mIdGenerator);
887 
888         final IntentFilter packageFilter = new IntentFilter();
889         packageFilter.setPriority(10);
890         packageFilter.addDataScheme("package");
891         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
892         packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
893         context.registerReceiver(mPackageReceiver, packageFilter);
894 
895         // Watch for invalidation of cached volumes
896         mStorageManager.registerStorageVolumeCallback(context.getMainExecutor(),
897                 new StorageVolumeCallback() {
898                     @Override
899                     public void onStateChanged(@NonNull StorageVolume volume) {
900                         updateVolumes();
901                    }
902                 });
903 
904         updateVolumes();
905         attachVolume(MediaStore.VOLUME_INTERNAL, /* validate */ false);
906         for (String volumeName : getExternalVolumeNames()) {
907             attachVolume(volumeName, /* validate */ false);
908         }
909 
910         // Watch for performance-sensitive activity
911         mAppOpsManager.startWatchingActive(new String[] {
912                 AppOpsManager.OPSTR_CAMERA
913         }, context.getMainExecutor(), mActiveListener);
914 
915         mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE,
916                 null /* all packages */, mModeListener);
917         mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE,
918                 null /* all packages */, mModeListener);
919         mAppOpsManager.startWatchingMode(permissionToOp(ACCESS_MEDIA_LOCATION),
920                 null /* all packages */, mModeListener);
921         // Legacy apps
922         mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_LEGACY_STORAGE,
923                 null /* all packages */, mModeListener);
924         // File managers
925         mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE,
926                 null /* all packages */, mModeListener);
927         // Default gallery changes
928         mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES,
929                 null /* all packages */, mModeListener);
930         mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO,
931                 null /* all packages */, mModeListener);
932         try {
933             // Here we are forced to depend on the non-public API of AppOpsManager. If
934             // OPSTR_NO_ISOLATED_STORAGE app op is not defined in AppOpsManager, then this call will
935             // throw an IllegalArgumentException during MediaProvider startup. In combination with
936             // MediaProvider's CTS tests it should give us guarantees that OPSTR_NO_ISOLATED_STORAGE
937             // is defined.
938             mAppOpsManager.startWatchingMode(PermissionUtils.OPSTR_NO_ISOLATED_STORAGE,
939                     null /* all packages */, mModeListener);
940         } catch (IllegalArgumentException e) {
941             Log.w(TAG, "Failed to start watching " + PermissionUtils.OPSTR_NO_ISOLATED_STORAGE, e);
942         }
943         return true;
944     }
945 
946     @Override
onCallingPackageChanged()947     public void onCallingPackageChanged() {
948         // Identity of the current thread has changed, so invalidate caches
949         mCallingIdentity.remove();
950     }
951 
clearLocalCallingIdentity()952     public LocalCallingIdentity clearLocalCallingIdentity() {
953         return clearLocalCallingIdentity(LocalCallingIdentity.fromSelf(getContext()));
954     }
955 
clearLocalCallingIdentity(LocalCallingIdentity replacement)956     public LocalCallingIdentity clearLocalCallingIdentity(LocalCallingIdentity replacement) {
957         final LocalCallingIdentity token = mCallingIdentity.get();
958         mCallingIdentity.set(replacement);
959         return token;
960     }
961 
restoreLocalCallingIdentity(LocalCallingIdentity token)962     public void restoreLocalCallingIdentity(LocalCallingIdentity token) {
963         mCallingIdentity.set(token);
964     }
965 
isPackageKnown(@onNull String packageName)966     private boolean isPackageKnown(@NonNull String packageName) {
967         final PackageManager pm = getContext().getPackageManager();
968 
969         // First, is the app actually installed?
970         try {
971             pm.getPackageInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES);
972             return true;
973         } catch (NameNotFoundException ignored) {
974         }
975 
976         // Second, is the app pending, probably from a backup/restore operation?
977         for (SessionInfo si : pm.getPackageInstaller().getAllSessions()) {
978             if (Objects.equals(packageName, si.getAppPackageName())) {
979                 return true;
980             }
981         }
982 
983         // I've never met this package in my life
984         return false;
985     }
986 
onIdleMaintenance(@onNull CancellationSignal signal)987     public void onIdleMaintenance(@NonNull CancellationSignal signal) {
988         final long startTime = SystemClock.elapsedRealtime();
989 
990         // Trim any stale log files before we emit new events below
991         Logging.trimPersistent();
992 
993         // Scan all volumes to resolve any staleness
994         for (String volumeName : getExternalVolumeNames()) {
995             // Possibly bail before digging into each volume
996             signal.throwIfCanceled();
997 
998             try {
999                 MediaService.onScanVolume(getContext(), volumeName, REASON_IDLE);
1000             } catch (IOException e) {
1001                 Log.w(TAG, e);
1002             }
1003 
1004             // Ensure that our thumbnails are valid
1005             mExternalDatabase.runWithTransaction((db) -> {
1006                 ensureThumbnailsValid(volumeName, db);
1007                 return null;
1008             });
1009         }
1010 
1011         // Delete any stale thumbnails
1012         final int staleThumbnails = mExternalDatabase.runWithTransaction((db) -> {
1013             return pruneThumbnails(db, signal);
1014         });
1015         Log.d(TAG, "Pruned " + staleThumbnails + " unknown thumbnails");
1016 
1017         // Finished orphaning any content whose package no longer exists
1018         final int stalePackages = mExternalDatabase.runWithTransaction((db) -> {
1019             final ArraySet<String> unknownPackages = new ArraySet<>();
1020             try (Cursor c = db.query(true, "files", new String[] { "owner_package_name" },
1021                     null, null, null, null, null, null, signal)) {
1022                 while (c.moveToNext()) {
1023                     final String packageName = c.getString(0);
1024                     if (TextUtils.isEmpty(packageName)) continue;
1025 
1026                     if (!isPackageKnown(packageName)) {
1027                         unknownPackages.add(packageName);
1028                     }
1029                 }
1030             }
1031             for (String packageName : unknownPackages) {
1032                 onPackageOrphaned(db, packageName);
1033             }
1034             return unknownPackages.size();
1035         });
1036         Log.d(TAG, "Pruned " + stalePackages + " unknown packages");
1037 
1038         // Delete any expired content; we're paranoid about wildly changing
1039         // clocks, so only delete items within the last week
1040         final long from = ((System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS) / 1000);
1041         final long to = (System.currentTimeMillis() / 1000);
1042         final int expiredMedia = mExternalDatabase.runWithTransaction((db) -> {
1043             try (Cursor c = db.query(true, "files", new String[] { "volume_name", "_id" },
1044                     FileColumns.DATE_EXPIRES + " BETWEEN " + from + " AND " + to, null,
1045                     null, null, null, null, signal)) {
1046                 while (c.moveToNext()) {
1047                     final String volumeName = c.getString(0);
1048                     final long id = c.getLong(1);
1049                     delete(Files.getContentUri(volumeName, id), null, null);
1050                 }
1051                 return c.getCount();
1052             }
1053         });
1054         Log.d(TAG, "Deleted " + expiredMedia + " expired items");
1055 
1056         // Forget any stale volumes
1057         mExternalDatabase.runWithTransaction((db) -> {
1058             final Set<String> recentVolumeNames = MediaStore
1059                     .getRecentExternalVolumeNames(getContext());
1060             final Set<String> knownVolumeNames = new ArraySet<>();
1061             try (Cursor c = db.query(true, "files", new String[] { MediaColumns.VOLUME_NAME },
1062                     null, null, null, null, null, null, signal)) {
1063                 while (c.moveToNext()) {
1064                     knownVolumeNames.add(c.getString(0));
1065                 }
1066             }
1067             final Set<String> staleVolumeNames = new ArraySet<>();
1068             staleVolumeNames.addAll(knownVolumeNames);
1069             staleVolumeNames.removeAll(recentVolumeNames);
1070             for (String staleVolumeName : staleVolumeNames) {
1071                 final int num = db.delete("files", FileColumns.VOLUME_NAME + "=?",
1072                         new String[] { staleVolumeName });
1073                 Log.d(TAG, "Forgot " + num + " stale items from " + staleVolumeName);
1074             }
1075             return null;
1076         });
1077 
1078         synchronized (mDirectoryCache) {
1079             mDirectoryCache.clear();
1080         }
1081 
1082         final long itemCount = mExternalDatabase.runWithTransaction((db) -> {
1083             return DatabaseHelper.getItemCount(db);
1084         });
1085 
1086         final long durationMillis = (SystemClock.elapsedRealtime() - startTime);
1087         Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount,
1088                 durationMillis, staleThumbnails, expiredMedia);
1089     }
1090 
onPackageOrphaned(String packageName)1091     public void onPackageOrphaned(String packageName) {
1092         mExternalDatabase.runWithTransaction((db) -> {
1093             onPackageOrphaned(db, packageName);
1094             return null;
1095         });
1096     }
1097 
onPackageOrphaned(@onNull SQLiteDatabase db, @NonNull String packageName)1098     public void onPackageOrphaned(@NonNull SQLiteDatabase db, @NonNull String packageName) {
1099         final ContentValues values = new ContentValues();
1100         values.putNull(FileColumns.OWNER_PACKAGE_NAME);
1101 
1102         final int count = db.update("files", values,
1103                 "owner_package_name=?", new String[] { packageName });
1104         if (count > 0) {
1105             Log.d(TAG, "Orphaned " + count + " items belonging to "
1106                     + packageName + " on " + db.getPath());
1107         }
1108     }
1109 
scanDirectory(File file, int reason)1110     public void scanDirectory(File file, int reason) {
1111         mMediaScanner.scanDirectory(file, reason);
1112     }
1113 
scanFile(File file, int reason)1114     public Uri scanFile(File file, int reason) {
1115         return mMediaScanner.scanFile(file, reason);
1116     }
1117 
scanFile(File file, int reason, String ownerPackage)1118     public Uri scanFile(File file, int reason, String ownerPackage) {
1119         return mMediaScanner.scanFile(file, reason, ownerPackage);
1120     }
1121 
1122     /**
1123      * Makes MediaScanner scan the given file.
1124      * @param file path of the file to be scanned
1125      *
1126      * Called from JNI in jni/MediaProviderWrapper.cpp
1127      */
1128     @Keep
scanFileForFuse(String file)1129     public void scanFileForFuse(String file) {
1130         scanFile(new File(file), REASON_DEMAND);
1131     }
1132 
1133     /**
1134      * Called when a new file is created through FUSE
1135      *
1136      * @param file path of the file that was created
1137      *
1138      * Called from JNI in jni/MediaProviderWrapper.cpp
1139      */
1140     @Keep
onFileCreatedForFuse(String path)1141     public void onFileCreatedForFuse(String path) {
1142         // Make sure we update the quota type of the file
1143         BackgroundThread.getExecutor().execute(() -> {
1144             File file = new File(path);
1145             int mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
1146             updateQuotaTypeForFileInternal(file, mediaType);
1147         });
1148     }
1149 
1150     /**
1151      * Returns true if the app denoted by the given {@code uid} and {@code packageName} is allowed
1152      * to clear other apps' cache directories.
1153      */
hasPermissionToClearCaches(Context context, ApplicationInfo ai)1154     static boolean hasPermissionToClearCaches(Context context, ApplicationInfo ai) {
1155         PermissionUtils.setOpDescription("clear app cache");
1156         try {
1157             return PermissionUtils.checkPermissionManager(context, /* pid */ -1, ai.uid,
1158                     ai.packageName, /* attributionTag */ null);
1159         } finally {
1160             PermissionUtils.clearOpDescription();
1161         }
1162     }
1163 
1164     @VisibleForTesting
computeAudioLocalizedValues(ContentValues values)1165     void computeAudioLocalizedValues(ContentValues values) {
1166         try {
1167             final String title = values.getAsString(AudioColumns.TITLE);
1168             final String titleRes = values.getAsString(AudioColumns.TITLE_RESOURCE_URI);
1169 
1170             if (!TextUtils.isEmpty(titleRes)) {
1171                 final String localized = getLocalizedTitle(titleRes);
1172                 if (!TextUtils.isEmpty(localized)) {
1173                     values.put(AudioColumns.TITLE, localized);
1174                 }
1175             } else {
1176                 final String localized = getLocalizedTitle(title);
1177                 if (!TextUtils.isEmpty(localized)) {
1178                     values.put(AudioColumns.TITLE, localized);
1179                     values.put(AudioColumns.TITLE_RESOURCE_URI, title);
1180                 }
1181             }
1182         } catch (Exception e) {
1183             Log.w(TAG, "Failed to localize title", e);
1184         }
1185     }
1186 
1187     @VisibleForTesting
computeAudioKeyValues(ContentValues values)1188     static void computeAudioKeyValues(ContentValues values) {
1189         computeAudioKeyValue(values,
1190                 AudioColumns.TITLE, AudioColumns.TITLE_KEY, null);
1191         computeAudioKeyValue(values,
1192                 AudioColumns.ALBUM, AudioColumns.ALBUM_KEY, AudioColumns.ALBUM_ID);
1193         computeAudioKeyValue(values,
1194                 AudioColumns.ARTIST, AudioColumns.ARTIST_KEY, AudioColumns.ARTIST_ID);
1195         computeAudioKeyValue(values,
1196                 AudioColumns.GENRE, AudioColumns.GENRE_KEY, AudioColumns.GENRE_ID);
1197     }
1198 
computeAudioKeyValue(@onNull ContentValues values, @NonNull String focus, @Nullable String focusKey, @Nullable String focusId)1199     private static void computeAudioKeyValue(@NonNull ContentValues values, @NonNull String focus,
1200             @Nullable String focusKey, @Nullable String focusId) {
1201         if (focusKey != null) values.remove(focusKey);
1202         if (focusId != null) values.remove(focusId);
1203 
1204         final String value = values.getAsString(focus);
1205         if (TextUtils.isEmpty(value)) return;
1206 
1207         final String key = Audio.keyFor(value);
1208         if (key == null) return;
1209 
1210         if (focusKey != null) {
1211             values.put(focusKey, key);
1212         }
1213         if (focusId != null) {
1214             // Many apps break if we generate negative IDs, so trim off the
1215             // highest bit to ensure we're always unsigned
1216             final long id = Hashing.farmHashFingerprint64()
1217                     .hashString(key, StandardCharsets.UTF_8).asLong() & ~(1L << 63);
1218             values.put(focusId, id);
1219         }
1220     }
1221 
1222     @Override
canonicalize(Uri uri)1223     public Uri canonicalize(Uri uri) {
1224         final boolean allowHidden = isCallingPackageAllowedHidden();
1225         final int match = matchUri(uri, allowHidden);
1226 
1227         // Skip when we have nothing to canonicalize
1228         if ("1".equals(uri.getQueryParameter(CANONICAL))) {
1229             return uri;
1230         }
1231 
1232         try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
1233             switch (match) {
1234                 case AUDIO_MEDIA_ID: {
1235                     final String title = getDefaultTitleFromCursor(c);
1236                     if (!TextUtils.isEmpty(title)) {
1237                         final Uri.Builder builder = uri.buildUpon();
1238                         builder.appendQueryParameter(AudioColumns.TITLE, title);
1239                         builder.appendQueryParameter(CANONICAL, "1");
1240                         return builder.build();
1241                     }
1242                     break;
1243                 }
1244                 case VIDEO_MEDIA_ID:
1245                 case IMAGES_MEDIA_ID: {
1246                     final String documentId = c
1247                             .getString(c.getColumnIndexOrThrow(MediaColumns.DOCUMENT_ID));
1248                     if (!TextUtils.isEmpty(documentId)) {
1249                         final Uri.Builder builder = uri.buildUpon();
1250                         builder.appendQueryParameter(MediaColumns.DOCUMENT_ID, documentId);
1251                         builder.appendQueryParameter(CANONICAL, "1");
1252                         return builder.build();
1253                     }
1254                     break;
1255                 }
1256             }
1257         } catch (FileNotFoundException e) {
1258             Log.w(TAG, e.getMessage());
1259         }
1260         return null;
1261     }
1262 
1263     @Override
uncanonicalize(Uri uri)1264     public Uri uncanonicalize(Uri uri) {
1265         final boolean allowHidden = isCallingPackageAllowedHidden();
1266         final int match = matchUri(uri, allowHidden);
1267 
1268         // Skip when we have nothing to uncanonicalize
1269         if (!"1".equals(uri.getQueryParameter(CANONICAL))) {
1270             return uri;
1271         }
1272 
1273         // Extract values and then clear to avoid recursive lookups
1274         final String title = uri.getQueryParameter(AudioColumns.TITLE);
1275         final String documentId = uri.getQueryParameter(MediaColumns.DOCUMENT_ID);
1276         uri = uri.buildUpon().clearQuery().build();
1277 
1278         switch (match) {
1279             case AUDIO_MEDIA_ID: {
1280                 // First check for an exact match
1281                 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
1282                     if (Objects.equals(title, getDefaultTitleFromCursor(c))) {
1283                         return uri;
1284                     }
1285                 } catch (FileNotFoundException e) {
1286                     Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e);
1287                 }
1288 
1289                 // Otherwise fallback to searching
1290                 final Uri baseUri = ContentUris.removeId(uri);
1291                 try (Cursor c = queryForSingleItem(baseUri,
1292                         new String[] { BaseColumns._ID },
1293                         AudioColumns.TITLE + "=?", new String[] { title }, null)) {
1294                     return ContentUris.withAppendedId(baseUri, c.getLong(0));
1295                 } catch (FileNotFoundException e) {
1296                     Log.w(TAG, "Failed to resolve " + uri + ": " + e);
1297                     return null;
1298                 }
1299             }
1300             case VIDEO_MEDIA_ID:
1301             case IMAGES_MEDIA_ID: {
1302                 // First check for an exact match
1303                 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
1304                     if (Objects.equals(title, getDefaultTitleFromCursor(c))) {
1305                         return uri;
1306                     }
1307                 } catch (FileNotFoundException e) {
1308                     Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e);
1309                 }
1310 
1311                 // Otherwise fallback to searching
1312                 final Uri baseUri = ContentUris.removeId(uri);
1313                 try (Cursor c = queryForSingleItem(baseUri,
1314                         new String[] { BaseColumns._ID },
1315                         MediaColumns.DOCUMENT_ID + "=?", new String[] { documentId }, null)) {
1316                     return ContentUris.withAppendedId(baseUri, c.getLong(0));
1317                 } catch (FileNotFoundException e) {
1318                     Log.w(TAG, "Failed to resolve " + uri + ": " + e);
1319                     return null;
1320                 }
1321             }
1322         }
1323 
1324         return uri;
1325     }
1326 
safeUncanonicalize(Uri uri)1327     private Uri safeUncanonicalize(Uri uri) {
1328         Uri newUri = uncanonicalize(uri);
1329         if (newUri != null) {
1330             return newUri;
1331         }
1332         return uri;
1333     }
1334 
1335     /**
1336      * @return where clause to exclude database rows where
1337      * <ul>
1338      * <li> {@code column} is set or
1339      * <li> {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE and not owned by
1340      * calling package.
1341      * </ul>
1342      */
getWhereClauseForMatchExclude(@onNull String column)1343     private String getWhereClauseForMatchExclude(@NonNull String column) {
1344         if (column.equalsIgnoreCase(MediaColumns.IS_PENDING)) {
1345             final String callingPackage = getCallingPackageOrSelf();
1346             final String matchSharedPackagesClause = FileColumns.OWNER_PACKAGE_NAME + " IN "
1347                     + getSharedPackages();
1348             // Include owned pending files from Fuse
1349             return String.format("%s=0 OR (%s=1 AND %s AND %s)", column, column,
1350                     MATCH_PENDING_FROM_FUSE, matchSharedPackagesClause);
1351         }
1352         return column + "=0";
1353     }
1354 
1355     /**
1356      * @return where clause to include database rows where
1357      * <ul>
1358      * <li> {@code column} is not set or
1359      * <li> {@code column} is set and calling package has write permission to corresponding db row
1360      *      or {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE.
1361      * </ul>
1362      * The method is used to match db rows corresponding to writable pending and trashed files.
1363      */
1364     @Nullable
getWhereClauseForMatchableVisibleFromFilePath(@onNull Uri uri, @NonNull String column)1365     private String getWhereClauseForMatchableVisibleFromFilePath(@NonNull Uri uri,
1366             @NonNull String column) {
1367         if (isCallingPackageLegacyWrite() || checkCallingPermissionGlobal(uri, /*forWrite*/ true)) {
1368             // No special filtering needed
1369             return null;
1370         }
1371 
1372         final String callingPackage = getCallingPackageOrSelf();
1373 
1374         final ArrayList<String> options = new ArrayList<>();
1375         switch(matchUri(uri, isCallingPackageAllowedHidden())) {
1376             case IMAGES_MEDIA_ID:
1377             case IMAGES_MEDIA:
1378             case IMAGES_THUMBNAILS_ID:
1379             case IMAGES_THUMBNAILS:
1380                 if (checkCallingPermissionImages(/*forWrite*/ true, callingPackage)) {
1381                     // No special filtering needed
1382                     return null;
1383                 }
1384                 break;
1385             case AUDIO_MEDIA_ID:
1386             case AUDIO_MEDIA:
1387             case AUDIO_PLAYLISTS_ID:
1388             case AUDIO_PLAYLISTS:
1389                 if (checkCallingPermissionAudio(/*forWrite*/ true, callingPackage)) {
1390                     // No special filtering needed
1391                     return null;
1392                 }
1393                 break;
1394             case VIDEO_MEDIA_ID:
1395             case VIDEO_MEDIA:
1396             case VIDEO_THUMBNAILS_ID:
1397             case VIDEO_THUMBNAILS:
1398                 if (checkCallingPermissionVideo(/*firWrite*/ true, callingPackage)) {
1399                     // No special filtering needed
1400                     return null;
1401                 }
1402                 break;
1403             case DOWNLOADS_ID:
1404             case DOWNLOADS:
1405                 // No app has special permissions for downloads.
1406                 break;
1407             case FILES_ID:
1408             case FILES:
1409                 if (checkCallingPermissionAudio(/*forWrite*/ true, callingPackage)) {
1410                     // Allow apps with audio permission to include audio* media types.
1411                     options.add(DatabaseUtils.bindSelection("media_type=?",
1412                             FileColumns.MEDIA_TYPE_AUDIO));
1413                     options.add(DatabaseUtils.bindSelection("media_type=?",
1414                             FileColumns.MEDIA_TYPE_PLAYLIST));
1415                     options.add(DatabaseUtils.bindSelection("media_type=?",
1416                             FileColumns.MEDIA_TYPE_SUBTITLE));
1417                 }
1418                 if (checkCallingPermissionVideo(/*forWrite*/ true, callingPackage)) {
1419                     // Allow apps with video permission to include video* media types.
1420                     options.add(DatabaseUtils.bindSelection("media_type=?",
1421                             FileColumns.MEDIA_TYPE_VIDEO));
1422                     options.add(DatabaseUtils.bindSelection("media_type=?",
1423                             FileColumns.MEDIA_TYPE_SUBTITLE));
1424                 }
1425                 if (checkCallingPermissionImages(/*forWrite*/ true, callingPackage)) {
1426                     // Allow apps with images permission to include images* media types.
1427                     options.add(DatabaseUtils.bindSelection("media_type=?",
1428                             FileColumns.MEDIA_TYPE_IMAGE));
1429                 }
1430                 break;
1431             default:
1432                 // is_pending, is_trashed are not applicable for rest of the media tables.
1433                 return null;
1434         }
1435 
1436         final String matchSharedPackagesClause = FileColumns.OWNER_PACKAGE_NAME + " IN "
1437                 + getSharedPackages();
1438         options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause));
1439 
1440         if (column.equalsIgnoreCase(MediaColumns.IS_PENDING)) {
1441             // Include all pending files from Fuse
1442             options.add(MATCH_PENDING_FROM_FUSE);
1443         }
1444 
1445         final String matchWritableRowsClause = String.format("%s=0 OR (%s=1 AND %s)", column,
1446                 column, TextUtils.join(" OR ", options));
1447         return matchWritableRowsClause;
1448     }
1449 
1450     /**
1451      * Gets list of files in {@code path} from media provider database.
1452      *
1453      * @param path path of the directory.
1454      * @param uid UID of the calling process.
1455      * @return a list of file names in the given directory path.
1456      * An empty list is returned if no files are visible to the calling app or the given directory
1457      * does not have any files.
1458      * A list with ["/"] is returned if the path is not indexed by MediaProvider database or
1459      * calling package is a legacy app and has appropriate storage permissions for the given path.
1460      * In both scenarios file names should be obtained from lower file system.
1461      * A list with empty string[""] is returned if the calling package doesn't have access to the
1462      * given path.
1463      *
1464      * <p>Directory names are always obtained from lower file system.
1465      *
1466      * Called from JNI in jni/MediaProviderWrapper.cpp
1467      */
1468     @Keep
getFilesInDirectoryForFuse(String path, int uid)1469     public String[] getFilesInDirectoryForFuse(String path, int uid) {
1470         final LocalCallingIdentity token =
1471                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
1472 
1473         try {
1474             if (isPrivatePackagePathNotOwnedByCaller(path)) {
1475                 return new String[] {""};
1476             }
1477 
1478             // Do not allow apps to list Android/data or Android/obb dirs. Installer and
1479             // MOUNT_EXTERNAL_ANDROID_WRITABLE apps won't be blocked by this, as their OBB dirs
1480             // are mounted to lowerfs directly.
1481             if (isDataOrObbPath(path)) {
1482                 return new String[] {""};
1483             }
1484 
1485             if (shouldBypassFuseRestrictions(/*forWrite*/ false, path)) {
1486                 return new String[] {"/"};
1487             }
1488             // Legacy apps that made is this far don't have the right storage permission and hence
1489             // are not allowed to access anything other than their external app directory
1490             if (isCallingPackageRequestingLegacy()) {
1491                 return new String[] {""};
1492             }
1493 
1494             // Get relative path for the contents of given directory.
1495             String relativePath = extractRelativePathForDirectory(path);
1496 
1497             if (relativePath == null) {
1498                 // Path is /storage/emulated/, if relativePath is null, MediaProvider doesn't
1499                 // have any details about the given directory. Use lower file system to obtain
1500                 // files and directories in the given directory.
1501                 return new String[] {"/"};
1502             }
1503 
1504             // For all other paths, get file names from media provider database.
1505             // Return media and non-media files visible to the calling package.
1506             ArrayList<String> fileNamesList = new ArrayList<>();
1507 
1508             // Only FileColumns.DATA contains actual name of the file.
1509             String[] projection = {MediaColumns.DATA};
1510 
1511             Bundle queryArgs = new Bundle();
1512             queryArgs.putString(QUERY_ARG_SQL_SELECTION, MediaColumns.RELATIVE_PATH +
1513                     " =? and mime_type not like 'null'");
1514             queryArgs.putStringArray(QUERY_ARG_SQL_SELECTION_ARGS, new String[] {relativePath});
1515             // Get database entries for files from MediaProvider database with
1516             // MediaColumns.RELATIVE_PATH as the given path.
1517             try (final Cursor cursor = query(FileUtils.getContentUriForPath(path), projection,
1518                     queryArgs, null)) {
1519                 while(cursor.moveToNext()) {
1520                     fileNamesList.add(extractDisplayName(cursor.getString(0)));
1521                 }
1522             }
1523             return fileNamesList.toArray(new String[fileNamesList.size()]);
1524         } finally {
1525             restoreLocalCallingIdentity(token);
1526         }
1527     }
1528 
1529     /**
1530      * Scan files during directory renames for the following reasons:
1531      * <ul>
1532      * <li>Because we don't update db rows for directories, we scan the oldPath to discard stale
1533      * directory db rows. This prevents conflicts during subsequent db operations with oldPath.
1534      * <li>We need to scan newPath as well, because the new directory may have become hidden
1535      * or unhidden, in which case we need to update the media types of the contained files
1536      * </ul>
1537      */
scanRenamedDirectoryForFuse(@onNull String oldPath, @NonNull String newPath)1538     private void scanRenamedDirectoryForFuse(@NonNull String oldPath, @NonNull String newPath) {
1539         final LocalCallingIdentity token = clearLocalCallingIdentity();
1540         try {
1541             scanFile(new File(oldPath), REASON_DEMAND);
1542             scanFile(new File(newPath), REASON_DEMAND);
1543         } finally {
1544             restoreLocalCallingIdentity(token);
1545         }
1546     }
1547 
1548     /**
1549      * Checks if given {@code mimeType} is supported in {@code path}.
1550      */
isMimeTypeSupportedInPath(String path, String mimeType)1551     private boolean isMimeTypeSupportedInPath(String path, String mimeType) {
1552         final String supportedPrimaryMimeType;
1553         final int match = matchUri(getContentUriForFile(path, mimeType), true);
1554         switch (match) {
1555             case AUDIO_MEDIA:
1556                 supportedPrimaryMimeType = "audio";
1557                 break;
1558             case VIDEO_MEDIA:
1559                 supportedPrimaryMimeType = "video";
1560                 break;
1561             case IMAGES_MEDIA:
1562                 supportedPrimaryMimeType = "image";
1563                 break;
1564             default:
1565                 supportedPrimaryMimeType = ClipDescription.MIMETYPE_UNKNOWN;
1566         }
1567         return (supportedPrimaryMimeType.equalsIgnoreCase(ClipDescription.MIMETYPE_UNKNOWN) ||
1568                 MimeUtils.startsWithIgnoreCase(mimeType, supportedPrimaryMimeType));
1569     }
1570 
1571     /**
1572      * Removes owner package for the renamed path if the calling package doesn't own the db row
1573      *
1574      * When oldPath is renamed to newPath, if newPath exists in the database, and caller is not the
1575      * owner of the file, owner package is set to 'null'. This prevents previous owner of newPath
1576      * from accessing renamed file.
1577      * @return {@code true} if
1578      * <ul>
1579      * <li> there is no corresponding database row for given {@code path}
1580      * <li> shared calling package is the owner of the database row
1581      * <li> owner package name is already set to 'null'
1582      * <li> updating owner package name to 'null' was successful.
1583      * </ul>
1584      * Returns {@code false} otherwise.
1585      */
maybeRemoveOwnerPackageForFuseRename(@onNull DatabaseHelper helper, @NonNull String path)1586     private boolean maybeRemoveOwnerPackageForFuseRename(@NonNull DatabaseHelper helper,
1587             @NonNull String path) {
1588 
1589         final Uri uri = FileUtils.getContentUriForPath(path);
1590         final int match = matchUri(uri, isCallingPackageAllowedHidden());
1591         final String ownerPackageName;
1592         final String selection = MediaColumns.DATA + " =? AND "
1593                 + MediaColumns.OWNER_PACKAGE_NAME + " != 'null'";
1594         final String[] selectionArgs = new String[] {path};
1595 
1596         final SQLiteQueryBuilder qbForQuery =
1597                 getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, null);
1598         try (Cursor c = qbForQuery.query(helper, new String[] {FileColumns.OWNER_PACKAGE_NAME},
1599                 selection, selectionArgs, null, null, null, null, null)) {
1600             if (!c.moveToFirst()) {
1601                 // We don't need to remove owner_package from db row if path doesn't exist in
1602                 // database or owner_package is already set to 'null'
1603                 return true;
1604             }
1605             ownerPackageName = c.getString(0);
1606             if (isCallingIdentitySharedPackageName(ownerPackageName)) {
1607                 // We don't need to remove owner_package from db row if calling package is the owner
1608                 // of the database row
1609                 return true;
1610             }
1611         }
1612 
1613         final SQLiteQueryBuilder qbForUpdate =
1614                 getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null);
1615         ContentValues values = new ContentValues();
1616         values.put(FileColumns.OWNER_PACKAGE_NAME, "null");
1617         return qbForUpdate.update(helper, values, selection, selectionArgs) == 1;
1618     }
1619 
updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values)1620     private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
1621             @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) {
1622         return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY);
1623     }
1624 
1625     /**
1626      * Updates database entry for given {@code path} with {@code values}
1627      */
updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, @NonNull Bundle qbExtras)1628     private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
1629             @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values,
1630             @NonNull Bundle qbExtras) {
1631         final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath);
1632         boolean allowHidden = isCallingPackageAllowedHidden();
1633         final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE,
1634                 matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null);
1635         final String selection = MediaColumns.DATA + " =? ";
1636         int count = 0;
1637         boolean retryUpdateWithReplace = false;
1638 
1639         try {
1640             // TODO(b/146777893): System gallery apps can rename a media directory containing
1641             // non-media files. This update doesn't support updating non-media files that are not
1642             // owned by system gallery app.
1643             count = qbForUpdate.update(helper, values, selection, new String[]{oldPath});
1644         } catch (SQLiteConstraintException e) {
1645             Log.w(TAG, "Database update failed while renaming " + oldPath, e);
1646             retryUpdateWithReplace = true;
1647         }
1648 
1649         if (retryUpdateWithReplace) {
1650             // We are replacing file in newPath with file in oldPath. If calling package has
1651             // write permission for newPath, delete existing database entry and retry update.
1652             final Uri uriNewPath = FileUtils.getContentUriForPath(oldPath);
1653             final SQLiteQueryBuilder qbForDelete = getQueryBuilder(TYPE_DELETE,
1654                     matchUri(uriNewPath, allowHidden), uriNewPath, qbExtras, null);
1655             if (qbForDelete.delete(helper, selection, new String[] {newPath}) == 1) {
1656                 Log.i(TAG, "Retrying database update after deleting conflicting entry");
1657                 count = qbForUpdate.update(helper, values, selection, new String[]{oldPath});
1658             } else {
1659                 return false;
1660             }
1661         }
1662         return count == 1;
1663     }
1664 
1665     /**
1666      * Gets {@link ContentValues} for updating database entry to {@code path}.
1667      */
getContentValuesForFuseRename(String path, String newMimeType, boolean checkHidden)1668     private ContentValues getContentValuesForFuseRename(String path, String newMimeType,
1669             boolean checkHidden) {
1670         ContentValues values = new ContentValues();
1671         values.put(MediaColumns.MIME_TYPE, newMimeType);
1672         values.put(MediaColumns.DATA, path);
1673 
1674         if (checkHidden && shouldFileBeHidden(new File(path))) {
1675             values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
1676         } else {
1677             int mediaType = MimeUtils.resolveMediaType(newMimeType);
1678             values.put(FileColumns.MEDIA_TYPE, mediaType);
1679         }
1680         final boolean allowHidden = isCallingPackageAllowedHidden();
1681         if (!newMimeType.equalsIgnoreCase("null") &&
1682                 matchUri(getContentUriForFile(path, newMimeType), allowHidden) == AUDIO_MEDIA) {
1683             computeAudioLocalizedValues(values);
1684             computeAudioKeyValues(values);
1685         }
1686         FileUtils.computeValuesFromData(values, isFuseThread());
1687         return values;
1688     }
1689 
getIncludedDefaultDirectories()1690     private ArrayList<String> getIncludedDefaultDirectories() {
1691         final ArrayList<String> includedDefaultDirs = new ArrayList<>();
1692         if (checkCallingPermissionVideo(/*forWrite*/ true, null)) {
1693             includedDefaultDirs.add(DIRECTORY_DCIM);
1694             includedDefaultDirs.add(DIRECTORY_PICTURES);
1695             includedDefaultDirs.add(DIRECTORY_MOVIES);
1696         } else if (checkCallingPermissionImages(/*forWrite*/ true, null)) {
1697             includedDefaultDirs.add(DIRECTORY_DCIM);
1698             includedDefaultDirs.add(DIRECTORY_PICTURES);
1699         }
1700         return includedDefaultDirs;
1701     }
1702 
1703     /**
1704      * Gets all files in the given {@code path} and subdirectories of the given {@code path}.
1705      */
getAllFilesForRenameDirectory(String oldPath)1706     private ArrayList<String> getAllFilesForRenameDirectory(String oldPath) {
1707         final String selection = MediaColumns.RELATIVE_PATH + " REGEXP '^" +
1708                 extractRelativePathForDirectory(oldPath) + "/?.*' and mime_type not like 'null'";
1709         ArrayList<String> fileList = new ArrayList<>();
1710 
1711         final LocalCallingIdentity token = clearLocalCallingIdentity();
1712         try (final Cursor c = query(FileUtils.getContentUriForPath(oldPath),
1713                 new String[] {MediaColumns.DATA}, selection, null, null)) {
1714             while (c.moveToNext()) {
1715                 final String filePath = c.getString(0).replaceFirst("^" + oldPath + "/(.*)", "$1");
1716                 fileList.add(filePath);
1717             }
1718         } finally {
1719             restoreLocalCallingIdentity(token);
1720         }
1721         return fileList;
1722     }
1723 
1724     /**
1725      * Gets files in the given {@code path} and subdirectories of the given {@code path} for which
1726      * calling package has write permissions.
1727      *
1728      * This method throws {@code IllegalArgumentException} if the directory has one or more
1729      * files for which calling package doesn't have write permission or if file type is not
1730      * supported in {@code newPath}
1731      */
getWritableFilesForRenameDirectory(String oldPath, String newPath)1732     private ArrayList<String> getWritableFilesForRenameDirectory(String oldPath, String newPath)
1733             throws IllegalArgumentException {
1734         // Try a simple check to see if the caller has full access to the given collections first
1735         // before falling back to performing a query to probe for access.
1736         final String oldRelativePath = extractRelativePathForDirectory(oldPath);
1737         final String newRelativePath = extractRelativePathForDirectory(newPath);
1738         boolean hasFullAccessToOldPath = false;
1739         boolean hasFullAccessToNewPath = false;
1740         for (String defaultDir : getIncludedDefaultDirectories()) {
1741             if (oldRelativePath.startsWith(defaultDir)) hasFullAccessToOldPath = true;
1742             if (newRelativePath.startsWith(defaultDir)) hasFullAccessToNewPath = true;
1743         }
1744         if (hasFullAccessToNewPath && hasFullAccessToOldPath) {
1745             return getAllFilesForRenameDirectory(oldPath);
1746         }
1747 
1748         final int countAllFilesInDirectory;
1749         final String selection = MediaColumns.RELATIVE_PATH + " REGEXP '^" +
1750                 extractRelativePathForDirectory(oldPath) + "/?.*' and mime_type not like 'null'";
1751         final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath);
1752 
1753         final LocalCallingIdentity token = clearLocalCallingIdentity();
1754         try (final Cursor c = query(uriOldPath, new String[] {MediaColumns._ID}, selection, null,
1755                 null)) {
1756             // get actual number of files in the given directory.
1757             countAllFilesInDirectory = c.getCount();
1758         } finally {
1759             restoreLocalCallingIdentity(token);
1760         }
1761 
1762         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE,
1763                 matchUri(uriOldPath, isCallingPackageAllowedHidden()), uriOldPath, Bundle.EMPTY,
1764                 null);
1765         final DatabaseHelper helper;
1766         try {
1767             helper = getDatabaseForUri(uriOldPath);
1768         } catch (VolumeNotFoundException e) {
1769             throw new IllegalStateException("Volume not found while querying files for renaming "
1770                     + oldPath);
1771         }
1772 
1773         ArrayList<String> fileList = new ArrayList<>();
1774         final String[] projection = {MediaColumns.DATA, MediaColumns.MIME_TYPE};
1775         try (Cursor c = qb.query(helper, projection, selection, null,
1776                 null, null, null, null, null)) {
1777             // Check if the calling package has write permission to all files in the given
1778             // directory. If calling package has write permission to all files in the directory, the
1779             // query with update uri should return same number of files as previous query.
1780             if (c.getCount() != countAllFilesInDirectory) {
1781                 throw new IllegalArgumentException("Calling package doesn't have write permission "
1782                         + " to rename one or more files in " + oldPath);
1783             }
1784             while(c.moveToNext()) {
1785                 final String filePath = c.getString(0).replaceFirst("^" + oldPath + "/(.*)", "$1");
1786                 final String mimeType = c.getString(1);
1787                 if (!isMimeTypeSupportedInPath(newPath + "/" + filePath, mimeType)) {
1788                     throw new IllegalArgumentException("Can't rename " + oldPath + "/" + filePath
1789                             + ". Mime type " + mimeType + " not supported in " + newPath);
1790                 }
1791                 fileList.add(filePath);
1792             }
1793         }
1794         return fileList;
1795     }
1796 
renameInLowerFs(String oldPath, String newPath)1797     private int renameInLowerFs(String oldPath, String newPath) {
1798         try {
1799             Os.rename(oldPath, newPath);
1800             return 0;
1801         } catch (ErrnoException e) {
1802             final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed.";
1803             Log.e(TAG, errorMessage, e);
1804             return e.errno;
1805         }
1806     }
1807 
1808     /**
1809      * Rename directory from {@code oldPath} to {@code newPath}.
1810      *
1811      * Renaming a directory is only allowed if calling package has write permission to all files in
1812      * the given directory tree and all file types in the given directory tree are supported by the
1813      * top level directory of new path. Renaming a directory is split into three steps:
1814      * 1. Check calling package's permissions for all files in the given directory tree. Also check
1815      *    file type support for all files in the {@code newPath}.
1816      * 2. Try updating database for all files in the directory.
1817      * 3. Rename the directory in lower file system. If rename in the lower file system is
1818      *    successful, commit database update.
1819      *
1820      * @param oldPath path of the directory to be renamed.
1821      * @param newPath new path of directory to be renamed.
1822      * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed.
1823      * <ul>
1824      * <li>{@link OsConstants#EPERM} Renaming a directory with file types not supported by
1825      * {@code newPath} or renaming a directory with files for which calling package doesn't have
1826      * write permission.
1827      * This method can also return errno returned from {@code Os.rename} function.
1828      */
renameDirectoryCheckedForFuse(String oldPath, String newPath)1829     private int renameDirectoryCheckedForFuse(String oldPath, String newPath) {
1830         final ArrayList<String> fileList;
1831         try {
1832             fileList = getWritableFilesForRenameDirectory(oldPath, newPath);
1833         } catch (IllegalArgumentException e) {
1834             final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. ";
1835             Log.e(TAG, errorMessage, e);
1836             return OsConstants.EPERM;
1837         }
1838 
1839         return renameDirectoryUncheckedForFuse(oldPath, newPath, fileList);
1840     }
1841 
renameDirectoryUncheckedForFuse(String oldPath, String newPath, ArrayList<String> fileList)1842     private int renameDirectoryUncheckedForFuse(String oldPath, String newPath,
1843             ArrayList<String> fileList) {
1844         final DatabaseHelper helper;
1845         try {
1846             helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath));
1847         } catch (VolumeNotFoundException e) {
1848             throw new IllegalStateException("Volume not found while trying to update database for "
1849                     + oldPath, e);
1850         }
1851 
1852         helper.beginTransaction();
1853         try {
1854             final Bundle qbExtras = new Bundle();
1855             qbExtras.putStringArrayList(INCLUDED_DEFAULT_DIRECTORIES,
1856                     getIncludedDefaultDirectories());
1857             for (String filePath : fileList) {
1858                 final String newFilePath = newPath + "/" + filePath;
1859                 final String mimeType = MimeUtils.resolveMimeType(new File(newFilePath));
1860                 if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath,
1861                         getContentValuesForFuseRename(newFilePath, mimeType,
1862                                 false /* checkHidden  - will be fixed up below */), qbExtras)) {
1863                     Log.e(TAG, "Calling package doesn't have write permission to rename file.");
1864                     return OsConstants.EPERM;
1865                 }
1866             }
1867 
1868             // Rename the directory in lower file system.
1869             int errno = renameInLowerFs(oldPath, newPath);
1870             if (errno == 0) {
1871                 helper.setTransactionSuccessful();
1872             } else {
1873                 return errno;
1874             }
1875         } finally {
1876             helper.endTransaction();
1877         }
1878         // Directory movement might have made new/old path hidden.
1879         scanRenamedDirectoryForFuse(oldPath, newPath);
1880         return 0;
1881     }
1882 
1883     /**
1884      * Rename a file from {@code oldPath} to {@code newPath}.
1885      *
1886      * Renaming a file is split into three parts:
1887      * 1. Check if {@code newPath} supports new file type.
1888      * 2. Try updating database entry from {@code oldPath} to {@code newPath}. This update may fail
1889      *    if calling package doesn't have write permission for {@code oldPath} and {@code newPath}.
1890      * 3. Rename the file in lower file system. If Rename in lower file system succeeds, commit
1891      *    database update.
1892      * @param oldPath path of the file to be renamed.
1893      * @param newPath new path of the file to be renamed.
1894      * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed.
1895      * <ul>
1896      * <li>{@link OsConstants#EPERM} Calling package doesn't have write permission for
1897      * {@code oldPath} or {@code newPath}, or file type is not supported by {@code newPath}.
1898      * This method can also return errno returned from {@code Os.rename} function.
1899      */
renameFileCheckedForFuse(String oldPath, String newPath)1900     private int renameFileCheckedForFuse(String oldPath, String newPath) {
1901         // Check if new mime type is supported in new path.
1902         final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
1903         if (!isMimeTypeSupportedInPath(newPath, newMimeType)) {
1904             return OsConstants.EPERM;
1905         }
1906         return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ false) ;
1907     }
1908 
renameFileUncheckedForFuse(String oldPath, String newPath)1909     private int renameFileUncheckedForFuse(String oldPath, String newPath) {
1910         return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ true) ;
1911     }
1912 
shouldFileBeHidden(@onNull File file)1913     private static boolean shouldFileBeHidden(@NonNull File file) {
1914         if (FileUtils.isFileHidden(file)) {
1915             return true;
1916         }
1917         File parent = file.getParentFile();
1918         while (parent != null) {
1919             if (FileUtils.isDirectoryHidden(parent)) {
1920                 return true;
1921             }
1922             parent = parent.getParentFile();
1923         }
1924 
1925         return false;
1926     }
1927 
renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions)1928     private int renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions) {
1929         final DatabaseHelper helper;
1930         try {
1931             helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath));
1932         } catch (VolumeNotFoundException e) {
1933             throw new IllegalStateException("Failed to update database row with " + oldPath, e);
1934         }
1935 
1936         helper.beginTransaction();
1937         try {
1938             final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
1939             if (!updateDatabaseForFuseRename(helper, oldPath, newPath,
1940                     getContentValuesForFuseRename(newPath, newMimeType, true /* checkHidden */))) {
1941                 if (!bypassRestrictions) {
1942                     Log.e(TAG, "Calling package doesn't have write permission to rename file.");
1943                     return OsConstants.EPERM;
1944                 } else if (!maybeRemoveOwnerPackageForFuseRename(helper, newPath)) {
1945                     Log.wtf(TAG, "Couldn't clear owner package name for " + newPath);
1946                     return OsConstants.EPERM;
1947                 }
1948             }
1949 
1950             // Try renaming oldPath to newPath in lower file system.
1951             int errno = renameInLowerFs(oldPath, newPath);
1952             if (errno == 0) {
1953                 helper.setTransactionSuccessful();
1954             } else {
1955                 return errno;
1956             }
1957         } finally {
1958             helper.endTransaction();
1959         }
1960         // The above code should have taken are of the mime/media type of the new file,
1961         // even if it was moved to/from a hidden directory.
1962         // This leaves cases where the source/dest of the move is a .nomedia file itself. Eg:
1963         // 1) /sdcard/foo/.nomedia => /sdcard/foo/bar.mp3
1964         //    in this case, the code above has given bar.mp3 the correct mime type, but we should
1965         //    still can /sdcard/foo, because it's now no longer hidden
1966         // 2) /sdcard/foo/.nomedia => /sdcard/bar/.nomedia
1967         //    in this case, we need to scan both /sdcard/foo and /sdcard/bar/
1968         // 3) /sdcard/foo/bar.mp3 => /sdcard/foo/.nomedia
1969         //    in this case, we need to scan all of /sdcard/foo
1970         if (extractDisplayName(oldPath).equals(".nomedia")) {
1971             scanFile(new File(oldPath).getParentFile(), REASON_DEMAND);
1972         }
1973         if (extractDisplayName(newPath).equals(".nomedia")) {
1974             scanFile(new File(newPath).getParentFile(), REASON_DEMAND);
1975         }
1976         return 0;
1977     }
1978 
1979     /**
1980      * Rename file/directory without imposing any restrictions.
1981      *
1982      * We don't impose any rename restrictions for apps that bypass scoped storage restrictions.
1983      * However, we update database entries for renamed files to keep the database consistent.
1984      */
renameUncheckedForFuse(String oldPath, String newPath)1985     private int renameUncheckedForFuse(String oldPath, String newPath) {
1986         if (new File(oldPath).isFile()) {
1987             return renameFileUncheckedForFuse(oldPath, newPath);
1988         } else {
1989             return renameDirectoryUncheckedForFuse(oldPath, newPath,
1990                     getAllFilesForRenameDirectory(oldPath));
1991         }
1992     }
1993 
1994     /**
1995      * Rename file or directory from {@code oldPath} to {@code newPath}.
1996      *
1997      * @param oldPath path of the file or directory to be renamed.
1998      * @param newPath new path of the file or directory to be renamed.
1999      * @param uid UID of the calling package.
2000      * @return 0 on successful rename, appropriate errno value if the rename is not allowed.
2001      * <ul>
2002      * <li>{@link OsConstants#ENOENT} Renaming a non-existing file or renaming a file from path that
2003      * is not indexed by MediaProvider database.
2004      * <li>{@link OsConstants#EPERM} Renaming a default directory or renaming a file to a file type
2005      * not supported by new path.
2006      * This method can also return errno returned from {@code Os.rename} function.
2007      *
2008      * Called from JNI in jni/MediaProviderWrapper.cpp
2009      */
2010     @Keep
renameForFuse(String oldPath, String newPath, int uid)2011     public int renameForFuse(String oldPath, String newPath, int uid) {
2012         final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. ";
2013         final LocalCallingIdentity token =
2014                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
2015 
2016         try {
2017             if (isPrivatePackagePathNotOwnedByCaller(oldPath)
2018                     || isPrivatePackagePathNotOwnedByCaller(newPath)) {
2019                 return OsConstants.EACCES;
2020             }
2021 
2022             if (!newPath.equals(getAbsoluteSanitizedPath(newPath))) {
2023                 Log.e(TAG, "New path name contains invalid characters.");
2024                 return OsConstants.EPERM;
2025             }
2026 
2027             if (shouldBypassFuseRestrictions(/*forWrite*/ true, oldPath)
2028                     && shouldBypassFuseRestrictions(/*forWrite*/ true, newPath)) {
2029                 return renameUncheckedForFuse(oldPath, newPath);
2030             }
2031             // Legacy apps that made is this far don't have the right storage permission and hence
2032             // are not allowed to access anything other than their external app directory
2033             if (isCallingPackageRequestingLegacy()) {
2034                 return OsConstants.EACCES;
2035             }
2036 
2037             final String[] oldRelativePath = sanitizePath(extractRelativePath(oldPath));
2038             final String[] newRelativePath = sanitizePath(extractRelativePath(newPath));
2039             if (oldRelativePath.length == 0 || newRelativePath.length == 0) {
2040                 // Rename not allowed on paths that can't be translated to RELATIVE_PATH.
2041                 Log.e(TAG, errorMessage +  "Invalid path.");
2042                 return OsConstants.EPERM;
2043             } else if (oldRelativePath.length == 1 && TextUtils.isEmpty(oldRelativePath[0])) {
2044                 // Allow rename of files/folders other than default directories.
2045                 final String displayName = extractDisplayName(oldPath);
2046                 for (String defaultFolder : sDefaultFolderNames) {
2047                     if (displayName.equals(defaultFolder)) {
2048                         Log.e(TAG, errorMessage + oldPath + " is a default folder."
2049                                 + " Renaming a default folder is not allowed.");
2050                         return OsConstants.EPERM;
2051                     }
2052                 }
2053             } else if (newRelativePath.length == 1 && TextUtils.isEmpty(newRelativePath[0])) {
2054                 Log.e(TAG, errorMessage +  newPath + " is in root folder."
2055                         + " Renaming a file/directory to root folder is not allowed");
2056                 return OsConstants.EPERM;
2057             }
2058 
2059             final File directoryAndroid = new File(Environment.getExternalStorageDirectory(),
2060                     DIRECTORY_ANDROID);
2061             final File directoryAndroidMedia = new File(directoryAndroid, DIRECTORY_MEDIA);
2062             if (directoryAndroidMedia.getAbsolutePath().equals(oldPath)) {
2063                 // Don't allow renaming 'Android/media' directory.
2064                 // Android/[data|obb] are bind mounted and these paths don't go through FUSE.
2065                 Log.e(TAG, errorMessage +  oldPath + " is a default folder in app external "
2066                         + "directory. Renaming a default folder is not allowed.");
2067                 return OsConstants.EPERM;
2068             } else if (FileUtils.contains(directoryAndroid, new File(newPath))) {
2069                 if (newRelativePath.length == 1) {
2070                     // New path is Android/*. Path is directly under Android. Don't allow moving
2071                     // files and directories to Android/.
2072                     Log.e(TAG, errorMessage +  newPath + " is in app external directory. "
2073                             + "Renaming a file/directory to app external directory is not "
2074                             + "allowed.");
2075                     return OsConstants.EPERM;
2076                 } else if(!FileUtils.contains(directoryAndroidMedia, new File(newPath))) {
2077                     // New path is  Android/*/*. Don't allow moving of files or directories
2078                     // to app external directory other than media directory.
2079                     Log.e(TAG, errorMessage +  newPath + " is not in external media directory."
2080                             + "File/directory can only be renamed to a path in external media "
2081                             + "directory. Renaming file/directory to path in other external "
2082                             + "directories is not allowed");
2083                     return OsConstants.EPERM;
2084                 }
2085             }
2086 
2087             // Continue renaming files/directories if rename of oldPath to newPath is allowed.
2088             if (new File(oldPath).isFile()) {
2089                 return renameFileCheckedForFuse(oldPath, newPath);
2090             } else {
2091                 return renameDirectoryCheckedForFuse(oldPath, newPath);
2092             }
2093         } finally {
2094             restoreLocalCallingIdentity(token);
2095         }
2096     }
2097 
2098     @Override
checkUriPermission(@onNull Uri uri, int uid, int modeFlags)2099     public int checkUriPermission(@NonNull Uri uri, int uid,
2100             /* @Intent.AccessUriMode */ int modeFlags) {
2101         final LocalCallingIdentity token = clearLocalCallingIdentity(
2102                 LocalCallingIdentity.fromExternal(getContext(), uid));
2103 
2104         try {
2105             final boolean allowHidden = isCallingPackageAllowedHidden();
2106             final int table = matchUri(uri, allowHidden);
2107 
2108             final DatabaseHelper helper;
2109             try {
2110                 helper = getDatabaseForUri(uri);
2111             } catch (VolumeNotFoundException e) {
2112                 return PackageManager.PERMISSION_DENIED;
2113             }
2114 
2115             final int type;
2116             if ((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
2117                 type = TYPE_UPDATE;
2118             } else {
2119                 type = TYPE_QUERY;
2120             }
2121 
2122             final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, null);
2123             try (Cursor c = qb.query(helper,
2124                     new String[] { BaseColumns._ID }, null, null, null, null, null, null, null)) {
2125                 if (c.getCount() == 1) {
2126                     return PackageManager.PERMISSION_GRANTED;
2127                 }
2128             }
2129         } finally {
2130             restoreLocalCallingIdentity(token);
2131         }
2132         return PackageManager.PERMISSION_DENIED;
2133     }
2134 
2135     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)2136     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
2137             String sortOrder) {
2138         return query(uri, projection,
2139                 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, sortOrder), null);
2140     }
2141 
2142     @Override
query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)2143     public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal) {
2144         Trace.beginSection("query");
2145         try {
2146             return queryInternal(uri, projection, queryArgs, signal);
2147         } catch (FallbackException e) {
2148             return e.translateForQuery(getCallingPackageTargetSdkVersion());
2149         } finally {
2150             Trace.endSection();
2151         }
2152     }
2153 
queryInternal(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)2154     private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs,
2155             CancellationSignal signal) throws FallbackException {
2156         queryArgs = (queryArgs != null) ? queryArgs : new Bundle();
2157 
2158         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
2159         queryArgs.remove(INCLUDED_DEFAULT_DIRECTORIES);
2160 
2161         final ArraySet<String> honoredArgs = new ArraySet<>();
2162         DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator);
2163 
2164         uri = safeUncanonicalize(uri);
2165 
2166         final String volumeName = getVolumeName(uri);
2167         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
2168         final boolean allowHidden = isCallingPackageAllowedHidden();
2169         final int table = matchUri(uri, allowHidden);
2170 
2171         //Log.v(TAG, "query: uri="+uri+", selection="+selection);
2172         // handle MEDIA_SCANNER before calling getDatabaseForUri()
2173         if (table == MEDIA_SCANNER) {
2174             // create a cursor to return volume currently being scanned by the media scanner
2175             MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME});
2176             c.addRow(new String[] {mMediaScannerVolume});
2177             return c;
2178         }
2179 
2180         // Used temporarily (until we have unique media IDs) to get an identifier
2181         // for the current sd card, so that the music app doesn't have to use the
2182         // non-public getFatVolumeId method
2183         if (table == FS_ID) {
2184             MatrixCursor c = new MatrixCursor(new String[] {"fsid"});
2185             c.addRow(new Integer[] {mVolumeId});
2186             return c;
2187         }
2188 
2189         if (table == VERSION) {
2190             MatrixCursor c = new MatrixCursor(new String[] {"version"});
2191             c.addRow(new Integer[] {DatabaseHelper.getDatabaseVersion(getContext())});
2192             return c;
2193         }
2194 
2195         final DatabaseHelper helper = getDatabaseForUri(uri);
2196         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, queryArgs,
2197                 honoredArgs::add);
2198 
2199         if (targetSdkVersion < Build.VERSION_CODES.R) {
2200             // Some apps are abusing "ORDER BY" clauses to inject "LIMIT"
2201             // clauses; gracefully lift them out.
2202             DatabaseUtils.recoverAbusiveSortOrder(queryArgs);
2203 
2204             // Some apps are abusing the Uri query parameters to inject LIMIT
2205             // clauses; gracefully lift them out.
2206             DatabaseUtils.recoverAbusiveLimit(uri, queryArgs);
2207         }
2208 
2209         if (targetSdkVersion < Build.VERSION_CODES.Q) {
2210             // Some apps are abusing the "WHERE" clause by injecting "GROUP BY"
2211             // clauses; gracefully lift them out.
2212             DatabaseUtils.recoverAbusiveSelection(queryArgs);
2213 
2214             // Some apps are abusing the first column to inject "DISTINCT";
2215             // gracefully lift them out.
2216             if ((projection != null) && (projection.length > 0)
2217                     && projection[0].startsWith("DISTINCT ")) {
2218                 projection[0] = projection[0].substring("DISTINCT ".length());
2219                 qb.setDistinct(true);
2220             }
2221 
2222             // Some apps are generating thumbnails with getThumbnail(), but then
2223             // ignoring the returned Bitmap and querying the raw table; give
2224             // them a row with enough information to find the original image.
2225             final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION);
2226             if ((table == IMAGES_THUMBNAILS || table == VIDEO_THUMBNAILS)
2227                     && !TextUtils.isEmpty(selection)) {
2228                 final Matcher matcher = PATTERN_SELECTION_ID.matcher(selection);
2229                 if (matcher.matches()) {
2230                     final long id = Long.parseLong(matcher.group(1));
2231 
2232                     final Uri fullUri;
2233                     if (table == IMAGES_THUMBNAILS) {
2234                         fullUri = ContentUris.withAppendedId(
2235                                 Images.Media.getContentUri(volumeName), id);
2236                     } else if (table == VIDEO_THUMBNAILS) {
2237                         fullUri = ContentUris.withAppendedId(
2238                                 Video.Media.getContentUri(volumeName), id);
2239                     } else {
2240                         throw new IllegalArgumentException();
2241                     }
2242 
2243                     final MatrixCursor cursor = new MatrixCursor(projection);
2244                     final File file = ContentResolver.encodeToFile(
2245                             fullUri.buildUpon().appendPath("thumbnail").build());
2246                     final String data = file.getAbsolutePath();
2247                     cursor.newRow().add(MediaColumns._ID, null)
2248                             .add(Images.Thumbnails.IMAGE_ID, id)
2249                             .add(Video.Thumbnails.VIDEO_ID, id)
2250                             .add(MediaColumns.DATA, data);
2251                     return cursor;
2252                 }
2253             }
2254         }
2255 
2256         final Cursor c = qb.query(helper, projection, queryArgs, signal);
2257         if (c != null) {
2258             // As a performance optimization, only configure notifications when
2259             // resulting cursor will leave our process
2260             final boolean callerIsRemote = mCallingIdentity.get().pid != android.os.Process.myPid();
2261             if (callerIsRemote && !isFuseThread()) {
2262                 c.setNotificationUri(getContext().getContentResolver(), uri);
2263             }
2264 
2265             final Bundle extras = new Bundle();
2266             extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS,
2267                     honoredArgs.toArray(new String[honoredArgs.size()]));
2268             c.setExtras(extras);
2269         }
2270         return c;
2271     }
2272 
2273     @Override
getType(Uri url)2274     public String getType(Uri url) {
2275         final int match = matchUri(url, true);
2276         switch (match) {
2277             case IMAGES_MEDIA_ID:
2278             case AUDIO_MEDIA_ID:
2279             case AUDIO_PLAYLISTS_ID:
2280             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
2281             case VIDEO_MEDIA_ID:
2282             case DOWNLOADS_ID:
2283             case FILES_ID:
2284                 final LocalCallingIdentity token = clearLocalCallingIdentity();
2285                 try (Cursor cursor = queryForSingleItem(url,
2286                         new String[] { MediaColumns.MIME_TYPE }, null, null, null)) {
2287                     return cursor.getString(0);
2288                 } catch (FileNotFoundException e) {
2289                     throw new IllegalArgumentException(e.getMessage());
2290                 } finally {
2291                      restoreLocalCallingIdentity(token);
2292                 }
2293 
2294             case IMAGES_MEDIA:
2295             case IMAGES_THUMBNAILS:
2296                 return Images.Media.CONTENT_TYPE;
2297 
2298             case AUDIO_ALBUMART_ID:
2299             case AUDIO_ALBUMART_FILE_ID:
2300             case IMAGES_THUMBNAILS_ID:
2301             case VIDEO_THUMBNAILS_ID:
2302                 return "image/jpeg";
2303 
2304             case AUDIO_MEDIA:
2305             case AUDIO_GENRES_ID_MEMBERS:
2306             case AUDIO_PLAYLISTS_ID_MEMBERS:
2307                 return Audio.Media.CONTENT_TYPE;
2308 
2309             case AUDIO_GENRES:
2310             case AUDIO_MEDIA_ID_GENRES:
2311                 return Audio.Genres.CONTENT_TYPE;
2312             case AUDIO_GENRES_ID:
2313             case AUDIO_MEDIA_ID_GENRES_ID:
2314                 return Audio.Genres.ENTRY_CONTENT_TYPE;
2315             case AUDIO_PLAYLISTS:
2316                 return Audio.Playlists.CONTENT_TYPE;
2317 
2318             case VIDEO_MEDIA:
2319                 return Video.Media.CONTENT_TYPE;
2320             case DOWNLOADS:
2321                 return Downloads.CONTENT_TYPE;
2322         }
2323         throw new IllegalStateException("Unknown URL : " + url);
2324     }
2325 
2326     @VisibleForTesting
ensureFileColumns(@onNull Uri uri, @NonNull ContentValues values)2327     void ensureFileColumns(@NonNull Uri uri, @NonNull ContentValues values)
2328             throws VolumeArgumentException, VolumeNotFoundException {
2329         final LocalUriMatcher matcher = new LocalUriMatcher(MediaStore.AUTHORITY);
2330         final int match = matcher.matchUri(uri, true);
2331         ensureNonUniqueFileColumns(match, uri, Bundle.EMPTY, values, null /* currentPath */);
2332     }
2333 
ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)2334     private void ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
2335             @NonNull ContentValues values, @Nullable String currentPath)
2336             throws VolumeArgumentException, VolumeNotFoundException {
2337         ensureFileColumns(match, uri, extras, values, true, currentPath);
2338     }
2339 
ensureNonUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)2340     private void ensureNonUniqueFileColumns(int match, @NonNull Uri uri,
2341             @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)
2342             throws VolumeArgumentException, VolumeNotFoundException {
2343         ensureFileColumns(match, uri, extras, values, false, currentPath);
2344     }
2345 
2346     /**
2347      * Get the various file-related {@link MediaColumns} in the given
2348      * {@link ContentValues} into sane condition. Also validates that defined
2349      * columns are valid for the given {@link Uri}, such as ensuring that only
2350      * {@code image/*} can be inserted into
2351      * {@link android.provider.MediaStore.Images}.
2352      */
ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)2353     private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
2354             @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)
2355             throws VolumeArgumentException, VolumeNotFoundException {
2356         Trace.beginSection("ensureFileColumns");
2357 
2358         Objects.requireNonNull(uri);
2359         Objects.requireNonNull(extras);
2360         Objects.requireNonNull(values);
2361 
2362         // Figure out defaults based on Uri being modified
2363         String defaultMimeType = ClipDescription.MIMETYPE_UNKNOWN;
2364         int defaultMediaType = FileColumns.MEDIA_TYPE_NONE;
2365         String defaultPrimary = Environment.DIRECTORY_DOWNLOADS;
2366         String defaultSecondary = null;
2367         List<String> allowedPrimary = Arrays.asList(
2368                 Environment.DIRECTORY_DOWNLOADS,
2369                 Environment.DIRECTORY_DOCUMENTS);
2370         switch (match) {
2371             case AUDIO_MEDIA:
2372             case AUDIO_MEDIA_ID:
2373                 defaultMimeType = "audio/mpeg";
2374                 defaultMediaType = FileColumns.MEDIA_TYPE_AUDIO;
2375                 defaultPrimary = Environment.DIRECTORY_MUSIC;
2376                 allowedPrimary = Arrays.asList(
2377                         Environment.DIRECTORY_ALARMS,
2378                         Environment.DIRECTORY_AUDIOBOOKS,
2379                         Environment.DIRECTORY_MUSIC,
2380                         Environment.DIRECTORY_NOTIFICATIONS,
2381                         Environment.DIRECTORY_PODCASTS,
2382                         Environment.DIRECTORY_RINGTONES);
2383                 break;
2384             case VIDEO_MEDIA:
2385             case VIDEO_MEDIA_ID:
2386                 defaultMimeType = "video/mp4";
2387                 defaultMediaType = FileColumns.MEDIA_TYPE_VIDEO;
2388                 defaultPrimary = Environment.DIRECTORY_MOVIES;
2389                 allowedPrimary = Arrays.asList(
2390                         Environment.DIRECTORY_DCIM,
2391                         Environment.DIRECTORY_MOVIES,
2392                         Environment.DIRECTORY_PICTURES);
2393                 break;
2394             case IMAGES_MEDIA:
2395             case IMAGES_MEDIA_ID:
2396                 defaultMimeType = "image/jpeg";
2397                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
2398                 defaultPrimary = Environment.DIRECTORY_PICTURES;
2399                 allowedPrimary = Arrays.asList(
2400                         Environment.DIRECTORY_DCIM,
2401                         Environment.DIRECTORY_PICTURES);
2402                 break;
2403             case AUDIO_ALBUMART:
2404             case AUDIO_ALBUMART_ID:
2405                 defaultMimeType = "image/jpeg";
2406                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
2407                 defaultPrimary = Environment.DIRECTORY_MUSIC;
2408                 allowedPrimary = Arrays.asList(defaultPrimary);
2409                 defaultSecondary = DIRECTORY_THUMBNAILS;
2410                 break;
2411             case VIDEO_THUMBNAILS:
2412             case VIDEO_THUMBNAILS_ID:
2413                 defaultMimeType = "image/jpeg";
2414                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
2415                 defaultPrimary = Environment.DIRECTORY_MOVIES;
2416                 allowedPrimary = Arrays.asList(defaultPrimary);
2417                 defaultSecondary = DIRECTORY_THUMBNAILS;
2418                 break;
2419             case IMAGES_THUMBNAILS:
2420             case IMAGES_THUMBNAILS_ID:
2421                 defaultMimeType = "image/jpeg";
2422                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
2423                 defaultPrimary = Environment.DIRECTORY_PICTURES;
2424                 allowedPrimary = Arrays.asList(defaultPrimary);
2425                 defaultSecondary = DIRECTORY_THUMBNAILS;
2426                 break;
2427             case AUDIO_PLAYLISTS:
2428             case AUDIO_PLAYLISTS_ID:
2429                 defaultMimeType = "audio/mpegurl";
2430                 defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
2431                 defaultPrimary = Environment.DIRECTORY_MUSIC;
2432                 allowedPrimary = Arrays.asList(
2433                         Environment.DIRECTORY_MUSIC,
2434                         Environment.DIRECTORY_MOVIES);
2435                 break;
2436             case DOWNLOADS:
2437             case DOWNLOADS_ID:
2438                 defaultPrimary = Environment.DIRECTORY_DOWNLOADS;
2439                 allowedPrimary = Arrays.asList(defaultPrimary);
2440                 break;
2441             case FILES:
2442             case FILES_ID:
2443                 // Use defaults above
2444                 break;
2445             default:
2446                 Log.w(TAG, "Unhandled location " + uri + "; assuming generic files");
2447                 break;
2448         }
2449 
2450         final String resolvedVolumeName = resolveVolumeName(uri);
2451 
2452         if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))
2453                 && MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)) {
2454             // TODO: promote this to top-level check
2455             throw new UnsupportedOperationException(
2456                     "Writing to internal storage is not supported.");
2457         }
2458 
2459         // Force values when raw path provided
2460         if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
2461             FileUtils.computeValuesFromData(values, isFuseThread());
2462         }
2463 
2464         final boolean isTargetSdkROrHigher =
2465                 getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R;
2466         final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
2467         final String mimeTypeFromExt = TextUtils.isEmpty(displayName) ? null :
2468                 MimeUtils.resolveMimeType(new File(displayName));
2469 
2470         if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) {
2471             if (isTargetSdkROrHigher) {
2472                 // Extract the MIME type from the display name if we couldn't resolve it from the
2473                 // raw path
2474                 if (mimeTypeFromExt != null) {
2475                     values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
2476                 } else {
2477                     // We couldn't resolve mimeType, it means that both display name and MIME type
2478                     // were missing in values, so we use defaultMimeType.
2479                     values.put(MediaColumns.MIME_TYPE, defaultMimeType);
2480                 }
2481             } else if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
2482                 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
2483             } else {
2484                 // We don't use mimeTypeFromExt to preserve legacy behavior.
2485                 values.put(MediaColumns.MIME_TYPE, defaultMimeType);
2486             }
2487         }
2488 
2489         String mimeType = values.getAsString(MediaColumns.MIME_TYPE);
2490         if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
2491             // We allow any mimeType for generic uri with default media type as MEDIA_TYPE_NONE.
2492         } else if (mimeType != null &&
2493                 MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) == null) {
2494             if (mimeTypeFromExt != null &&
2495                     defaultMediaType == MimeUtils.resolveMediaType(mimeTypeFromExt)) {
2496                 // If mimeType from extension matches the defaultMediaType of uri, we use mimeType
2497                 // from file extension as mimeType. This is an effort to guess the mimeType when we
2498                 // get unsupported mimeType.
2499                 // Note: We can't force defaultMimeType because when we force defaultMimeType, we
2500                 // will force the file extension as well. For example, if DISPLAY_NAME=Foo.png and
2501                 // mimeType="image/*". If we force mimeType to be "image/jpeg", we append the file
2502                 // name with the new file extension i.e., "Foo.png.jpg" where as the expected file
2503                 // name was "Foo.png"
2504                 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
2505             } else if (isTargetSdkROrHigher) {
2506                 // We are here because given mimeType is unsupported also we couldn't guess valid
2507                 // mimeType from file extension.
2508                 throw new IllegalArgumentException("Unsupported MIME type " + mimeType);
2509             } else {
2510                 // We can't throw error for legacy apps, so we try to use defaultMimeType.
2511                 values.put(MediaColumns.MIME_TYPE, defaultMimeType);
2512             }
2513         }
2514 
2515         // Give ourselves sane defaults when missing
2516         if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) {
2517             values.put(MediaColumns.DISPLAY_NAME,
2518                     String.valueOf(System.currentTimeMillis()));
2519         }
2520         final Integer formatObject = values.getAsInteger(FileColumns.FORMAT);
2521         final int format = formatObject == null ? 0 : formatObject.intValue();
2522         if (format == MtpConstants.FORMAT_ASSOCIATION) {
2523             values.putNull(MediaColumns.MIME_TYPE);
2524         }
2525 
2526         mimeType = values.getAsString(MediaColumns.MIME_TYPE);
2527         // Sanity check MIME type against table
2528         if (mimeType != null) {
2529             final int actualMediaType = MimeUtils.resolveMediaType(mimeType);
2530             if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
2531                 // Give callers an opportunity to work with playlists and
2532                 // subtitles using the generic files table
2533                 switch (actualMediaType) {
2534                     case FileColumns.MEDIA_TYPE_PLAYLIST:
2535                         defaultMimeType = "audio/mpegurl";
2536                         defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
2537                         defaultPrimary = Environment.DIRECTORY_MUSIC;
2538                         allowedPrimary = Arrays.asList(
2539                                 Environment.DIRECTORY_MUSIC,
2540                                 Environment.DIRECTORY_MOVIES);
2541                         break;
2542                     case FileColumns.MEDIA_TYPE_SUBTITLE:
2543                         defaultMimeType = "application/x-subrip";
2544                         defaultMediaType = FileColumns.MEDIA_TYPE_SUBTITLE;
2545                         defaultPrimary = Environment.DIRECTORY_MOVIES;
2546                         allowedPrimary = Arrays.asList(
2547                                 Environment.DIRECTORY_MUSIC,
2548                                 Environment.DIRECTORY_MOVIES);
2549                         break;
2550                 }
2551             } else if (defaultMediaType != actualMediaType) {
2552                 final String[] split = defaultMimeType.split("/");
2553                 throw new IllegalArgumentException(
2554                         "MIME type " + mimeType + " cannot be inserted into " + uri
2555                                 + "; expected MIME type under " + split[0] + "/*");
2556             }
2557         }
2558 
2559         // Use default directories when missing
2560         if (TextUtils.isEmpty(values.getAsString(MediaColumns.RELATIVE_PATH))) {
2561             if (defaultSecondary != null) {
2562                 values.put(MediaColumns.RELATIVE_PATH,
2563                         defaultPrimary + '/' + defaultSecondary + '/');
2564             } else {
2565                 values.put(MediaColumns.RELATIVE_PATH,
2566                         defaultPrimary + '/');
2567             }
2568         }
2569 
2570         // Generate path when undefined
2571         if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
2572             File volumePath;
2573             try {
2574                 volumePath = getVolumePath(resolvedVolumeName);
2575             } catch (FileNotFoundException e) {
2576                 throw new IllegalArgumentException(e);
2577             }
2578 
2579             FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ !isFuseThread());
2580             FileUtils.computeDataFromValues(values, volumePath, isFuseThread());
2581 
2582             // Create result file
2583             File res = new File(values.getAsString(MediaColumns.DATA));
2584             try {
2585                 if (makeUnique) {
2586                     res = FileUtils.buildUniqueFile(res.getParentFile(),
2587                             mimeType, res.getName());
2588                 } else {
2589                     res = FileUtils.buildNonUniqueFile(res.getParentFile(),
2590                             mimeType, res.getName());
2591                 }
2592             } catch (FileNotFoundException e) {
2593                 throw new IllegalStateException(
2594                         "Failed to build unique file: " + res + " " + values);
2595             }
2596 
2597             // Require that content lives under well-defined directories to help
2598             // keep the user's content organized
2599 
2600             // Start by saying unchanged directories are valid
2601             final String currentDir = (currentPath != null)
2602                     ? new File(currentPath).getParent() : null;
2603             boolean validPath = res.getParent().equals(currentDir);
2604 
2605             // Next, consider allowing based on allowed primary directory
2606             final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/");
2607             final String primary = (relativePath.length > 0) ? relativePath[0] : null;
2608             if (!validPath) {
2609                 validPath = allowedPrimary.contains(primary);
2610             }
2611 
2612             // Next, consider allowing paths when referencing a related item
2613             final Uri relatedUri = extras.getParcelable(QUERY_ARG_RELATED_URI);
2614             if (!validPath && relatedUri != null) {
2615                 try (Cursor c = queryForSingleItem(relatedUri, new String[] {
2616                         MediaColumns.MIME_TYPE,
2617                         MediaColumns.RELATIVE_PATH,
2618                 }, null, null, null)) {
2619                     // If top-level MIME type matches, and relative path
2620                     // matches, then allow caller to place things here
2621 
2622                     final String expectedType = MimeUtils.extractPrimaryType(
2623                             c.getString(0));
2624                     final String actualType = MimeUtils.extractPrimaryType(
2625                             values.getAsString(MediaColumns.MIME_TYPE));
2626                     if (!Objects.equals(expectedType, actualType)) {
2627                         throw new IllegalArgumentException("Placement of " + actualType
2628                                 + " item not allowed in relation to " + expectedType + " item");
2629                     }
2630 
2631                     final String expectedPath = c.getString(1);
2632                     final String actualPath = values.getAsString(MediaColumns.RELATIVE_PATH);
2633                     if (!Objects.equals(expectedPath, actualPath)) {
2634                         throw new IllegalArgumentException("Placement of " + actualPath
2635                                 + " item not allowed in relation to " + expectedPath + " item");
2636                     }
2637 
2638                     // If we didn't see any trouble above, then we'll allow it
2639                     validPath = true;
2640                 } catch (FileNotFoundException e) {
2641                     Log.w(TAG, "Failed to find related item " + relatedUri + ": " + e);
2642                 }
2643             }
2644 
2645             // Consider allowing external media directory of calling package
2646             if (!validPath) {
2647                 final String pathOwnerPackage = extractPathOwnerPackageName(res.getAbsolutePath());
2648                 if (pathOwnerPackage != null) {
2649                     validPath = isExternalMediaDirectory(res.getAbsolutePath()) &&
2650                             isCallingIdentitySharedPackageName(pathOwnerPackage);
2651                 }
2652             }
2653 
2654             // Allow apps with MANAGE_EXTERNAL_STORAGE to create files anywhere
2655             if (!validPath) {
2656                 validPath = isCallingPackageManager();
2657             }
2658 
2659             // Allow system gallery to create image/video files.
2660             if (!validPath) {
2661                 // System gallery can create image/video files in any existing directory, it can
2662                 // also create subdirectories in any existing top-level directory. However, system
2663                 // gallery is not allowed to create non-default top level directory.
2664                 final boolean createNonDefaultTopLevelDir = primary != null &&
2665                         !FileUtils.buildPath(volumePath, primary).exists();
2666                 validPath = !createNonDefaultTopLevelDir &&
2667                         canAccessMediaFile(res.getAbsolutePath(), /*allowLegacy*/ false);
2668             }
2669 
2670             // Nothing left to check; caller can't use this path
2671             if (!validPath) {
2672                 throw new IllegalArgumentException(
2673                         "Primary directory " + primary + " not allowed for " + uri
2674                                 + "; allowed directories are " + allowedPrimary);
2675             }
2676 
2677             // Ensure all parent folders of result file exist
2678             res.getParentFile().mkdirs();
2679             if (!res.getParentFile().exists()) {
2680                 throw new IllegalStateException("Failed to create directory: " + res);
2681             }
2682             values.put(MediaColumns.DATA, res.getAbsolutePath());
2683             // buildFile may have changed the file name, compute values to extract new DISPLAY_NAME.
2684             // Note: We can't extract displayName from res.getPath() because for pending & trashed
2685             // files DISPLAY_NAME will not be same as file name.
2686             FileUtils.computeValuesFromData(values, isFuseThread());
2687         } else {
2688             assertFileColumnsSane(match, uri, values);
2689         }
2690 
2691         // Drop columns that aren't relevant for special tables
2692         switch (match) {
2693             case AUDIO_ALBUMART:
2694             case VIDEO_THUMBNAILS:
2695             case IMAGES_THUMBNAILS:
2696                 final Set<String> valid = getProjectionMap(MediaStore.Images.Thumbnails.class)
2697                         .keySet();
2698                 for (String key : new ArraySet<>(values.keySet())) {
2699                     if (!valid.contains(key)) {
2700                         values.remove(key);
2701                     }
2702                 }
2703                 break;
2704         }
2705 
2706         Trace.endSection();
2707     }
2708 
2709     /**
2710      * Sanity check that any requested {@link MediaColumns#DATA} paths actually
2711      * live on the storage volume being targeted.
2712      */
assertFileColumnsSane(int match, Uri uri, ContentValues values)2713     private void assertFileColumnsSane(int match, Uri uri, ContentValues values)
2714             throws VolumeArgumentException, VolumeNotFoundException {
2715         if (!values.containsKey(MediaColumns.DATA)) return;
2716 
2717         final String volumeName = resolveVolumeName(uri);
2718         try {
2719             // Sanity check that the requested path actually lives on volume
2720             final Collection<File> allowed = getVolumeScanPaths(volumeName);
2721             final File actual = new File(values.getAsString(MediaColumns.DATA))
2722                     .getCanonicalFile();
2723             if (!FileUtils.contains(allowed, actual)) {
2724                 throw new VolumeArgumentException(actual, allowed);
2725             }
2726         } catch (IOException e) {
2727             throw new VolumeNotFoundException(volumeName);
2728         }
2729     }
2730 
2731     @Override
bulkInsert(Uri uri, ContentValues values[])2732     public int bulkInsert(Uri uri, ContentValues values[]) {
2733         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
2734         final boolean allowHidden = isCallingPackageAllowedHidden();
2735         final int match = matchUri(uri, allowHidden);
2736 
2737         if (match == VOLUMES) {
2738             return super.bulkInsert(uri, values);
2739         }
2740 
2741         final DatabaseHelper helper;
2742         try {
2743             helper = getDatabaseForUri(uri);
2744         } catch (VolumeNotFoundException e) {
2745             return e.translateForUpdateDelete(targetSdkVersion);
2746         }
2747 
2748         helper.beginTransaction();
2749         try {
2750             final int result = super.bulkInsert(uri, values);
2751             helper.setTransactionSuccessful();
2752             return result;
2753         } finally {
2754             helper.endTransaction();
2755         }
2756     }
2757 
insertDirectory(@onNull SQLiteDatabase db, @NonNull String path)2758     private long insertDirectory(@NonNull SQLiteDatabase db, @NonNull String path) {
2759         if (LOGV) Log.v(TAG, "inserting directory " + path);
2760         ContentValues values = new ContentValues();
2761         values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
2762         values.put(FileColumns.DATA, path);
2763         values.put(FileColumns.PARENT, getParent(db, path));
2764         values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path));
2765         values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
2766         values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
2767         values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
2768         values.put(FileColumns.IS_DOWNLOAD, isDownload(path) ? 1 : 0);
2769         File file = new File(path);
2770         if (file.exists()) {
2771             values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
2772         }
2773         return db.insert("files", FileColumns.DATE_MODIFIED, values);
2774     }
2775 
getParent(@onNull SQLiteDatabase db, @NonNull String path)2776     private long getParent(@NonNull SQLiteDatabase db, @NonNull String path) {
2777         final String parentPath = new File(path).getParent();
2778         if (Objects.equals("/", parentPath)) {
2779             return -1;
2780         } else {
2781             synchronized (mDirectoryCache) {
2782                 Long id = mDirectoryCache.get(parentPath);
2783                 if (id != null) {
2784                     return id;
2785                 }
2786             }
2787 
2788             final long id;
2789             try (Cursor c = db.query("files", new String[] { FileColumns._ID },
2790                     FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) {
2791                 if (c.moveToFirst()) {
2792                     id = c.getLong(0);
2793                 } else {
2794                     id = insertDirectory(db, parentPath);
2795                 }
2796             }
2797 
2798             synchronized (mDirectoryCache) {
2799                 mDirectoryCache.put(parentPath, id);
2800             }
2801             return id;
2802         }
2803     }
2804 
2805     /**
2806      * @param c the Cursor whose title to retrieve
2807      * @return the result of {@link #getDefaultTitle(String)} if the result is valid; otherwise
2808      * the value of the {@code MediaStore.Audio.Media.TITLE} column
2809      */
getDefaultTitleFromCursor(Cursor c)2810     private String getDefaultTitleFromCursor(Cursor c) {
2811         String title = null;
2812         final int columnIndex = c.getColumnIndex("title_resource_uri");
2813         // Necessary to check for existence because we may be reading from an old DB version
2814         if (columnIndex > -1) {
2815             final String titleResourceUri = c.getString(columnIndex);
2816             if (titleResourceUri != null) {
2817                 try {
2818                     title = getDefaultTitle(titleResourceUri);
2819                 } catch (Exception e) {
2820                     // Best attempt only
2821                 }
2822             }
2823         }
2824         if (title == null) {
2825             title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE));
2826         }
2827         return title;
2828     }
2829 
2830     /**
2831      * @param title_resource_uri The title resource for which to retrieve the default localization
2832      * @return The title localized to {@code Locale.US}, or {@code null} if unlocalizable
2833      * @throws Exception Thrown if the title appears to be localizable, but the localization failed
2834      * for any reason. For example, the application from which the localized title is fetched is not
2835      * installed, or it does not have the resource which needs to be localized
2836      */
getDefaultTitle(String title_resource_uri)2837     private String getDefaultTitle(String title_resource_uri) throws Exception{
2838         try {
2839             return getTitleFromResourceUri(title_resource_uri, false);
2840         } catch (Exception e) {
2841             Log.e(TAG, "Error getting default title for " + title_resource_uri, e);
2842             throw e;
2843         }
2844     }
2845 
2846     /**
2847      * @param title_resource_uri The title resource to localize
2848      * @return The localized title, or {@code null} if unlocalizable
2849      * @throws Exception Thrown if the title appears to be localizable, but the localization failed
2850      * for any reason. For example, the application from which the localized title is fetched is not
2851      * installed, or it does not have the resource which needs to be localized
2852      */
getLocalizedTitle(String title_resource_uri)2853     private String getLocalizedTitle(String title_resource_uri) throws Exception {
2854         try {
2855             return getTitleFromResourceUri(title_resource_uri, true);
2856         } catch (Exception e) {
2857             Log.e(TAG, "Error getting localized title for " + title_resource_uri, e);
2858             throw e;
2859         }
2860     }
2861 
2862     /**
2863      * Localizable titles conform to this URI pattern:
2864      *   Scheme: {@link ContentResolver.SCHEME_ANDROID_RESOURCE}
2865      *   Authority: Package Name of ringtone title provider
2866      *   First Path Segment: Type of resource (must be "string")
2867      *   Second Path Segment: Resource name of title
2868      *
2869      * @param title_resource_uri The title resource to retrieve
2870      * @param localize Whether or not to localize the title
2871      * @return The title, or {@code null} if unlocalizable
2872      * @throws Exception Thrown if the title appears to be localizable, but the localization failed
2873      * for any reason. For example, the application from which the localized title is fetched is not
2874      * installed, or it does not have the resource which needs to be localized
2875      */
getTitleFromResourceUri(String title_resource_uri, boolean localize)2876     private String getTitleFromResourceUri(String title_resource_uri, boolean localize)
2877         throws Exception {
2878         if (TextUtils.isEmpty(title_resource_uri)) {
2879             return null;
2880         }
2881         final Uri titleUri = Uri.parse(title_resource_uri);
2882         final String scheme = titleUri.getScheme();
2883         if (!ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
2884             return null;
2885         }
2886         final List<String> pathSegments = titleUri.getPathSegments();
2887         if (pathSegments.size() != 2) {
2888             Log.e(TAG, "Error getting localized title for " + title_resource_uri
2889                 + ", must have 2 path segments");
2890             return null;
2891         }
2892         final String type = pathSegments.get(0);
2893         if (!"string".equals(type)) {
2894             Log.e(TAG, "Error getting localized title for " + title_resource_uri
2895                 + ", first path segment must be \"string\"");
2896             return null;
2897         }
2898         final String packageName = titleUri.getAuthority();
2899         final Resources resources;
2900         if (localize) {
2901             resources = mPackageManager.getResourcesForApplication(packageName);
2902         } else {
2903             final Context packageContext = getContext().createPackageContext(packageName, 0);
2904             final Configuration configuration = packageContext.getResources().getConfiguration();
2905             configuration.setLocale(Locale.US);
2906             resources = packageContext.createConfigurationContext(configuration).getResources();
2907         }
2908         final String resourceIdentifier = pathSegments.get(1);
2909         final int id = resources.getIdentifier(resourceIdentifier, type, packageName);
2910         return resources.getString(id);
2911     }
2912 
onLocaleChanged()2913     public void onLocaleChanged() {
2914         mInternalDatabase.runWithTransaction((db) -> {
2915             localizeTitles(db);
2916             return null;
2917         });
2918     }
2919 
localizeTitles(@onNull SQLiteDatabase db)2920     private void localizeTitles(@NonNull SQLiteDatabase db) {
2921         try (Cursor c = db.query("files", new String[]{"_id", "title_resource_uri"},
2922             "title_resource_uri IS NOT NULL", null, null, null, null)) {
2923             while (c.moveToNext()) {
2924                 final String id = c.getString(0);
2925                 final String titleResourceUri = c.getString(1);
2926                 final ContentValues values = new ContentValues();
2927                 try {
2928                     values.put(AudioColumns.TITLE_RESOURCE_URI, titleResourceUri);
2929                     computeAudioLocalizedValues(values);
2930                     computeAudioKeyValues(values);
2931                     db.update("files", values, "_id=?", new String[]{id});
2932                 } catch (Exception e) {
2933                     Log.e(TAG, "Error updating localized title for " + titleResourceUri
2934                         + ", keeping old localization");
2935                 }
2936             }
2937         }
2938     }
2939 
insertFile(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, int mediaType)2940     private Uri insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper,
2941             int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values,
2942             int mediaType) throws VolumeArgumentException, VolumeNotFoundException {
2943         boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA)
2944                 || TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA));
2945 
2946         // Make sure all file-related columns are defined
2947         ensureUniqueFileColumns(match, uri, extras, values, null);
2948 
2949         switch (mediaType) {
2950             case FileColumns.MEDIA_TYPE_AUDIO: {
2951                 computeAudioLocalizedValues(values);
2952                 computeAudioKeyValues(values);
2953                 break;
2954             }
2955         }
2956 
2957         // compute bucket_id and bucket_display_name for all files
2958         String path = values.getAsString(MediaStore.MediaColumns.DATA);
2959         FileUtils.computeValuesFromData(values, isFuseThread());
2960         values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
2961 
2962         String title = values.getAsString(MediaStore.MediaColumns.TITLE);
2963         if (title == null && path != null) {
2964             title = extractFileName(path);
2965         }
2966         values.put(FileColumns.TITLE, title);
2967 
2968         String mimeType = null;
2969         int format = MtpConstants.FORMAT_ASSOCIATION;
2970         if (path != null && new File(path).isDirectory()) {
2971             values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
2972             values.putNull(MediaStore.MediaColumns.MIME_TYPE);
2973         } else {
2974             mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE);
2975             final Integer formatObject = values.getAsInteger(FileColumns.FORMAT);
2976             format = (formatObject == null ? 0 : formatObject.intValue());
2977         }
2978 
2979         if (format == 0) {
2980             format = MimeUtils.resolveFormatCode(mimeType);
2981         }
2982         if (path != null && path.endsWith("/")) {
2983             // TODO: convert to using FallbackException once VERSION_CODES.S is defined
2984             Log.e(TAG, "directory has trailing slash: " + path);
2985             return null;
2986         }
2987         if (format != 0) {
2988             values.put(FileColumns.FORMAT, format);
2989         }
2990 
2991         if (mimeType == null && path != null && format != MtpConstants.FORMAT_ASSOCIATION) {
2992             mimeType = MimeUtils.resolveMimeType(new File(path));
2993         }
2994 
2995         if (mimeType != null) {
2996             values.put(FileColumns.MIME_TYPE, mimeType);
2997             if (isCallingPackageSelf() && values.containsKey(FileColumns.MEDIA_TYPE)) {
2998                 // Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and
2999                 // FileColumns.MEDIA_TYPE is already populated.
3000             } else if (path != null && shouldFileBeHidden(new File(path))) {
3001                 values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
3002             } else {
3003                 values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType));
3004             }
3005         } else {
3006             values.put(FileColumns.MEDIA_TYPE, mediaType);
3007         }
3008 
3009         final long rowId;
3010         {
3011             if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
3012                 String name = values.getAsString(Audio.Playlists.NAME);
3013                 if (name == null && path == null) {
3014                     // MediaScanner will compute the name from the path if we have one
3015                     throw new IllegalArgumentException(
3016                             "no name was provided when inserting abstract playlist");
3017                 }
3018             } else {
3019                 if (path == null) {
3020                     // path might be null for playlists created on the device
3021                     // or transfered via MTP
3022                     throw new IllegalArgumentException(
3023                             "no path was provided when inserting new file");
3024                 }
3025             }
3026 
3027             // make sure modification date and size are set
3028             if (path != null) {
3029                 File file = new File(path);
3030                 if (file.exists()) {
3031                     values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
3032                     if (!values.containsKey(FileColumns.SIZE)) {
3033                         values.put(FileColumns.SIZE, file.length());
3034                     }
3035                 }
3036             }
3037 
3038             rowId = insertAllowingUpsert(qb, helper, values, path);
3039         }
3040         if (format == MtpConstants.FORMAT_ASSOCIATION) {
3041             synchronized (mDirectoryCache) {
3042                 mDirectoryCache.put(path, rowId);
3043             }
3044         }
3045 
3046         return ContentUris.withAppendedId(uri, rowId);
3047     }
3048 
3049     /**
3050      * Inserts a new row in MediaProvider database with {@code values}. Treats insert as upsert for
3051      * double inserts from same package.
3052      */
insertAllowingUpsert(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)3053     private long insertAllowingUpsert(@NonNull SQLiteQueryBuilder qb,
3054             @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)
3055             throws SQLiteConstraintException {
3056         return helper.runWithTransaction((db) -> {
3057             Long parent = values.getAsLong(FileColumns.PARENT);
3058             if (parent == null) {
3059                 if (path != null) {
3060                     final long parentId = getParent(db, path);
3061                     values.put(FileColumns.PARENT, parentId);
3062                 }
3063             }
3064 
3065             try {
3066                 return qb.insert(helper, values);
3067             } catch (SQLiteConstraintException e) {
3068                 final String packages = getAllowedPackagesForUpsert(
3069                         values.getAsString(MediaColumns.OWNER_PACKAGE_NAME));
3070                 SQLiteQueryBuilder qbForUpsert = getQueryBuilderForUpsert(path);
3071                 final long rowId = getIdIfPathOwnedByPackages(qbForUpsert, helper, path, packages);
3072                 // Apps sometimes create a file via direct path and then insert it into
3073                 // MediaStore via ContentResolver. The former should create a database entry,
3074                 // so we have to treat the latter as an upsert.
3075                 // TODO(b/149917493) Perform all INSERT operations as UPSERT.
3076                 if (rowId != -1 && qbForUpsert.update(helper, values, "_id=?",
3077                         new String[]{Long.toString(rowId)}) == 1) {
3078                     return rowId;
3079                 }
3080                 // Rethrow SQLiteConstraintException on failed upsert.
3081                 throw e;
3082             }
3083         });
3084     }
3085 
3086     /**
3087      * @return row id of the entry with path {@code path} if the owner is one of {@code packages}.
3088      */
getIdIfPathOwnedByPackages(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, String path, String packages)3089     private long getIdIfPathOwnedByPackages(@NonNull SQLiteQueryBuilder qb,
3090             @NonNull DatabaseHelper helper, String path, String packages) {
3091         final String[] projection = new String[] {FileColumns._ID};
3092         final  String ownerPackageMatchClause = DatabaseUtils.bindSelection(
3093                 MediaColumns.OWNER_PACKAGE_NAME + " IN " + packages);
3094         final String selection = FileColumns.DATA + " =? AND " + ownerPackageMatchClause;
3095 
3096         try (Cursor c = qb.query(helper, projection, selection, new String[] {path}, null, null,
3097                 null, null, null)) {
3098             if (c.moveToFirst()) {
3099                 return c.getLong(0);
3100             }
3101         }
3102         return -1;
3103     }
3104 
3105     /**
3106      * Gets packages that should match to upsert a db row.
3107      *
3108      * A database row can be upserted if
3109      * <ul>
3110      * <li> Calling package or one of the shared packages owns the db row.
3111      * <li> {@code givenOwnerPackage} owns the db row. This is useful when DownloadProvider
3112      * requests upsert on behalf of another app
3113      * </ul>
3114      */
getAllowedPackagesForUpsert(@ullable String givenOwnerPackage)3115     private String getAllowedPackagesForUpsert(@Nullable String givenOwnerPackage) {
3116         ArrayList<String> packages = new ArrayList<>();
3117         packages.addAll(Arrays.asList(mCallingIdentity.get().getSharedPackageNames()));
3118 
3119         // If givenOwnerPackage is CallingIdentity, packages list would already have shared package
3120         // names of givenOwnerPackage. If givenOwnerPackage is not CallingIdentity, since
3121         // DownloadProvider can upsert a row on behalf of app, we should include all shared packages
3122         // of givenOwnerPackage.
3123         if (givenOwnerPackage != null && isCallingPackageDelegator() &&
3124                 !isCallingIdentitySharedPackageName(givenOwnerPackage)) {
3125             // Allow DownloadProvider to Upsert if givenOwnerPackage is owner of the db row.
3126             packages.addAll(Arrays.asList(getSharedPackagesForPackage(givenOwnerPackage)));
3127         }
3128         return bindList((Object[]) packages.toArray());
3129     }
3130 
3131     /**
3132      * @return {@link SQLiteQueryBuilder} for upsert with Files uri. This disables strict columns
3133      * check to allow upsert to update any column with Files uri.
3134      */
getQueryBuilderForUpsert(@onNull String path)3135     private SQLiteQueryBuilder getQueryBuilderForUpsert(@NonNull String path) {
3136         final boolean allowHidden = isCallingPackageAllowedHidden();
3137         Bundle extras = new Bundle();
3138         extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE);
3139         extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE);
3140 
3141         // When Fuse inserts a file to database it doesn't set is_download column. When app tries
3142         // insert with Downloads uri, upsert fails because getIdIfPathExistsForCallingPackage can't
3143         // find a row ID with is_download=1. Use Files uri to get queryBuilder & update any existing
3144         // row irrespective of is_download=1.
3145         final Uri uri = FileUtils.getContentUriForPath(path);
3146         SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri,
3147                 extras, null);
3148 
3149         // We won't be able to update columns that are not part of projection map of Files table. We
3150         // have already checked strict columns in previous insert operation which failed with
3151         // exception. Any malicious column usage would have got caught in insert operation, hence we
3152         // can safely disable strict column check for upsert.
3153         qb.setStrictColumns(false);
3154         return qb;
3155     }
3156 
maybePut(@onNull ContentValues values, @NonNull String key, @Nullable String value)3157     private void maybePut(@NonNull ContentValues values, @NonNull String key,
3158             @Nullable String value) {
3159         if (value != null) {
3160             values.put(key, value);
3161         }
3162     }
3163 
maybeMarkAsDownload(@onNull ContentValues values)3164     private boolean maybeMarkAsDownload(@NonNull ContentValues values) {
3165         final String path = values.getAsString(MediaColumns.DATA);
3166         if (path != null && isDownload(path)) {
3167             values.put(FileColumns.IS_DOWNLOAD, 1);
3168             return true;
3169         }
3170         return false;
3171     }
3172 
resolveVolumeName(@onNull Uri uri)3173     private static @NonNull String resolveVolumeName(@NonNull Uri uri) {
3174         final String volumeName = getVolumeName(uri);
3175         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
3176             return MediaStore.VOLUME_EXTERNAL_PRIMARY;
3177         } else {
3178             return volumeName;
3179         }
3180     }
3181 
3182     /**
3183      * @deprecated all operations should be routed through the overload that
3184      *             accepts a {@link Bundle} of extras.
3185      */
3186     @Override
3187     @Deprecated
insert(Uri uri, ContentValues values)3188     public Uri insert(Uri uri, ContentValues values) {
3189         return insert(uri, values, null);
3190     }
3191 
3192     @Override
insert(@onNull Uri uri, @Nullable ContentValues values, @Nullable Bundle extras)3193     public @Nullable Uri insert(@NonNull Uri uri, @Nullable ContentValues values,
3194             @Nullable Bundle extras) {
3195         Trace.beginSection("insert");
3196         try {
3197             try {
3198                 return insertInternal(uri, values, extras);
3199             } catch (SQLiteConstraintException e) {
3200                 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
3201                     throw e;
3202                 } else {
3203                     return null;
3204                 }
3205             }
3206         } catch (FallbackException e) {
3207             return e.translateForInsert(getCallingPackageTargetSdkVersion());
3208         } finally {
3209             Trace.endSection();
3210         }
3211     }
3212 
insertInternal(@onNull Uri uri, @Nullable ContentValues initialValues, @Nullable Bundle extras)3213     private @Nullable Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
3214             @Nullable Bundle extras) throws FallbackException {
3215         extras = (extras != null) ? extras : new Bundle();
3216 
3217         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
3218         extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
3219 
3220         final boolean allowHidden = isCallingPackageAllowedHidden();
3221         final int match = matchUri(uri, allowHidden);
3222 
3223         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
3224         final String originalVolumeName = getVolumeName(uri);
3225         final String resolvedVolumeName = resolveVolumeName(uri);
3226 
3227         // handle MEDIA_SCANNER before calling getDatabaseForUri()
3228         if (match == MEDIA_SCANNER) {
3229             mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);
3230 
3231             final DatabaseHelper helper = getDatabaseForUri(
3232                     MediaStore.Files.getContentUri(mMediaScannerVolume));
3233 
3234             helper.mScanStartTime = SystemClock.elapsedRealtime();
3235             return MediaStore.getMediaScannerUri();
3236         }
3237 
3238         if (match == VOLUMES) {
3239             String name = initialValues.getAsString("name");
3240             Uri attachedVolume = attachVolume(name, /* validate */ true);
3241             if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) {
3242                 final DatabaseHelper helper = getDatabaseForUri(
3243                         MediaStore.Files.getContentUri(mMediaScannerVolume));
3244                 helper.mScanStartTime = SystemClock.elapsedRealtime();
3245             }
3246             return attachedVolume;
3247         }
3248 
3249         switch (match) {
3250             case AUDIO_PLAYLISTS_ID:
3251             case AUDIO_PLAYLISTS_ID_MEMBERS: {
3252                 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
3253                 final Uri playlistUri = ContentUris.withAppendedId(
3254                         MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId);
3255 
3256                 final long audioId = initialValues
3257                         .getAsLong(MediaStore.Audio.Playlists.Members.AUDIO_ID);
3258                 final Uri audioUri = ContentUris.withAppendedId(
3259                         MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId);
3260 
3261                 // Require that caller has write access to underlying media
3262                 enforceCallingPermission(playlistUri, Bundle.EMPTY, true);
3263                 enforceCallingPermission(audioUri, Bundle.EMPTY, false);
3264 
3265                 // Playlist contents are always persisted directly into playlist
3266                 // files on disk to ensure that we can reliably migrate between
3267                 // devices and recover from database corruption
3268                 final long id = addPlaylistMembers(playlistUri, initialValues);
3269                 return ContentUris.withAppendedId(MediaStore.Audio.Playlists.Members
3270                         .getContentUri(originalVolumeName, playlistId), id);
3271             }
3272         }
3273 
3274         String path = null;
3275         String ownerPackageName = null;
3276         if (initialValues != null) {
3277             // IDs are forever; nobody should be editing them
3278             initialValues.remove(MediaColumns._ID);
3279 
3280             // Expiration times are hard-coded; let's derive them
3281             FileUtils.computeDateExpires(initialValues);
3282 
3283             // Ignore or augment incoming raw filesystem paths
3284             for (String column : sDataColumns.keySet()) {
3285                 if (!initialValues.containsKey(column)) continue;
3286 
3287                 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
3288                     // Mutation allowed
3289                 } else {
3290                     Log.w(TAG, "Ignoring mutation of  " + column + " from "
3291                             + getCallingPackageOrSelf());
3292                     initialValues.remove(column);
3293                 }
3294             }
3295 
3296             path = initialValues.getAsString(MediaStore.MediaColumns.DATA);
3297 
3298             if (!isCallingPackageSelf()) {
3299                 initialValues.remove(FileColumns.IS_DOWNLOAD);
3300             }
3301 
3302             // We no longer track location metadata
3303             if (initialValues.containsKey(ImageColumns.LATITUDE)) {
3304                 initialValues.putNull(ImageColumns.LATITUDE);
3305             }
3306             if (initialValues.containsKey(ImageColumns.LONGITUDE)) {
3307                 initialValues.putNull(ImageColumns.LONGITUDE);
3308             }
3309 
3310             if (isCallingPackageSelf() || isCallingPackageShell()) {
3311                 // When media inserted by ourselves during a scan, or by the
3312                 // shell, the best we can do is guess ownership based on path
3313                 // when it's not explicitly provided
3314                 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
3315                 if (TextUtils.isEmpty(ownerPackageName)) {
3316                     ownerPackageName = extractPathOwnerPackageName(path);
3317                 }
3318             } else if (isCallingPackageDelegator()) {
3319                 // When caller is a delegator, we handle ownership as a hybrid
3320                 // of the two other cases: we're willing to accept any ownership
3321                 // transfer attempted during insert, but we fall back to using
3322                 // the Binder identity if they don't request a specific owner
3323                 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
3324                 if (TextUtils.isEmpty(ownerPackageName)) {
3325                     ownerPackageName = getCallingPackageOrSelf();
3326                 }
3327             } else {
3328                 // Remote callers have no direct control over owner column; we force
3329                 // it be whoever is creating the content.
3330                 initialValues.remove(FileColumns.OWNER_PACKAGE_NAME);
3331                 ownerPackageName = getCallingPackageOrSelf();
3332             }
3333         }
3334 
3335         long rowId = -1;
3336         Uri newUri = null;
3337 
3338         final DatabaseHelper helper = getDatabaseForUri(uri);
3339         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null);
3340 
3341         switch (match) {
3342             case IMAGES_MEDIA: {
3343                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
3344                 final boolean isDownload = maybeMarkAsDownload(initialValues);
3345                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
3346                         FileColumns.MEDIA_TYPE_IMAGE);
3347                 break;
3348             }
3349 
3350             case IMAGES_THUMBNAILS: {
3351                 if (helper.mInternal) {
3352                     throw new UnsupportedOperationException(
3353                             "Writing to internal storage is not supported.");
3354                 }
3355 
3356                 // Require that caller has write access to underlying media
3357                 final long imageId = initialValues.getAsLong(MediaStore.Images.Thumbnails.IMAGE_ID);
3358                 enforceCallingPermission(ContentUris.withAppendedId(
3359                         MediaStore.Images.Media.getContentUri(resolvedVolumeName), imageId),
3360                         extras, true);
3361 
3362                 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
3363 
3364                 rowId = qb.insert(helper, initialValues);
3365                 if (rowId > 0) {
3366                     newUri = ContentUris.withAppendedId(Images.Thumbnails.
3367                             getContentUri(originalVolumeName), rowId);
3368                 }
3369                 break;
3370             }
3371 
3372             case VIDEO_THUMBNAILS: {
3373                 if (helper.mInternal) {
3374                     throw new UnsupportedOperationException(
3375                             "Writing to internal storage is not supported.");
3376                 }
3377 
3378                 // Require that caller has write access to underlying media
3379                 final long videoId = initialValues.getAsLong(MediaStore.Video.Thumbnails.VIDEO_ID);
3380                 enforceCallingPermission(ContentUris.withAppendedId(
3381                         MediaStore.Video.Media.getContentUri(resolvedVolumeName), videoId),
3382                         Bundle.EMPTY, true);
3383 
3384                 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
3385 
3386                 rowId = qb.insert(helper, initialValues);
3387                 if (rowId > 0) {
3388                     newUri = ContentUris.withAppendedId(Video.Thumbnails.
3389                             getContentUri(originalVolumeName), rowId);
3390                 }
3391                 break;
3392             }
3393 
3394             case AUDIO_MEDIA: {
3395                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
3396                 final boolean isDownload = maybeMarkAsDownload(initialValues);
3397                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
3398                         FileColumns.MEDIA_TYPE_AUDIO);
3399                 break;
3400             }
3401 
3402             case AUDIO_MEDIA_ID_GENRES: {
3403                 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
3404             }
3405 
3406             case AUDIO_GENRES: {
3407                 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
3408             }
3409 
3410             case AUDIO_GENRES_ID_MEMBERS: {
3411                 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
3412             }
3413 
3414             case AUDIO_PLAYLISTS: {
3415                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
3416                 final boolean isDownload = maybeMarkAsDownload(initialValues);
3417                 ContentValues values = new ContentValues(initialValues);
3418                 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
3419                 // Playlist names are stored as display names, but leave
3420                 // values untouched if the caller is ModernMediaScanner
3421                 if (!isCallingPackageSelf()) {
3422                     if (values.containsKey(Playlists.NAME)) {
3423                         values.put(MediaColumns.DISPLAY_NAME, values.getAsString(Playlists.NAME));
3424                     }
3425                     if (!values.containsKey(MediaColumns.MIME_TYPE)) {
3426                         values.put(MediaColumns.MIME_TYPE, "audio/mpegurl");
3427                     }
3428                 }
3429                 newUri = insertFile(qb, helper, match, uri, extras, values,
3430                         FileColumns.MEDIA_TYPE_PLAYLIST);
3431                 if (newUri != null) {
3432                     // Touch empty playlist file on disk so its ready for renames
3433                     if (Binder.getCallingUid() != android.os.Process.myUid()) {
3434                         try (OutputStream out = ContentResolver.wrap(this)
3435                                 .openOutputStream(newUri)) {
3436                         } catch (IOException ignored) {
3437                         }
3438                     }
3439                 }
3440                 break;
3441             }
3442 
3443             case VIDEO_MEDIA: {
3444                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
3445                 final boolean isDownload = maybeMarkAsDownload(initialValues);
3446                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
3447                         FileColumns.MEDIA_TYPE_VIDEO);
3448                 break;
3449             }
3450 
3451             case AUDIO_ALBUMART: {
3452                 if (helper.mInternal) {
3453                     throw new UnsupportedOperationException("no internal album art allowed");
3454                 }
3455 
3456                 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
3457 
3458                 rowId = qb.insert(helper, initialValues);
3459                 if (rowId > 0) {
3460                     newUri = ContentUris.withAppendedId(uri, rowId);
3461                 }
3462                 break;
3463             }
3464 
3465             case FILES: {
3466                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
3467                 final boolean isDownload = maybeMarkAsDownload(initialValues);
3468                 final String mimeType = initialValues.getAsString(MediaColumns.MIME_TYPE);
3469                 final int mediaType = MimeUtils.resolveMediaType(mimeType);
3470                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
3471                         mediaType);
3472                 break;
3473             }
3474 
3475             case DOWNLOADS:
3476                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
3477                 initialValues.put(FileColumns.IS_DOWNLOAD, 1);
3478                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
3479                         FileColumns.MEDIA_TYPE_NONE);
3480                 break;
3481 
3482             default:
3483                 throw new UnsupportedOperationException("Invalid URI " + uri);
3484         }
3485 
3486         // Remember that caller is owner of this item, to speed up future
3487         // permission checks for this caller
3488         mCallingIdentity.get().setOwned(rowId, true);
3489 
3490         if (path != null && path.toLowerCase(Locale.ROOT).endsWith("/.nomedia")) {
3491             mMediaScanner.scanFile(new File(path).getParentFile(), REASON_DEMAND);
3492         }
3493 
3494         return newUri;
3495     }
3496 
3497     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)3498     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
3499                 throws OperationApplicationException {
3500         // Open transactions on databases for requested volumes
3501         final Set<DatabaseHelper> transactions = new ArraySet<>();
3502         try {
3503             for (ContentProviderOperation op : operations) {
3504                 final DatabaseHelper helper = getDatabaseForUri(op.getUri());
3505                 if (transactions.contains(helper)) continue;
3506 
3507                 if (!helper.isTransactionActive()) {
3508                     helper.beginTransaction();
3509                     transactions.add(helper);
3510                 } else {
3511                     // We normally don't allow nested transactions (since we
3512                     // don't have a good way to selectively roll them back) but
3513                     // if the incoming operation is ignoring exceptions, then we
3514                     // don't need to worry about partial rollback and can
3515                     // piggyback on the larger active transaction
3516                     if (!op.isExceptionAllowed()) {
3517                         throw new IllegalStateException("Nested transactions not supported");
3518                     }
3519                 }
3520             }
3521 
3522             final ContentProviderResult[] result = super.applyBatch(operations);
3523             for (DatabaseHelper helper : transactions) {
3524                 helper.setTransactionSuccessful();
3525             }
3526             return result;
3527         } catch (VolumeNotFoundException e) {
3528             throw e.rethrowAsIllegalArgumentException();
3529         } finally {
3530             for (DatabaseHelper helper : transactions) {
3531                 helper.endTransaction();
3532             }
3533         }
3534     }
3535 
appendWhereStandaloneMatch(@onNull SQLiteQueryBuilder qb, @NonNull String column, int match, Uri uri)3536     private void appendWhereStandaloneMatch(@NonNull SQLiteQueryBuilder qb,
3537             @NonNull String column, /* @Match */ int match, Uri uri) {
3538         switch (match) {
3539             case MATCH_INCLUDE:
3540                 // No special filtering needed
3541                 break;
3542             case MATCH_EXCLUDE:
3543                 appendWhereStandalone(qb, getWhereClauseForMatchExclude(column));
3544                 break;
3545             case MATCH_ONLY:
3546                 appendWhereStandalone(qb, column + "=?", 1);
3547                 break;
3548             case MATCH_VISIBLE_FOR_FILEPATH:
3549                 final String whereClause =
3550                         getWhereClauseForMatchableVisibleFromFilePath(uri, column);
3551                 if (whereClause != null) {
3552                     appendWhereStandalone(qb, whereClause);
3553                 }
3554                 break;
3555             default:
3556                 throw new IllegalArgumentException();
3557         }
3558     }
3559 
appendWhereStandalone(@onNull SQLiteQueryBuilder qb, @Nullable String selection, @Nullable Object... selectionArgs)3560     private static void appendWhereStandalone(@NonNull SQLiteQueryBuilder qb,
3561             @Nullable String selection, @Nullable Object... selectionArgs) {
3562         qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs));
3563     }
3564 
appendWhereStandaloneFilter(@onNull SQLiteQueryBuilder qb, @NonNull String[] columns, @Nullable String filter)3565     private static void appendWhereStandaloneFilter(@NonNull SQLiteQueryBuilder qb,
3566             @NonNull String[] columns, @Nullable String filter) {
3567         if (TextUtils.isEmpty(filter)) return;
3568         for (String filterWord : filter.split("\\s+")) {
3569             appendWhereStandalone(qb, String.join("||", columns) + " LIKE ? ESCAPE '\\'",
3570                     "%" + DatabaseUtils.escapeForLike(Audio.keyFor(filterWord)) + "%");
3571         }
3572     }
3573 
3574     @Deprecated
getSharedPackages()3575     private String getSharedPackages() {
3576         final String[] sharedPackageNames = mCallingIdentity.get().getSharedPackageNames();
3577         return bindList((Object[]) sharedPackageNames);
3578     }
3579 
3580     /**
3581      * Gets shared packages names for given {@code packageName}
3582      */
getSharedPackagesForPackage(String packageName)3583     private String[] getSharedPackagesForPackage(String packageName) {
3584         try {
3585             final int packageUid = getContext().getPackageManager()
3586                     .getPackageUid(packageName, 0);
3587             return getContext().getPackageManager().getPackagesForUid(packageUid);
3588         } catch (NameNotFoundException ignored) {
3589             return new String[] {packageName};
3590         }
3591     }
3592 
3593     private static final int TYPE_QUERY = 0;
3594     private static final int TYPE_INSERT = 1;
3595     private static final int TYPE_UPDATE = 2;
3596     private static final int TYPE_DELETE = 3;
3597 
3598     /**
3599      * Generate a {@link SQLiteQueryBuilder} that is filtered based on the
3600      * runtime permissions and/or {@link Uri} grants held by the caller.
3601      * <ul>
3602      * <li>If caller holds a {@link Uri} grant, access is allowed according to
3603      * that grant.
3604      * <li>If caller holds the write permission for a collection, they can
3605      * read/write all contents of that collection.
3606      * <li>If caller holds the read permission for a collection, they can read
3607      * all contents of that collection, but writes are limited to content they
3608      * own.
3609      * <li>If caller holds no permissions for a collection, all reads/write are
3610      * limited to content they own.
3611      * </ul>
3612      */
getQueryBuilder(int type, int match, @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored)3613     private @NonNull SQLiteQueryBuilder getQueryBuilder(int type, int match,
3614             @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) {
3615         Trace.beginSection("getQueryBuilder");
3616         try {
3617             return getQueryBuilderInternal(type, match, uri, extras, honored);
3618         } finally {
3619             Trace.endSection();
3620         }
3621     }
3622 
getQueryBuilderInternal(int type, int match, @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored)3623     private @NonNull SQLiteQueryBuilder getQueryBuilderInternal(int type, int match,
3624             @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) {
3625         final boolean forWrite;
3626         switch (type) {
3627             case TYPE_QUERY: forWrite = false; break;
3628             case TYPE_INSERT: forWrite = true; break;
3629             case TYPE_UPDATE: forWrite = true; break;
3630             case TYPE_DELETE: forWrite = true; break;
3631             default: throw new IllegalStateException();
3632         }
3633 
3634         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
3635         if (uri.getBooleanQueryParameter("distinct", false)) {
3636             qb.setDistinct(true);
3637         }
3638         qb.setStrict(true);
3639         if (isCallingPackageSelf()) {
3640             // When caller is system, such as the media scanner, we're willing
3641             // to let them access any columns they want
3642         } else {
3643             qb.setTargetSdkVersion(getCallingPackageTargetSdkVersion());
3644             qb.setStrictColumns(true);
3645             qb.setStrictGrammar(true);
3646         }
3647 
3648         // TODO: throw when requesting a currently unmounted volume
3649         final String volumeName = MediaStore.getVolumeName(uri);
3650         final String includeVolumes;
3651         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
3652             includeVolumes = bindList(getExternalVolumeNames().toArray());
3653         } else {
3654             includeVolumes = bindList(volumeName);
3655         }
3656         final String sharedPackages = getSharedPackages();
3657         final String matchSharedPackagesClause = FileColumns.OWNER_PACKAGE_NAME + " IN "
3658                 + sharedPackages;
3659 
3660         final boolean allowGlobal = checkCallingPermissionGlobal(uri, forWrite);
3661         final boolean allowLegacy =
3662                 forWrite ? isCallingPackageLegacyWrite() : isCallingPackageLegacyRead();
3663         final boolean allowLegacyRead = allowLegacy && !forWrite;
3664 
3665         int matchPending = extras.getInt(QUERY_ARG_MATCH_PENDING, MATCH_DEFAULT);
3666         int matchTrashed = extras.getInt(QUERY_ARG_MATCH_TRASHED, MATCH_DEFAULT);
3667         int matchFavorite = extras.getInt(QUERY_ARG_MATCH_FAVORITE, MATCH_DEFAULT);
3668 
3669         final ArrayList<String> includedDefaultDirs = extras.getStringArrayList(
3670                 INCLUDED_DEFAULT_DIRECTORIES);
3671 
3672         // Handle callers using legacy arguments
3673         if (MediaStore.getIncludePending(uri)) matchPending = MATCH_INCLUDE;
3674 
3675         // Resolve any remaining default options
3676         final int defaultMatchForPendingAndTrashed;
3677         if (isFuseThread()) {
3678             // Write operations always check for file ownership, we don't need additional write
3679             // permission check for is_pending and is_trashed.
3680             defaultMatchForPendingAndTrashed =
3681                     forWrite ? MATCH_INCLUDE : MATCH_VISIBLE_FOR_FILEPATH;
3682         } else {
3683             defaultMatchForPendingAndTrashed = MATCH_EXCLUDE;
3684         }
3685         if (matchPending == MATCH_DEFAULT) matchPending = defaultMatchForPendingAndTrashed;
3686         if (matchTrashed == MATCH_DEFAULT) matchTrashed = defaultMatchForPendingAndTrashed;
3687         if (matchFavorite == MATCH_DEFAULT) matchFavorite = MATCH_INCLUDE;
3688 
3689         // Handle callers using legacy filtering
3690         final String filter = uri.getQueryParameter("filter");
3691 
3692         boolean includeAllVolumes = false;
3693         final String callingPackage = getCallingPackageOrSelf();
3694 
3695         switch (match) {
3696             case IMAGES_MEDIA_ID:
3697                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
3698                 matchPending = MATCH_INCLUDE;
3699                 matchTrashed = MATCH_INCLUDE;
3700                 // fall-through
3701             case IMAGES_MEDIA: {
3702                 if (type == TYPE_QUERY) {
3703                     qb.setTables("images");
3704                     qb.setProjectionMap(
3705                             getProjectionMap(Images.Media.class));
3706                 } else {
3707                     qb.setTables("files");
3708                     qb.setProjectionMap(
3709                             getProjectionMap(Images.Media.class, Files.FileColumns.class));
3710                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
3711                             FileColumns.MEDIA_TYPE_IMAGE);
3712                 }
3713                 if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) {
3714                     appendWhereStandalone(qb, matchSharedPackagesClause);
3715                 }
3716                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
3717                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
3718                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
3719                 if (honored != null) {
3720                     honored.accept(QUERY_ARG_MATCH_PENDING);
3721                     honored.accept(QUERY_ARG_MATCH_TRASHED);
3722                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
3723                 }
3724                 if (!includeAllVolumes) {
3725                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
3726                 }
3727                 break;
3728             }
3729             case IMAGES_THUMBNAILS_ID:
3730                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
3731                 // fall-through
3732             case IMAGES_THUMBNAILS: {
3733                 qb.setTables("thumbnails");
3734 
3735                 final ArrayMap<String, String> projectionMap = new ArrayMap<>(
3736                         getProjectionMap(Images.Thumbnails.class));
3737                 projectionMap.put(Images.Thumbnails.THUMB_DATA,
3738                         "NULL AS " + Images.Thumbnails.THUMB_DATA);
3739                 qb.setProjectionMap(projectionMap);
3740 
3741                 if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) {
3742                     appendWhereStandalone(qb,
3743                             "image_id IN (SELECT _id FROM images WHERE "
3744                                     + matchSharedPackagesClause + ")");
3745                 }
3746                 break;
3747             }
3748             case AUDIO_MEDIA_ID:
3749                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
3750                 matchPending = MATCH_INCLUDE;
3751                 matchTrashed = MATCH_INCLUDE;
3752                 // fall-through
3753             case AUDIO_MEDIA: {
3754                 if (type == TYPE_QUERY) {
3755                     qb.setTables("audio");
3756                     qb.setProjectionMap(
3757                             getProjectionMap(Audio.Media.class));
3758                 } else {
3759                     qb.setTables("files");
3760                     qb.setProjectionMap(
3761                             getProjectionMap(Audio.Media.class, Files.FileColumns.class));
3762                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
3763                             FileColumns.MEDIA_TYPE_AUDIO);
3764                 }
3765                 if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) {
3766                     // Apps without Audio permission can only see their own
3767                     // media, but we also let them see ringtone-style media to
3768                     // support legacy use-cases.
3769                     appendWhereStandalone(qb,
3770                             DatabaseUtils.bindSelection(matchSharedPackagesClause
3771                                     + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1"));
3772                 }
3773                 appendWhereStandaloneFilter(qb, new String[] {
3774                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
3775                 }, filter);
3776                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
3777                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
3778                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
3779                 if (honored != null) {
3780                     honored.accept(QUERY_ARG_MATCH_PENDING);
3781                     honored.accept(QUERY_ARG_MATCH_TRASHED);
3782                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
3783                 }
3784                 if (!includeAllVolumes) {
3785                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
3786                 }
3787                 break;
3788             }
3789             case AUDIO_MEDIA_ID_GENRES_ID:
3790                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5));
3791                 // fall-through
3792             case AUDIO_MEDIA_ID_GENRES: {
3793                 if (type == TYPE_QUERY) {
3794                     qb.setTables("audio_genres");
3795                     qb.setProjectionMap(getProjectionMap(Audio.Genres.class));
3796                 } else {
3797                     throw new UnsupportedOperationException("Genres cannot be directly modified");
3798                 }
3799                 appendWhereStandalone(qb, "_id IN (SELECT genre_id FROM " +
3800                         "audio WHERE _id=?)", uri.getPathSegments().get(3));
3801                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3802                     // We don't have a great way to filter parsed metadata by
3803                     // owner, so callers need to hold READ_MEDIA_AUDIO
3804                     appendWhereStandalone(qb, "0");
3805                 }
3806                 break;
3807             }
3808             case AUDIO_GENRES_ID:
3809                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
3810                 // fall-through
3811             case AUDIO_GENRES: {
3812                 qb.setTables("audio_genres");
3813                 qb.setProjectionMap(getProjectionMap(Audio.Genres.class));
3814                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3815                     // We don't have a great way to filter parsed metadata by
3816                     // owner, so callers need to hold READ_MEDIA_AUDIO
3817                     appendWhereStandalone(qb, "0");
3818                 }
3819                 break;
3820             }
3821             case AUDIO_GENRES_ID_MEMBERS:
3822                 appendWhereStandalone(qb, "genre_id=?", uri.getPathSegments().get(3));
3823                 // fall-through
3824             case AUDIO_GENRES_ALL_MEMBERS: {
3825                 if (type == TYPE_QUERY) {
3826                     qb.setTables("audio");
3827 
3828                     final ArrayMap<String, String> projectionMap = new ArrayMap<>(
3829                             getProjectionMap(Audio.Genres.Members.class));
3830                     projectionMap.put(Audio.Genres.Members.AUDIO_ID,
3831                             "_id AS " + Audio.Genres.Members.AUDIO_ID);
3832                     qb.setProjectionMap(projectionMap);
3833                 } else {
3834                     throw new UnsupportedOperationException("Genres cannot be directly modified");
3835                 }
3836                 appendWhereStandaloneFilter(qb, new String[] {
3837                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
3838                 }, filter);
3839                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3840                     // We don't have a great way to filter parsed metadata by
3841                     // owner, so callers need to hold READ_MEDIA_AUDIO
3842                     appendWhereStandalone(qb, "0");
3843                 }
3844                 break;
3845             }
3846             case AUDIO_PLAYLISTS_ID:
3847                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
3848                 matchPending = MATCH_INCLUDE;
3849                 matchTrashed = MATCH_INCLUDE;
3850                 // fall-through
3851             case AUDIO_PLAYLISTS: {
3852                 if (type == TYPE_QUERY) {
3853                     qb.setTables("audio_playlists");
3854                     qb.setProjectionMap(
3855                             getProjectionMap(Audio.Playlists.class));
3856                 } else {
3857                     qb.setTables("files");
3858                     qb.setProjectionMap(
3859                             getProjectionMap(Audio.Playlists.class, Files.FileColumns.class));
3860                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
3861                             FileColumns.MEDIA_TYPE_PLAYLIST);
3862                 }
3863                 if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) {
3864                     appendWhereStandalone(qb, matchSharedPackagesClause);
3865                 }
3866                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
3867                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
3868                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
3869                 if (honored != null) {
3870                     honored.accept(QUERY_ARG_MATCH_PENDING);
3871                     honored.accept(QUERY_ARG_MATCH_TRASHED);
3872                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
3873                 }
3874                 if (!includeAllVolumes) {
3875                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
3876                 }
3877                 break;
3878             }
3879             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
3880                 appendWhereStandalone(qb, "audio_playlists_map._id=?",
3881                         uri.getPathSegments().get(5));
3882                 // fall-through
3883             case AUDIO_PLAYLISTS_ID_MEMBERS: {
3884                 appendWhereStandalone(qb, "playlist_id=?", uri.getPathSegments().get(3));
3885                 if (type == TYPE_QUERY) {
3886                     qb.setTables("audio_playlists_map, audio");
3887 
3888                     final ArrayMap<String, String> projectionMap = new ArrayMap<>(
3889                             getProjectionMap(Audio.Playlists.Members.class));
3890                     projectionMap.put(Audio.Playlists.Members._ID,
3891                             "audio_playlists_map._id AS " + Audio.Playlists.Members._ID);
3892                     qb.setProjectionMap(projectionMap);
3893 
3894                     appendWhereStandalone(qb, "audio._id = audio_id");
3895                 } else {
3896                     qb.setTables("audio_playlists_map");
3897                     qb.setProjectionMap(getProjectionMap(Audio.Playlists.Members.class));
3898                 }
3899                 appendWhereStandaloneFilter(qb, new String[] {
3900                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
3901                 }, filter);
3902                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3903                     // We don't have a great way to filter parsed metadata by
3904                     // owner, so callers need to hold READ_MEDIA_AUDIO
3905                     appendWhereStandalone(qb, "0");
3906                 }
3907                 break;
3908             }
3909             case AUDIO_ALBUMART_ID:
3910                 appendWhereStandalone(qb, "album_id=?", uri.getPathSegments().get(3));
3911                 // fall-through
3912             case AUDIO_ALBUMART: {
3913                 qb.setTables("album_art");
3914 
3915                 final ArrayMap<String, String> projectionMap = new ArrayMap<>(
3916                         getProjectionMap(Audio.Thumbnails.class));
3917                 projectionMap.put(Audio.Thumbnails._ID,
3918                         "album_id AS " + Audio.Thumbnails._ID);
3919                 qb.setProjectionMap(projectionMap);
3920 
3921                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3922                     // We don't have a great way to filter parsed metadata by
3923                     // owner, so callers need to hold READ_MEDIA_AUDIO
3924                     appendWhereStandalone(qb, "0");
3925                 }
3926                 break;
3927             }
3928             case AUDIO_ARTISTS_ID_ALBUMS: {
3929                 if (type == TYPE_QUERY) {
3930                     qb.setTables("audio_albums");
3931                     qb.setProjectionMap(getProjectionMap(Audio.Artists.Albums.class));
3932 
3933                     final String artistId = uri.getPathSegments().get(3);
3934                     appendWhereStandalone(qb, "artist_id=?", artistId);
3935                 } else {
3936                     throw new UnsupportedOperationException("Albums cannot be directly modified");
3937                 }
3938                 appendWhereStandaloneFilter(qb, new String[] {
3939                         AudioColumns.ALBUM_KEY
3940                 }, filter);
3941                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3942                     // We don't have a great way to filter parsed metadata by
3943                     // owner, so callers need to hold READ_MEDIA_AUDIO
3944                     appendWhereStandalone(qb, "0");
3945                 }
3946                 break;
3947             }
3948             case AUDIO_ARTISTS_ID:
3949                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
3950                 // fall-through
3951             case AUDIO_ARTISTS: {
3952                 if (type == TYPE_QUERY) {
3953                     qb.setTables("audio_artists");
3954                     qb.setProjectionMap(getProjectionMap(Audio.Artists.class));
3955                 } else {
3956                     throw new UnsupportedOperationException("Artists cannot be directly modified");
3957                 }
3958                 appendWhereStandaloneFilter(qb, new String[] {
3959                         AudioColumns.ARTIST_KEY
3960                 }, filter);
3961                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3962                     // We don't have a great way to filter parsed metadata by
3963                     // owner, so callers need to hold READ_MEDIA_AUDIO
3964                     appendWhereStandalone(qb, "0");
3965                 }
3966                 break;
3967             }
3968             case AUDIO_ALBUMS_ID:
3969                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
3970                 // fall-through
3971             case AUDIO_ALBUMS: {
3972                 if (type == TYPE_QUERY) {
3973                     qb.setTables("audio_albums");
3974                     qb.setProjectionMap(getProjectionMap(Audio.Albums.class));
3975                 } else {
3976                     throw new UnsupportedOperationException("Albums cannot be directly modified");
3977                 }
3978                 appendWhereStandaloneFilter(qb, new String[] {
3979                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY
3980                 }, filter);
3981                 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3982                     // We don't have a great way to filter parsed metadata by
3983                     // owner, so callers need to hold READ_MEDIA_AUDIO
3984                     appendWhereStandalone(qb, "0");
3985                 }
3986                 break;
3987             }
3988             case VIDEO_MEDIA_ID:
3989                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
3990                 matchPending = MATCH_INCLUDE;
3991                 matchTrashed = MATCH_INCLUDE;
3992                 // fall-through
3993             case VIDEO_MEDIA: {
3994                 if (type == TYPE_QUERY) {
3995                     qb.setTables("video");
3996                     qb.setProjectionMap(
3997                             getProjectionMap(Video.Media.class));
3998                 } else {
3999                     qb.setTables("files");
4000                     qb.setProjectionMap(
4001                             getProjectionMap(Video.Media.class, Files.FileColumns.class));
4002                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
4003                             FileColumns.MEDIA_TYPE_VIDEO);
4004                 }
4005                 if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) {
4006                     appendWhereStandalone(qb, matchSharedPackagesClause);
4007                 }
4008                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
4009                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
4010                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
4011                 if (honored != null) {
4012                     honored.accept(QUERY_ARG_MATCH_PENDING);
4013                     honored.accept(QUERY_ARG_MATCH_TRASHED);
4014                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
4015                 }
4016                 if (!includeAllVolumes) {
4017                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
4018                 }
4019                 break;
4020             }
4021             case VIDEO_THUMBNAILS_ID:
4022                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
4023                 // fall-through
4024             case VIDEO_THUMBNAILS: {
4025                 qb.setTables("videothumbnails");
4026                 qb.setProjectionMap(getProjectionMap(Video.Thumbnails.class));
4027                 if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) {
4028                     appendWhereStandalone(qb,
4029                             "video_id IN (SELECT _id FROM video WHERE " +
4030                                     matchSharedPackagesClause + ")");
4031                 }
4032                 break;
4033             }
4034             case FILES_ID:
4035                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2));
4036                 matchPending = MATCH_INCLUDE;
4037                 matchTrashed = MATCH_INCLUDE;
4038                 // fall-through
4039             case FILES: {
4040                 qb.setTables("files");
4041                 qb.setProjectionMap(getProjectionMap(Files.FileColumns.class));
4042 
4043                 final ArrayList<String> options = new ArrayList<>();
4044                 if (!allowGlobal && !allowLegacyRead) {
4045                     options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause));
4046                     if (allowLegacy) {
4047                         options.add(DatabaseUtils.bindSelection("volume_name=?",
4048                                 MediaStore.VOLUME_EXTERNAL_PRIMARY));
4049                     }
4050                     if (checkCallingPermissionAudio(forWrite, callingPackage)) {
4051                         options.add(DatabaseUtils.bindSelection("media_type=?",
4052                                 FileColumns.MEDIA_TYPE_AUDIO));
4053                         options.add(DatabaseUtils.bindSelection("media_type=?",
4054                                 FileColumns.MEDIA_TYPE_PLAYLIST));
4055                         options.add(DatabaseUtils.bindSelection("media_type=?",
4056                                 FileColumns.MEDIA_TYPE_SUBTITLE));
4057                         options.add(matchSharedPackagesClause
4058                                 + " AND media_type=0 AND mime_type LIKE 'audio/%'");
4059                     }
4060                     if (checkCallingPermissionVideo(forWrite, callingPackage)) {
4061                         options.add(DatabaseUtils.bindSelection("media_type=?",
4062                                 FileColumns.MEDIA_TYPE_VIDEO));
4063                         options.add(DatabaseUtils.bindSelection("media_type=?",
4064                                 FileColumns.MEDIA_TYPE_SUBTITLE));
4065                         options.add(matchSharedPackagesClause
4066                                 + " AND media_type=0 AND mime_type LIKE 'video/%'");
4067                     }
4068                     if (checkCallingPermissionImages(forWrite, callingPackage)) {
4069                         options.add(DatabaseUtils.bindSelection("media_type=?",
4070                                 FileColumns.MEDIA_TYPE_IMAGE));
4071                         options.add(matchSharedPackagesClause
4072                                 + " AND media_type=0 AND mime_type LIKE 'image/%'");
4073                     }
4074                     if (includedDefaultDirs != null) {
4075                         for (String defaultDir : includedDefaultDirs) {
4076                             options.add(FileColumns.RELATIVE_PATH + " LIKE '" + defaultDir + "/%'");
4077                         }
4078                     }
4079                 }
4080                 if (options.size() > 0) {
4081                     appendWhereStandalone(qb, TextUtils.join(" OR ", options));
4082                 }
4083 
4084                 appendWhereStandaloneFilter(qb, new String[] {
4085                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
4086                 }, filter);
4087                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
4088                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
4089                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
4090                 if (honored != null) {
4091                     honored.accept(QUERY_ARG_MATCH_PENDING);
4092                     honored.accept(QUERY_ARG_MATCH_TRASHED);
4093                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
4094                 }
4095                 if (!includeAllVolumes) {
4096                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
4097                 }
4098                 break;
4099             }
4100             case DOWNLOADS_ID:
4101                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2));
4102                 matchPending = MATCH_INCLUDE;
4103                 matchTrashed = MATCH_INCLUDE;
4104                 // fall-through
4105             case DOWNLOADS: {
4106                 if (type == TYPE_QUERY) {
4107                     qb.setTables("downloads");
4108                     qb.setProjectionMap(
4109                             getProjectionMap(Downloads.class));
4110                 } else {
4111                     qb.setTables("files");
4112                     qb.setProjectionMap(
4113                             getProjectionMap(Downloads.class, Files.FileColumns.class));
4114                     appendWhereStandalone(qb, FileColumns.IS_DOWNLOAD + "=1");
4115                 }
4116 
4117                 final ArrayList<String> options = new ArrayList<>();
4118                 if (!allowGlobal && !allowLegacyRead) {
4119                     options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause));
4120                     if (allowLegacy) {
4121                         options.add(DatabaseUtils.bindSelection("volume_name=?",
4122                                 MediaStore.VOLUME_EXTERNAL_PRIMARY));
4123                     }
4124                 }
4125                 if (options.size() > 0) {
4126                     appendWhereStandalone(qb, TextUtils.join(" OR ", options));
4127                 }
4128 
4129                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
4130                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
4131                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
4132                 if (honored != null) {
4133                     honored.accept(QUERY_ARG_MATCH_PENDING);
4134                     honored.accept(QUERY_ARG_MATCH_TRASHED);
4135                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
4136                 }
4137                 if (!includeAllVolumes) {
4138                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
4139                 }
4140                 break;
4141             }
4142             default:
4143                 throw new UnsupportedOperationException(
4144                         "Unknown or unsupported URL: " + uri.toString());
4145         }
4146 
4147         // To ensure we're enforcing our security model, all operations must
4148         // have a projection map configured
4149         if (qb.getProjectionMap() == null) {
4150             throw new IllegalStateException("All queries must have a projection map");
4151         }
4152 
4153         // If caller is an older app, we're willing to let through a
4154         // greylist of technically invalid columns
4155         if (getCallingPackageTargetSdkVersion() < Build.VERSION_CODES.Q) {
4156             qb.setProjectionGreylist(sGreylist);
4157         }
4158 
4159         return qb;
4160     }
4161 
4162     /**
4163      * Determine if given {@link Uri} has a
4164      * {@link MediaColumns#OWNER_PACKAGE_NAME} column.
4165      */
hasOwnerPackageName(Uri uri)4166     private boolean hasOwnerPackageName(Uri uri) {
4167         // It's easier to maintain this as an inverted list
4168         final int table = matchUri(uri, true);
4169         switch (table) {
4170             case IMAGES_THUMBNAILS_ID:
4171             case IMAGES_THUMBNAILS:
4172             case VIDEO_THUMBNAILS_ID:
4173             case VIDEO_THUMBNAILS:
4174             case AUDIO_ALBUMART:
4175             case AUDIO_ALBUMART_ID:
4176             case AUDIO_ALBUMART_FILE_ID:
4177                 return false;
4178             default:
4179                 return true;
4180         }
4181     }
4182 
4183     /**
4184      * @deprecated all operations should be routed through the overload that
4185      *             accepts a {@link Bundle} of extras.
4186      */
4187     @Override
4188     @Deprecated
delete(Uri uri, String selection, String[] selectionArgs)4189     public int delete(Uri uri, String selection, String[] selectionArgs) {
4190         return delete(uri,
4191                 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null));
4192     }
4193 
4194     @Override
delete(@onNull Uri uri, @Nullable Bundle extras)4195     public int delete(@NonNull Uri uri, @Nullable Bundle extras) {
4196         Trace.beginSection("delete");
4197         try {
4198             return deleteInternal(uri, extras);
4199         } catch (FallbackException e) {
4200             return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion());
4201         } finally {
4202             Trace.endSection();
4203         }
4204     }
4205 
deleteInternal(@onNull Uri uri, @Nullable Bundle extras)4206     private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras)
4207             throws FallbackException {
4208         extras = (extras != null) ? extras : new Bundle();
4209 
4210         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
4211         extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
4212 
4213         uri = safeUncanonicalize(uri);
4214         final boolean allowHidden = isCallingPackageAllowedHidden();
4215         final int match = matchUri(uri, allowHidden);
4216 
4217         switch(match) {
4218             case AUDIO_MEDIA_ID:
4219             case AUDIO_PLAYLISTS_ID:
4220             case VIDEO_MEDIA_ID:
4221             case IMAGES_MEDIA_ID:
4222             case DOWNLOADS_ID:
4223             case FILES_ID: {
4224                 if (!isFuseThread() && getCachedCallingIdentityForFuse(Binder.getCallingUid()).
4225                         removeDeletedRowId(Long.parseLong(uri.getLastPathSegment()))) {
4226                     // Apps sometimes delete the file via filePath and then try to delete the db row
4227                     // using MediaProvider#delete. Since we would have already deleted the db row
4228                     // during the filePath operation, the latter will result in a security
4229                     // exception. Apps which don't expect an exception will break here. Since we
4230                     // have already deleted the db row, silently return zero as deleted count.
4231                     return 0;
4232                 }
4233             }
4234             break;
4235             default:
4236                 // For other match types, given uri will not correspond to a valid file.
4237                 break;
4238         }
4239 
4240         final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION);
4241         final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);
4242 
4243         int count = 0;
4244 
4245         final String volumeName = getVolumeName(uri);
4246         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
4247 
4248         // handle MEDIA_SCANNER before calling getDatabaseForUri()
4249         if (match == MEDIA_SCANNER) {
4250             if (mMediaScannerVolume == null) {
4251                 return 0;
4252             }
4253 
4254             final DatabaseHelper helper = getDatabaseForUri(
4255                     MediaStore.Files.getContentUri(mMediaScannerVolume));
4256 
4257             helper.mScanStopTime = SystemClock.elapsedRealtime();
4258 
4259             mMediaScannerVolume = null;
4260             return 1;
4261         }
4262 
4263         if (match == VOLUMES_ID) {
4264             detachVolume(uri);
4265             count = 1;
4266         }
4267 
4268         switch (match) {
4269             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
4270                 extras.putString(QUERY_ARG_SQL_SELECTION,
4271                         BaseColumns._ID + "=" + uri.getPathSegments().get(5));
4272                 // fall-through
4273             case AUDIO_PLAYLISTS_ID_MEMBERS: {
4274                 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
4275                 final Uri playlistUri = ContentUris.withAppendedId(
4276                         MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);
4277 
4278                 // Playlist contents are always persisted directly into playlist
4279                 // files on disk to ensure that we can reliably migrate between
4280                 // devices and recover from database corruption
4281                 return removePlaylistMembers(playlistUri, extras);
4282             }
4283         }
4284 
4285         final DatabaseHelper helper = getDatabaseForUri(uri);
4286         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null);
4287 
4288         {
4289             // Give callers interacting with a specific media item a chance to
4290             // escalate access if they don't already have it
4291             switch (match) {
4292                 case AUDIO_MEDIA_ID:
4293                 case VIDEO_MEDIA_ID:
4294                 case IMAGES_MEDIA_ID:
4295                     enforceCallingPermission(uri, extras, true);
4296             }
4297 
4298             final String[] projection = new String[] {
4299                     FileColumns.MEDIA_TYPE,
4300                     FileColumns.DATA,
4301                     FileColumns._ID,
4302                     FileColumns.IS_DOWNLOAD,
4303                     FileColumns.MIME_TYPE,
4304             };
4305             final boolean isFilesTable = qb.getTables().equals("files");
4306             final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>();
4307             if (isFilesTable) {
4308                 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
4309                 if (deleteparam == null || ! deleteparam.equals("false")) {
4310                     Cursor c = qb.query(helper, projection, userWhere, userWhereArgs,
4311                             null, null, null, null, null);
4312                     try {
4313                         while (c.moveToNext()) {
4314                             final int mediaType = c.getInt(0);
4315                             final String data = c.getString(1);
4316                             final long id = c.getLong(2);
4317                             final int isDownload = c.getInt(3);
4318                             final String mimeType = c.getString(4);
4319 
4320                             // Forget that caller is owner of this item
4321                             mCallingIdentity.get().setOwned(id, false);
4322 
4323                             deleteIfAllowed(uri, extras, data);
4324                             count += qb.delete(helper, BaseColumns._ID + "=" + id, null);
4325 
4326                             // Only need to inform DownloadProvider about the downloads deleted on
4327                             // external volume.
4328                             if (isDownload == 1) {
4329                                 deletedDownloadIds.put(id, mimeType);
4330                             }
4331 
4332                             // Update any playlists that reference this item
4333                             if ((mediaType == FileColumns.MEDIA_TYPE_AUDIO)
4334                                     && helper.isExternal()) {
4335                                 helper.runWithTransaction((db) -> {
4336                                     try (Cursor cc = db.query("audio_playlists_map",
4337                                             new String[] { "playlist_id" }, "audio_id=" + id,
4338                                             null, "playlist_id", null, null)) {
4339                                         while (cc.moveToNext()) {
4340                                             final Uri playlistUri = ContentUris.withAppendedId(
4341                                                     Playlists.getContentUri(volumeName),
4342                                                     cc.getLong(0));
4343                                             resolvePlaylistMembers(playlistUri);
4344                                         }
4345                                     }
4346                                     return null;
4347                                 });
4348                             }
4349                         }
4350                     } finally {
4351                         FileUtils.closeQuietly(c);
4352                     }
4353                     // Do not allow deletion if the file/object is referenced as parent
4354                     // by some other entries. It could cause database corruption.
4355                     appendWhereStandalone(qb, ID_NOT_PARENT_CLAUSE);
4356                 }
4357             }
4358 
4359             switch (match) {
4360                 case AUDIO_GENRES_ID_MEMBERS:
4361                     throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
4362 
4363                 case IMAGES_THUMBNAILS_ID:
4364                 case IMAGES_THUMBNAILS:
4365                 case VIDEO_THUMBNAILS_ID:
4366                 case VIDEO_THUMBNAILS:
4367                     // Delete the referenced files first.
4368                     Cursor c = qb.query(helper, sDataOnlyColumn, userWhere, userWhereArgs, null,
4369                             null, null, null, null);
4370                     if (c != null) {
4371                         try {
4372                             while (c.moveToNext()) {
4373                                 deleteIfAllowed(uri, extras, c.getString(0));
4374                             }
4375                         } finally {
4376                             FileUtils.closeQuietly(c);
4377                         }
4378                     }
4379                     count += deleteRecursive(qb, helper, userWhere, userWhereArgs);
4380                     break;
4381 
4382                 default:
4383                     count += deleteRecursive(qb, helper, userWhere, userWhereArgs);
4384                     break;
4385             }
4386 
4387             if (deletedDownloadIds.size() > 0) {
4388                 // Do this on a background thread, since we don't want to make binder
4389                 // calls as part of a FUSE call.
4390                 helper.postBackground(() -> {
4391                     getContext().getSystemService(DownloadManager.class)
4392                             .onMediaStoreDownloadsDeleted(deletedDownloadIds);
4393                 });
4394             }
4395 
4396             if (isFilesTable && !isCallingPackageSelf()) {
4397                 Metrics.logDeletion(volumeName, mCallingIdentity.get().uid,
4398                         getCallingPackageOrSelf(), count);
4399             }
4400         }
4401 
4402         return count;
4403     }
4404 
4405     /**
4406      * Executes identical delete repeatedly within a single transaction until
4407      * stability is reached. Combined with {@link #ID_NOT_PARENT_CLAUSE}, this
4408      * can be used to recursively delete all matching entries, since it only
4409      * deletes parents when no references remaining.
4410      */
deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere, String[] userWhereArgs)4411     private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere,
4412             String[] userWhereArgs) {
4413         return (int) helper.runWithTransaction((db) -> {
4414             synchronized (mDirectoryCache) {
4415                 mDirectoryCache.clear();
4416             }
4417 
4418             int n = 0;
4419             int total = 0;
4420             do {
4421                 n = qb.delete(helper, userWhere, userWhereArgs);
4422                 total += n;
4423             } while (n > 0);
4424             return total;
4425         });
4426     }
4427 
4428     @Override
4429     public Bundle call(String method, String arg, Bundle extras) {
4430         Trace.beginSection("call");
4431         try {
4432             return callInternal(method, arg, extras);
4433         } finally {
4434             Trace.endSection();
4435         }
4436     }
4437 
4438     private Bundle callInternal(String method, String arg, Bundle extras) {
4439         switch (method) {
4440             case MediaStore.RESOLVE_PLAYLIST_MEMBERS_CALL: {
4441                 final LocalCallingIdentity token = clearLocalCallingIdentity();
4442                 final CallingIdentity providerToken = clearCallingIdentity();
4443                 try {
4444                     final Uri playlistUri = extras.getParcelable(MediaStore.EXTRA_URI);
4445                     resolvePlaylistMembers(playlistUri);
4446                 } finally {
4447                     restoreCallingIdentity(providerToken);
4448                     restoreLocalCallingIdentity(token);
4449                 }
4450                 return null;
4451             }
4452             case MediaStore.RUN_IDLE_MAINTENANCE_CALL: {
4453                 // Protect ourselves from random apps by requiring a generic
4454                 // permission held by common debugging components, such as shell
4455                 getContext().enforceCallingOrSelfPermission(
4456                         android.Manifest.permission.DUMP, TAG);
4457                 final LocalCallingIdentity token = clearLocalCallingIdentity();
4458                 final CallingIdentity providerToken = clearCallingIdentity();
4459                 try {
4460                     onIdleMaintenance(new CancellationSignal());
4461                 } finally {
4462                     restoreCallingIdentity(providerToken);
4463                     restoreLocalCallingIdentity(token);
4464                 }
4465                 return null;
4466             }
4467             case MediaStore.WAIT_FOR_IDLE_CALL: {
4468                 ForegroundThread.waitForIdle();
4469                 BackgroundThread.waitForIdle();
4470                 return null;
4471             }
4472             case MediaStore.SCAN_FILE_CALL:
4473             case MediaStore.SCAN_VOLUME_CALL: {
4474                 final LocalCallingIdentity token = clearLocalCallingIdentity();
4475                 final CallingIdentity providerToken = clearCallingIdentity();
4476                 try {
4477                     final Bundle res = new Bundle();
4478                     switch (method) {
4479                         case MediaStore.SCAN_FILE_CALL: {
4480                             final File file = new File(arg);
4481                             res.putParcelable(Intent.EXTRA_STREAM, scanFile(file, REASON_DEMAND));
4482                             break;
4483                         }
4484                         case MediaStore.SCAN_VOLUME_CALL: {
4485                             final String volumeName = arg;
4486                             MediaService.onScanVolume(getContext(), volumeName, REASON_DEMAND);
4487                             break;
4488                         }
4489                     }
4490                     return res;
4491                 } catch (IOException e) {
4492                     throw new RuntimeException(e);
4493                 } finally {
4494                     restoreCallingIdentity(providerToken);
4495                     restoreLocalCallingIdentity(token);
4496                 }
4497             }
4498             case MediaStore.GET_VERSION_CALL: {
4499                 final String volumeName = extras.getString(Intent.EXTRA_TEXT);
4500 
4501                 final DatabaseHelper helper;
4502                 try {
4503                     helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName));
4504                 } catch (VolumeNotFoundException e) {
4505                     throw e.rethrowAsIllegalArgumentException();
4506                 }
4507 
4508                 final String version = helper.runWithoutTransaction((db) -> {
4509                     return db.getVersion() + ":" + DatabaseHelper.getOrCreateUuid(db);
4510                 });
4511 
4512                 final Bundle res = new Bundle();
4513                 res.putString(Intent.EXTRA_TEXT, version);
4514                 return res;
4515             }
4516             case MediaStore.GET_GENERATION_CALL: {
4517                 final String volumeName = extras.getString(Intent.EXTRA_TEXT);
4518 
4519                 final DatabaseHelper helper;
4520                 try {
4521                     helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName));
4522                 } catch (VolumeNotFoundException e) {
4523                     throw e.rethrowAsIllegalArgumentException();
4524                 }
4525 
4526                 final long generation = helper.runWithoutTransaction((db) -> {
4527                     return DatabaseHelper.getGeneration(db);
4528                 });
4529 
4530                 final Bundle res = new Bundle();
4531                 res.putLong(Intent.EXTRA_INDEX, generation);
4532                 return res;
4533             }
4534             case MediaStore.GET_DOCUMENT_URI_CALL: {
4535                 final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI);
4536                 enforceCallingPermission(mediaUri, extras, false);
4537 
4538                 final Uri fileUri;
4539                 final LocalCallingIdentity token = clearLocalCallingIdentity();
4540                 try {
4541                     fileUri = Uri.fromFile(queryForDataFile(mediaUri, null));
4542                 } catch (FileNotFoundException e) {
4543                     throw new IllegalArgumentException(e);
4544                 } finally {
4545                     restoreLocalCallingIdentity(token);
4546                 }
4547 
4548                 try (ContentProviderClient client = getContext().getContentResolver()
4549                         .acquireUnstableContentProviderClient(
4550                                 MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
4551                     extras.putParcelable(MediaStore.EXTRA_URI, fileUri);
4552                     return client.call(method, null, extras);
4553                 } catch (RemoteException e) {
4554                     throw new IllegalStateException(e);
4555                 }
4556             }
4557             case MediaStore.GET_MEDIA_URI_CALL: {
4558                 final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI);
4559                 getContext().enforceCallingUriPermission(documentUri,
4560                         Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG);
4561 
4562                 final Uri fileUri;
4563                 try (ContentProviderClient client = getContext().getContentResolver()
4564                         .acquireUnstableContentProviderClient(
4565                                 MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
4566                     final Bundle res = client.call(method, null, extras);
4567                     fileUri = res.getParcelable(MediaStore.EXTRA_URI);
4568                 } catch (RemoteException e) {
4569                     throw new IllegalStateException(e);
4570                 }
4571 
4572                 final LocalCallingIdentity token = clearLocalCallingIdentity();
4573                 try {
4574                     final Bundle res = new Bundle();
4575                     res.putParcelable(MediaStore.EXTRA_URI,
4576                             queryForMediaUri(new File(fileUri.getPath()), null));
4577                     return res;
4578                 } catch (FileNotFoundException e) {
4579                     throw new IllegalArgumentException(e);
4580                 } finally {
4581                     restoreLocalCallingIdentity(token);
4582                 }
4583             }
4584             case MediaStore.CREATE_WRITE_REQUEST_CALL:
4585             case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
4586             case MediaStore.CREATE_TRASH_REQUEST_CALL:
4587             case MediaStore.CREATE_DELETE_REQUEST_CALL: {
4588                 final PendingIntent pi = createRequest(method, extras);
4589                 final Bundle res = new Bundle();
4590                 res.putParcelable(MediaStore.EXTRA_RESULT, pi);
4591                 return res;
4592             }
4593             default:
4594                 throw new UnsupportedOperationException("Unsupported call: " + method);
4595         }
4596     }
4597 
4598     static List<Uri> collectUris(ClipData clipData) {
4599         final ArrayList<Uri> res = new ArrayList<>();
4600         for (int i = 0; i < clipData.getItemCount(); i++) {
4601             res.add(clipData.getItemAt(i).getUri());
4602         }
4603         return res;
4604     }
4605 
4606     /**
4607      * Generate the {@link PendingIntent} for the given grant request. This
4608      * method also sanity checks the incoming arguments for security purposes
4609      * before creating the privileged {@link PendingIntent}.
4610      */
4611     private @NonNull PendingIntent createRequest(@NonNull String method, @NonNull Bundle extras) {
4612         final ClipData clipData = extras.getParcelable(MediaStore.EXTRA_CLIP_DATA);
4613         final List<Uri> uris = collectUris(clipData);
4614 
4615         for (Uri uri : uris) {
4616             final int match = matchUri(uri, false);
4617             switch (match) {
4618                 case IMAGES_MEDIA_ID:
4619                 case AUDIO_MEDIA_ID:
4620                 case VIDEO_MEDIA_ID:
4621                     // Caller is requesting a specific media item by its ID,
4622                     // which means it's valid for requests
4623                     break;
4624                 default:
4625                     throw new IllegalArgumentException(
4626                             "All requested items must be referenced by specific ID");
4627             }
4628         }
4629 
4630         // Enforce that limited set of columns can be mutated
4631         final ContentValues values = extras.getParcelable(MediaStore.EXTRA_CONTENT_VALUES);
4632         final List<String> allowedColumns;
4633         switch (method) {
4634             case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
4635                 allowedColumns = Arrays.asList(
4636                         MediaColumns.IS_FAVORITE);
4637                 break;
4638             case MediaStore.CREATE_TRASH_REQUEST_CALL:
4639                 allowedColumns = Arrays.asList(
4640                         MediaColumns.IS_TRASHED);
4641                 break;
4642             default:
4643                 allowedColumns = Arrays.asList();
4644                 break;
4645         }
4646         if (values != null) {
4647             for (String key : values.keySet()) {
4648                 if (!allowedColumns.contains(key)) {
4649                     throw new IllegalArgumentException("Invalid column " + key);
4650                 }
4651             }
4652         }
4653 
4654         final Context context = getContext();
4655         final Intent intent = new Intent(method, null, context, PermissionActivity.class);
4656         intent.putExtras(extras);
4657         return PendingIntent.getActivity(context, PermissionActivity.REQUEST_CODE, intent,
4658                 FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE);
4659     }
4660 
4661     /**
4662      * Ensure that all local databases have a custom collator registered for the
4663      * given {@link ULocale} locale.
4664      *
4665      * @return the corresponding custom collation name to be used in
4666      *         {@code ORDER BY} clauses.
4667      */
4668     private @NonNull String ensureCustomCollator(@NonNull String locale) {
4669         // Quick sanity check that requested locale looks sane
4670         new ULocale(locale);
4671 
4672         final String collationName = "custom_" + locale.replaceAll("[^a-zA-Z]", "");
4673         synchronized (mCustomCollators) {
4674             if (!mCustomCollators.contains(collationName)) {
4675                 for (DatabaseHelper helper : new DatabaseHelper[] {
4676                         mInternalDatabase,
4677                         mExternalDatabase
4678                 }) {
4679                     helper.runWithoutTransaction((db) -> {
4680                         db.execPerConnectionSQL("SELECT icu_load_collation(?, ?);",
4681                                 new String[] { locale, collationName });
4682                         return null;
4683                     });
4684                 }
4685                 mCustomCollators.add(collationName);
4686             }
4687         }
4688         return collationName;
4689     }
4690 
4691     private int pruneThumbnails(@NonNull SQLiteDatabase db, @NonNull CancellationSignal signal) {
4692         int prunedCount = 0;
4693 
4694         // Determine all known media items
4695         final LongArray knownIds = new LongArray();
4696         try (Cursor c = db.query(true, "files", new String[] { BaseColumns._ID },
4697                 null, null, null, null, null, null, signal)) {
4698             while (c.moveToNext()) {
4699                 knownIds.add(c.getLong(0));
4700             }
4701         }
4702 
4703         final long[] knownIdsRaw = knownIds.toArray();
4704         Arrays.sort(knownIdsRaw);
4705 
4706         for (String volumeName : getExternalVolumeNames()) {
4707             final List<File> thumbDirs;
4708             try {
4709                 thumbDirs = getThumbnailDirectories(volumeName);
4710             } catch (FileNotFoundException e) {
4711                 Log.w(TAG, "Failed to resolve volume " + volumeName, e);
4712                 continue;
4713             }
4714 
4715             // Reconcile all thumbnails, deleting stale items
4716             for (File thumbDir : thumbDirs) {
4717                 // Possibly bail before digging into each directory
4718                 signal.throwIfCanceled();
4719 
4720                 final File[] files = thumbDir.listFiles();
4721                 for (File thumbFile : (files != null) ? files : new File[0]) {
4722                     if (Objects.equals(thumbFile.getName(), FILE_DATABASE_UUID)) continue;
4723                     final String name = FileUtils.extractFileName(thumbFile.getName());
4724                     try {
4725                         final long id = Long.parseLong(name);
4726                         if (Arrays.binarySearch(knownIdsRaw, id) >= 0) {
4727                             // Thumbnail belongs to known media, keep it
4728                             continue;
4729                         }
4730                     } catch (NumberFormatException e) {
4731                     }
4732 
4733                     Log.v(TAG, "Deleting stale thumbnail " + thumbFile);
4734                     deleteAndInvalidate(thumbFile);
4735                     prunedCount++;
4736                 }
4737             }
4738         }
4739 
4740         // Also delete stale items from legacy tables
4741         db.execSQL("delete from thumbnails "
4742                 + "where image_id not in (select _id from images)");
4743         db.execSQL("delete from videothumbnails "
4744                 + "where video_id not in (select _id from video)");
4745 
4746         return prunedCount;
4747     }
4748 
4749     abstract class Thumbnailer {
4750         final String directoryName;
4751 
4752         public Thumbnailer(String directoryName) {
4753             this.directoryName = directoryName;
4754         }
4755 
4756         private File getThumbnailFile(Uri uri) throws IOException {
4757             final String volumeName = resolveVolumeName(uri);
4758             final File volumePath = getVolumePath(volumeName);
4759             return FileUtils.buildPath(volumePath, directoryName,
4760                     DIRECTORY_THUMBNAILS, ContentUris.parseId(uri) + ".jpg");
4761         }
4762 
4763         public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal)
4764                 throws IOException;
4765 
4766         public ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
4767                 throws IOException {
4768             // First attempt to fast-path by opening the thumbnail; if it
4769             // doesn't exist we fall through to create it below
4770             final File thumbFile = getThumbnailFile(uri);
4771             try {
4772                 return FileUtils.openSafely(thumbFile,
4773                         ParcelFileDescriptor.MODE_READ_ONLY);
4774             } catch (FileNotFoundException ignored) {
4775             }
4776 
4777             final File thumbDir = thumbFile.getParentFile();
4778             thumbDir.mkdirs();
4779 
4780             // When multiple threads race for the same thumbnail, the second
4781             // thread could return a file with a thumbnail still in
4782             // progress. We could add heavy per-ID locking to mitigate this
4783             // rare race condition, but it's simpler to have both threads
4784             // generate the same thumbnail using temporary files and rename
4785             // them into place once finished.
4786             final File thumbTempFile = File.createTempFile("thumb", null, thumbDir);
4787 
4788             ParcelFileDescriptor thumbWrite = null;
4789             ParcelFileDescriptor thumbRead = null;
4790             try {
4791                 // Open our temporary file twice: once for local writing, and
4792                 // once for remote reading. Both FDs point at the same
4793                 // underlying inode on disk, so they're stable across renames
4794                 // to avoid race conditions between threads.
4795                 thumbWrite = FileUtils.openSafely(thumbTempFile,
4796                         ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE);
4797                 thumbRead = FileUtils.openSafely(thumbTempFile,
4798                         ParcelFileDescriptor.MODE_READ_ONLY);
4799 
4800                 final Bitmap thumbnail = getThumbnailBitmap(uri, signal);
4801                 thumbnail.compress(Bitmap.CompressFormat.JPEG, 90,
4802                         new FileOutputStream(thumbWrite.getFileDescriptor()));
4803 
4804                 try {
4805                     // Use direct syscall for better failure logs
4806                     Os.rename(thumbTempFile.getAbsolutePath(), thumbFile.getAbsolutePath());
4807                 } catch (ErrnoException e) {
4808                     e.rethrowAsIOException();
4809                 }
4810 
4811                 // Everything above went peachy, so return a duplicate of our
4812                 // already-opened read FD to keep our finally logic below simple
4813                 return thumbRead.dup();
4814 
4815             } finally {
4816                 // Regardless of success or failure, try cleaning up any
4817                 // remaining temporary file and close all our local FDs
4818                 FileUtils.closeQuietly(thumbWrite);
4819                 FileUtils.closeQuietly(thumbRead);
4820                 deleteAndInvalidate(thumbTempFile);
4821             }
4822         }
4823 
4824         public void invalidateThumbnail(Uri uri) throws IOException {
4825             deleteAndInvalidate(getThumbnailFile(uri));
4826         }
4827     }
4828 
4829     private Thumbnailer mAudioThumbnailer = new Thumbnailer(Environment.DIRECTORY_MUSIC) {
4830         @Override
4831         public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
4832             return ThumbnailUtils.createAudioThumbnail(queryForDataFile(uri, signal),
4833                     mThumbSize, signal);
4834         }
4835     };
4836 
4837     private Thumbnailer mVideoThumbnailer = new Thumbnailer(Environment.DIRECTORY_MOVIES) {
4838         @Override
4839         public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
4840             return ThumbnailUtils.createVideoThumbnail(queryForDataFile(uri, signal),
4841                     mThumbSize, signal);
4842         }
4843     };
4844 
4845     private Thumbnailer mImageThumbnailer = new Thumbnailer(Environment.DIRECTORY_PICTURES) {
4846         @Override
4847         public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
4848             return ThumbnailUtils.createImageThumbnail(queryForDataFile(uri, signal),
4849                     mThumbSize, signal);
4850         }
4851     };
4852 
4853     private List<File> getThumbnailDirectories(String volumeName) throws FileNotFoundException {
4854         final File volumePath = getVolumePath(volumeName);
4855         return Arrays.asList(
4856                 FileUtils.buildPath(volumePath, DIRECTORY_MUSIC, DIRECTORY_THUMBNAILS),
4857                 FileUtils.buildPath(volumePath, DIRECTORY_MOVIES, DIRECTORY_THUMBNAILS),
4858                 FileUtils.buildPath(volumePath, DIRECTORY_PICTURES, DIRECTORY_THUMBNAILS));
4859     }
4860 
4861     private void invalidateThumbnails(Uri uri) {
4862         Trace.beginSection("invalidateThumbnails");
4863         try {
4864             invalidateThumbnailsInternal(uri);
4865         } finally {
4866             Trace.endSection();
4867         }
4868     }
4869 
4870     private void invalidateThumbnailsInternal(Uri uri) {
4871         final long id = ContentUris.parseId(uri);
4872         try {
4873             mAudioThumbnailer.invalidateThumbnail(uri);
4874             mVideoThumbnailer.invalidateThumbnail(uri);
4875             mImageThumbnailer.invalidateThumbnail(uri);
4876         } catch (IOException ignored) {
4877         }
4878 
4879         final DatabaseHelper helper;
4880         try {
4881             helper = getDatabaseForUri(uri);
4882         } catch (VolumeNotFoundException e) {
4883             Log.w(TAG, e);
4884             return;
4885         }
4886 
4887         helper.runWithTransaction((db) -> {
4888             final String idString = Long.toString(id);
4889             try (Cursor c = db.rawQuery("select _data from thumbnails where image_id=?"
4890                     + " union all select _data from videothumbnails where video_id=?",
4891                     new String[] { idString, idString })) {
4892                 while (c.moveToNext()) {
4893                     String path = c.getString(0);
4894                     deleteIfAllowed(uri, Bundle.EMPTY, path);
4895                 }
4896             }
4897 
4898             db.execSQL("delete from thumbnails where image_id=?", new String[] { idString });
4899             db.execSQL("delete from videothumbnails where video_id=?", new String[] { idString });
4900             return null;
4901         });
4902     }
4903 
4904     /**
4905      * @deprecated all operations should be routed through the overload that
4906      *             accepts a {@link Bundle} of extras.
4907      */
4908     @Override
4909     @Deprecated
4910     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
4911         return update(uri, values,
4912                 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null));
4913     }
4914 
4915     @Override
4916     public int update(@NonNull Uri uri, @Nullable ContentValues values,
4917             @Nullable Bundle extras) {
4918         Trace.beginSection("update");
4919         try {
4920             return updateInternal(uri, values, extras);
4921         } catch (FallbackException e) {
4922             return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion());
4923         } finally {
4924             Trace.endSection();
4925         }
4926     }
4927 
4928     private int updateInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
4929             @Nullable Bundle extras) throws FallbackException {
4930         extras = (extras != null) ? extras : new Bundle();
4931 
4932         // Related items are only considered for new media creation, and they
4933         // can't be leveraged to move existing content into blocked locations
4934         extras.remove(QUERY_ARG_RELATED_URI);
4935         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
4936         extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
4937 
4938         final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION);
4939         final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);
4940 
4941         // Limit the hacky workaround to camera targeting Q and below, to allow newer versions
4942         // of camera that does the right thing to work correctly.
4943         if ("com.google.android.GoogleCamera".equals(getCallingPackageOrSelf())
4944                 && getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) {
4945             if (matchUri(uri, false) == IMAGES_MEDIA_ID) {
4946                 Log.w(TAG, "Working around app bug in b/111966296");
4947                 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri));
4948             } else if (matchUri(uri, false) == VIDEO_MEDIA_ID) {
4949                 Log.w(TAG, "Working around app bug in b/112246630");
4950                 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri));
4951             }
4952         }
4953 
4954         uri = safeUncanonicalize(uri);
4955 
4956         int count;
4957 
4958         final String volumeName = getVolumeName(uri);
4959         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
4960         final boolean allowHidden = isCallingPackageAllowedHidden();
4961         final int match = matchUri(uri, allowHidden);
4962 
4963         switch (match) {
4964             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
4965                 extras.putString(QUERY_ARG_SQL_SELECTION,
4966                         BaseColumns._ID + "=" + uri.getPathSegments().get(5));
4967                 // fall-through
4968             case AUDIO_PLAYLISTS_ID_MEMBERS: {
4969                 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
4970                 final Uri playlistUri = ContentUris.withAppendedId(
4971                         MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);
4972 
4973                 if (uri.getBooleanQueryParameter("move", false)) {
4974                     // Convert explicit request into query; sigh, moveItem()
4975                     // uses zero-based indexing instead of one-based indexing
4976                     final int from = Integer.parseInt(uri.getPathSegments().get(5)) + 1;
4977                     final int to = initialValues.getAsInteger(Playlists.Members.PLAY_ORDER) + 1;
4978                     extras.putString(QUERY_ARG_SQL_SELECTION,
4979                             Playlists.Members.PLAY_ORDER + "=" + from);
4980                     initialValues.put(Playlists.Members.PLAY_ORDER, to);
4981                 }
4982 
4983                 // Playlist contents are always persisted directly into playlist
4984                 // files on disk to ensure that we can reliably migrate between
4985                 // devices and recover from database corruption
4986                 final int index;
4987                 if (initialValues.containsKey(Playlists.Members.PLAY_ORDER)) {
4988                     index = movePlaylistMembers(playlistUri, initialValues, extras);
4989                 } else {
4990                     index = resolvePlaylistIndex(playlistUri, extras);
4991                 }
4992                 if (initialValues.containsKey(Playlists.Members.AUDIO_ID)) {
4993                     final Bundle queryArgs = new Bundle();
4994                     queryArgs.putString(QUERY_ARG_SQL_SELECTION,
4995                             Playlists.Members.PLAY_ORDER + "=" + (index + 1));
4996                     removePlaylistMembers(playlistUri, queryArgs);
4997 
4998                     final ContentValues values = new ContentValues();
4999                     values.put(Playlists.Members.AUDIO_ID,
5000                             initialValues.getAsString(Playlists.Members.AUDIO_ID));
5001                     values.put(Playlists.Members.PLAY_ORDER, (index + 1));
5002                     addPlaylistMembers(playlistUri, values);
5003                 }
5004                 return 1;
5005             }
5006         }
5007 
5008         final DatabaseHelper helper = getDatabaseForUri(uri);
5009         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null);
5010 
5011         // Give callers interacting with a specific media item a chance to
5012         // escalate access if they don't already have it
5013         switch (match) {
5014             case AUDIO_MEDIA_ID:
5015             case VIDEO_MEDIA_ID:
5016             case IMAGES_MEDIA_ID:
5017                 enforceCallingPermission(uri, extras, true);
5018         }
5019 
5020         boolean triggerInvalidate = false;
5021         boolean triggerScan = false;
5022         if (initialValues != null) {
5023             // IDs are forever; nobody should be editing them
5024             initialValues.remove(MediaColumns._ID);
5025 
5026             // Expiration times are hard-coded; let's derive them
5027             FileUtils.computeDateExpires(initialValues);
5028 
5029             // Ignore or augment incoming raw filesystem paths
5030             for (String column : sDataColumns.keySet()) {
5031                 if (!initialValues.containsKey(column)) continue;
5032 
5033                 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
5034                     // Mutation allowed
5035                 } else {
5036                     Log.w(TAG, "Ignoring mutation of  " + column + " from "
5037                             + getCallingPackageOrSelf());
5038                     initialValues.remove(column);
5039                 }
5040             }
5041 
5042             // Enforce allowed ownership transfers
5043             if (initialValues.containsKey(MediaColumns.OWNER_PACKAGE_NAME)) {
5044                 if (isCallingPackageSelf() || isCallingPackageShell()) {
5045                     // When the caller is the media scanner or the shell, we let
5046                     // them change ownership however they see fit; nothing to do
5047                 } else if (isCallingPackageDelegator()) {
5048                     // When the caller is a delegator, allow them to shift
5049                     // ownership only when current owner, or when ownerless
5050                     final String currentOwner;
5051                     final String proposedOwner = initialValues
5052                             .getAsString(MediaColumns.OWNER_PACKAGE_NAME);
5053                     final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
5054                             ContentUris.parseId(uri));
5055                     try (Cursor c = queryForSingleItem(genericUri,
5056                             new String[] { MediaColumns.OWNER_PACKAGE_NAME }, null, null, null)) {
5057                         currentOwner = c.getString(0);
5058                     } catch (FileNotFoundException e) {
5059                         throw new IllegalStateException(e);
5060                     }
5061                     final boolean transferAllowed = (currentOwner == null)
5062                             || Arrays.asList(getSharedPackagesForPackage(getCallingPackageOrSelf()))
5063                                     .contains(currentOwner);
5064                     if (transferAllowed) {
5065                         Log.v(TAG, "Ownership transfer from " + currentOwner + " to "
5066                                 + proposedOwner + " allowed");
5067                     } else {
5068                         Log.w(TAG, "Ownership transfer from " + currentOwner + " to "
5069                                 + proposedOwner + " blocked");
5070                         initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
5071                     }
5072                 } else {
5073                     // Otherwise no ownership changes are allowed
5074                     initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
5075                 }
5076             }
5077 
5078             if (!isCallingPackageSelf()) {
5079                 Trace.beginSection("filter");
5080 
5081                 // We default to filtering mutable columns, except when we know
5082                 // the single item being updated is pending; when it's finally
5083                 // published we'll overwrite these values.
5084                 final Uri finalUri = uri;
5085                 final Supplier<Boolean> isPending = new CachedSupplier<>(() -> {
5086                     return isPending(finalUri);
5087                 });
5088 
5089                 // Column values controlled by media scanner aren't writable by
5090                 // apps, since any edits here don't reflect the metadata on
5091                 // disk, and they'd be overwritten during a rescan.
5092                 for (String column : new ArraySet<>(initialValues.keySet())) {
5093                     if (sMutableColumns.contains(column)) {
5094                         // Mutation normally allowed
5095                     } else if (isPending.get()) {
5096                         // Mutation relaxed while pending
5097                     } else {
5098                         Log.w(TAG, "Ignoring mutation of " + column + " from "
5099                                 + getCallingPackageOrSelf());
5100                         initialValues.remove(column);
5101                         triggerScan = true;
5102                     }
5103 
5104                     // If we're publishing this item, perform a blocking scan to
5105                     // make sure metadata is updated
5106                     if (MediaColumns.IS_PENDING.equals(column)) {
5107                         triggerScan = true;
5108 
5109                         // Explicitly clear columns used to ignore no-op scans,
5110                         // since we need to force a scan on publish
5111                         initialValues.putNull(MediaColumns.DATE_MODIFIED);
5112                         initialValues.putNull(MediaColumns.SIZE);
5113                     }
5114                 }
5115 
5116                 Trace.endSection();
5117             }
5118 
5119             if ("files".equals(qb.getTables())) {
5120                 maybeMarkAsDownload(initialValues);
5121             }
5122 
5123             // We no longer track location metadata
5124             if (initialValues.containsKey(ImageColumns.LATITUDE)) {
5125                 initialValues.putNull(ImageColumns.LATITUDE);
5126             }
5127             if (initialValues.containsKey(ImageColumns.LONGITUDE)) {
5128                 initialValues.putNull(ImageColumns.LONGITUDE);
5129             }
5130         }
5131 
5132         // If we're not updating anything, then we can skip
5133         if (initialValues.isEmpty()) return 0;
5134 
5135         final boolean isThumbnail;
5136         switch (match) {
5137             case IMAGES_THUMBNAILS:
5138             case IMAGES_THUMBNAILS_ID:
5139             case VIDEO_THUMBNAILS:
5140             case VIDEO_THUMBNAILS_ID:
5141             case AUDIO_ALBUMART:
5142             case AUDIO_ALBUMART_ID:
5143                 isThumbnail = true;
5144                 break;
5145             default:
5146                 isThumbnail = false;
5147                 break;
5148         }
5149 
5150         switch (match) {
5151             case AUDIO_PLAYLISTS:
5152             case AUDIO_PLAYLISTS_ID:
5153                 // Playlist names are stored as display names, but leave
5154                 // values untouched if the caller is ModernMediaScanner
5155                 if (!isCallingPackageSelf()) {
5156                     if (initialValues.containsKey(Playlists.NAME)) {
5157                         initialValues.put(MediaColumns.DISPLAY_NAME,
5158                                 initialValues.getAsString(Playlists.NAME));
5159                     }
5160                     if (!initialValues.containsKey(MediaColumns.MIME_TYPE)) {
5161                         initialValues.put(MediaColumns.MIME_TYPE, "audio/mpegurl");
5162                     }
5163                 }
5164                 break;
5165         }
5166 
5167         // If we're touching columns that would change placement of a file,
5168         // blend in current values and recalculate path
5169         final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT,
5170                 !isCallingPackageSelf());
5171         if (containsAny(initialValues.keySet(), sPlacementColumns)
5172                 && !initialValues.containsKey(MediaColumns.DATA)
5173                 && !isThumbnail
5174                 && allowMovement) {
5175             Trace.beginSection("movement");
5176 
5177             // We only support movement under well-defined collections
5178             switch (match) {
5179                 case AUDIO_MEDIA_ID:
5180                 case AUDIO_PLAYLISTS_ID:
5181                 case VIDEO_MEDIA_ID:
5182                 case IMAGES_MEDIA_ID:
5183                 case DOWNLOADS_ID:
5184                 case FILES_ID:
5185                     break;
5186                 default:
5187                     throw new IllegalArgumentException("Movement of " + uri
5188                             + " which isn't part of well-defined collection not allowed");
5189             }
5190 
5191             final LocalCallingIdentity token = clearLocalCallingIdentity();
5192             final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
5193                     ContentUris.parseId(uri));
5194             try (Cursor c = queryForSingleItem(genericUri,
5195                     sPlacementColumns.toArray(new String[0]), userWhere, userWhereArgs, null)) {
5196                 for (int i = 0; i < c.getColumnCount(); i++) {
5197                     final String column = c.getColumnName(i);
5198                     if (!initialValues.containsKey(column)) {
5199                         initialValues.put(column, c.getString(i));
5200                     }
5201                 }
5202             } catch (FileNotFoundException e) {
5203                 throw new IllegalStateException(e);
5204             } finally {
5205                 restoreLocalCallingIdentity(token);
5206             }
5207 
5208             // Regenerate path using blended values; this will throw if caller
5209             // is attempting to place file into invalid location
5210             final String beforePath = initialValues.getAsString(MediaColumns.DATA);
5211             final String beforeVolume = extractVolumeName(beforePath);
5212             final String beforeOwner = extractPathOwnerPackageName(beforePath);
5213 
5214             initialValues.remove(MediaColumns.DATA);
5215             ensureNonUniqueFileColumns(match, uri, extras, initialValues, beforePath);
5216 
5217             final String probePath = initialValues.getAsString(MediaColumns.DATA);
5218             final String probeVolume = extractVolumeName(probePath);
5219             final String probeOwner = extractPathOwnerPackageName(probePath);
5220             if (Objects.equals(beforePath, probePath)) {
5221                 Log.d(TAG, "Identical paths " + beforePath + "; not moving");
5222             } else if (!Objects.equals(beforeVolume, probeVolume)) {
5223                 throw new IllegalArgumentException("Changing volume from " + beforePath + " to "
5224                         + probePath + " not allowed");
5225             } else if (!Objects.equals(beforeOwner, probeOwner)) {
5226                 throw new IllegalArgumentException("Changing ownership from " + beforePath + " to "
5227                         + probePath + " not allowed");
5228             } else {
5229                 // Now that we've confirmed an actual movement is taking place,
5230                 // ensure we have a unique destination
5231                 initialValues.remove(MediaColumns.DATA);
5232                 ensureUniqueFileColumns(match, uri, extras, initialValues, beforePath);
5233 
5234                 final String afterPath = initialValues.getAsString(MediaColumns.DATA);
5235 
5236                 Log.d(TAG, "Moving " + beforePath + " to " + afterPath);
5237                 try {
5238                     Os.rename(beforePath, afterPath);
5239                     invalidateFuseDentry(beforePath);
5240                     invalidateFuseDentry(afterPath);
5241                 } catch (ErrnoException e) {
5242                     if (e.errno == OsConstants.ENOENT) {
5243                         Log.d(TAG, "Missing file at " + beforePath + "; continuing anyway");
5244                     } else {
5245                         throw new IllegalStateException(e);
5246                     }
5247                 }
5248                 initialValues.put(MediaColumns.DATA, afterPath);
5249 
5250                 // Some indexed metadata may have been derived from the path on
5251                 // disk, so scan this item again to update it
5252                 triggerScan = true;
5253             }
5254 
5255             Trace.endSection();
5256         }
5257 
5258         // Make sure any updated paths look sane
5259         assertFileColumnsSane(match, uri, initialValues);
5260 
5261         if (initialValues.containsKey(FileColumns.DATA)) {
5262             // If we're changing paths, invalidate any thumbnails
5263             triggerInvalidate = true;
5264 
5265             // If the new file exists, trigger a scan to adjust any metadata
5266             // that might be derived from the path
5267             final String data = initialValues.getAsString(FileColumns.DATA);
5268             if (!TextUtils.isEmpty(data) && new File(data).exists()) {
5269                 triggerScan = true;
5270             }
5271         }
5272 
5273         // If we're already doing this update from an internal scan, no need to
5274         // kick off another no-op scan
5275         if (isCallingPackageSelf()) {
5276             triggerScan = false;
5277         }
5278 
5279         // Since the update mutation may prevent us from matching items after
5280         // it's applied, we need to snapshot affected IDs here
5281         final LongArray updatedIds = new LongArray();
5282         if (triggerInvalidate || triggerScan) {
5283             Trace.beginSection("snapshot");
5284             final LocalCallingIdentity token = clearLocalCallingIdentity();
5285             try (Cursor c = qb.query(helper, new String[] { FileColumns._ID },
5286                     userWhere, userWhereArgs, null, null, null, null, null)) {
5287                 while (c.moveToNext()) {
5288                     updatedIds.add(c.getLong(0));
5289                 }
5290             } finally {
5291                 restoreLocalCallingIdentity(token);
5292                 Trace.endSection();
5293             }
5294         }
5295 
5296         final ContentValues values = new ContentValues(initialValues);
5297         switch (match) {
5298             case AUDIO_MEDIA_ID:
5299             case AUDIO_PLAYLISTS_ID:
5300             case VIDEO_MEDIA_ID:
5301             case IMAGES_MEDIA_ID:
5302             case FILES_ID:
5303             case DOWNLOADS_ID: {
5304                 FileUtils.computeValuesFromData(values, isFuseThread());
5305                 break;
5306             }
5307         }
5308 
5309         if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) {
5310             final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE);
5311             switch (mediaType) {
5312                 case FileColumns.MEDIA_TYPE_AUDIO: {
5313                     computeAudioLocalizedValues(values);
5314                     computeAudioKeyValues(values);
5315                     break;
5316                 }
5317             }
5318         }
5319 
5320         count = updateAllowingReplace(qb, helper, values, userWhere, userWhereArgs);
5321 
5322         // If the caller tried (and failed) to update metadata, the file on disk
5323         // might have changed, to scan it to collect the latest metadata.
5324         if (triggerInvalidate || triggerScan) {
5325             Trace.beginSection("invalidate");
5326             final LocalCallingIdentity token = clearLocalCallingIdentity();
5327             try {
5328                 for (int i = 0; i < updatedIds.size(); i++) {
5329                     final long updatedId = updatedIds.get(i);
5330                     final Uri updatedUri = Files.getContentUri(volumeName, updatedId);
5331                     helper.postBackground(() -> {
5332                         invalidateThumbnails(updatedUri);
5333                     });
5334 
5335                     if (triggerScan) {
5336                         try (Cursor c = queryForSingleItem(updatedUri,
5337                                 new String[] { FileColumns.DATA }, null, null, null)) {
5338                             final File file = new File(c.getString(0));
5339                             helper.postBlocking(() -> {
5340                                 final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
5341                                 try {
5342                                     mMediaScanner.scanFile(file, REASON_DEMAND);
5343                                 } finally {
5344                                     restoreLocalCallingIdentity(tokenInner);
5345                                 }
5346                             });
5347                         } catch (Exception e) {
5348                             Log.w(TAG, "Failed to update metadata for " + updatedUri, e);
5349                         }
5350                     }
5351                 }
5352             } finally {
5353                 restoreLocalCallingIdentity(token);
5354                 Trace.endSection();
5355             }
5356         }
5357 
5358         return count;
5359     }
5360 
5361     /**
5362      * Update row(s) that match {@code userWhere} in MediaProvider database with {@code values}.
5363      * Treats update as replace for updates with conflicts.
5364      */
5365     private int updateAllowingReplace(@NonNull SQLiteQueryBuilder qb,
5366             @NonNull DatabaseHelper helper, @NonNull ContentValues values, String userWhere,
5367             String[] userWhereArgs) throws SQLiteConstraintException {
5368         return helper.runWithTransaction((db) -> {
5369             try {
5370                 return qb.update(helper, values, userWhere, userWhereArgs);
5371             } catch (SQLiteConstraintException e) {
5372                 // b/155320967 Apps sometimes create a file via file path and then update another
5373                 // explicitly inserted db row to this file. We have to resolve this update with a
5374                 // replace.
5375 
5376                 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
5377                     // We don't support replace for non-legacy apps. Non legacy apps should have
5378                     // clearer interactions with MediaProvider.
5379                     throw e;
5380                 }
5381 
5382                 final String path = values.getAsString(FileColumns.DATA);
5383 
5384                 // We will only handle UNIQUE constraint error for FileColumns.DATA. We will not try
5385                 // update and replace if no file exists for conflicting db row.
5386                 if (path == null || !new File(path).exists()) {
5387                     throw e;
5388                 }
5389 
5390                 final Uri uri = FileUtils.getContentUriForPath(path);
5391                 final boolean allowHidden = isCallingPackageAllowedHidden();
5392                 // The db row which caused UNIQUE constraint error may not match all column values
5393                 // of the given queryBuilder, hence using a generic queryBuilder with Files uri.
5394                 Bundle extras = new Bundle();
5395                 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE);
5396                 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE);
5397                 final SQLiteQueryBuilder qbForReplace = getQueryBuilder(TYPE_DELETE,
5398                         matchUri(uri, allowHidden), uri, extras, null);
5399                 final long rowId = getIdIfPathOwnedByPackages(qbForReplace, helper, path,
5400                         getSharedPackages());
5401 
5402                 if (rowId != -1 && qbForReplace.delete(helper, "_id=?",
5403                         new String[] {Long.toString(rowId)}) == 1) {
5404                     Log.i(TAG, "Retrying database update after deleting conflicting entry");
5405                     return qb.update(helper, values, userWhere, userWhereArgs);
5406                 }
5407                 // Rethrow SQLiteConstraintException if app doesn't own the conflicting db row.
5408                 throw e;
5409             }
5410         });
5411     }
5412 
5413     /**
5414      * Update the internal table of {@link MediaStore.Audio.Playlists.Members}
5415      * by parsing the playlist file on disk and resolving it against scanned
5416      * audio items.
5417      * <p>
5418      * When a playlist references a missing audio item, the associated
5419      * {@link Playlists.Members#PLAY_ORDER} is skipped, leaving a gap to ensure
5420      * that the playlist entry is retained to avoid user data loss.
5421      */
5422     private void resolvePlaylistMembers(@NonNull Uri playlistUri) {
5423         Trace.beginSection("resolvePlaylistMembers");
5424         try {
5425             final DatabaseHelper helper;
5426             try {
5427                 helper = getDatabaseForUri(playlistUri);
5428             } catch (VolumeNotFoundException e) {
5429                 throw e.rethrowAsIllegalArgumentException();
5430             }
5431 
5432             helper.runWithTransaction((db) -> {
5433                 resolvePlaylistMembersInternal(playlistUri, db);
5434                 return null;
5435             });
5436         } finally {
5437             Trace.endSection();
5438         }
5439     }
5440 
5441     private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri,
5442             @NonNull SQLiteDatabase db) {
5443         try {
5444             // Refresh playlist members based on what we parse from disk
5445             final String volumeName = getVolumeName(playlistUri);
5446             final long playlistId = ContentUris.parseId(playlistUri);
5447             db.delete("audio_playlists_map", "playlist_id=" + playlistId, null);
5448 
5449             final Path playlistPath = queryForDataFile(playlistUri, null).toPath();
5450             final Playlist playlist = new Playlist();
5451             playlist.read(playlistPath.toFile());
5452 
5453             final List<Path> members = playlist.asList();
5454             for (int i = 0; i < members.size(); i++) {
5455                 try {
5456                     final Path audioPath = playlistPath.getParent().resolve(members.get(i));
5457                     final long audioId = queryForPlaylistMember(volumeName, audioPath);
5458 
5459                     final ContentValues values = new ContentValues();
5460                     values.put(Playlists.Members.PLAY_ORDER, i + 1);
5461                     values.put(Playlists.Members.PLAYLIST_ID, playlistId);
5462                     values.put(Playlists.Members.AUDIO_ID, audioId);
5463                     db.insert("audio_playlists_map", null, values);
5464                 } catch (IOException e) {
5465                     Log.w(TAG, "Failed to resolve playlist member", e);
5466                 }
5467             }
5468         } catch (IOException e) {
5469             Log.w(TAG, "Failed to refresh playlist", e);
5470         }
5471     }
5472 
5473     /**
5474      * Make two attempts to query this playlist member: first based on the exact
5475      * path, and if that fails, fall back to picking a single item matching the
5476      * display name. When there are multiple items with the same display name,
5477      * we can't resolve between them, and leave this member unresolved.
5478      */
5479     private long queryForPlaylistMember(@NonNull String volumeName, @NonNull Path path)
5480             throws IOException {
5481         final Uri audioUri = Audio.Media.getContentUri(volumeName);
5482         try (Cursor c = queryForSingleItem(audioUri,
5483                 new String[] { BaseColumns._ID }, MediaColumns.DATA + "=?",
5484                 new String[] { path.toFile().getCanonicalPath() }, null)) {
5485             return c.getLong(0);
5486         } catch (FileNotFoundException ignored) {
5487         }
5488         try (Cursor c = queryForSingleItem(audioUri,
5489                 new String[] { BaseColumns._ID }, MediaColumns.DISPLAY_NAME + "=?",
5490                 new String[] { path.toFile().getName() }, null)) {
5491             return c.getLong(0);
5492         } catch (FileNotFoundException ignored) {
5493         }
5494         throw new FileNotFoundException();
5495     }
5496 
5497     /**
5498      * Add the given audio item to the given playlist. Defaults to adding at the
5499      * end of the playlist when no {@link Playlists.Members#PLAY_ORDER} is
5500      * defined.
5501      */
5502     private long addPlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values)
5503             throws FallbackException {
5504         final long audioId = values.getAsLong(Audio.Playlists.Members.AUDIO_ID);
5505         final Uri audioUri = Audio.Media.getContentUri(getVolumeName(playlistUri), audioId);
5506 
5507         Integer playOrder = values.getAsInteger(Playlists.Members.PLAY_ORDER);
5508         playOrder = (playOrder != null) ? (playOrder - 1) : Integer.MAX_VALUE;
5509 
5510         try {
5511             final File playlistFile = queryForDataFile(playlistUri, null);
5512             final File audioFile = queryForDataFile(audioUri, null);
5513 
5514             final Playlist playlist = new Playlist();
5515             playlist.read(playlistFile);
5516             playOrder = playlist.add(playOrder,
5517                     playlistFile.toPath().getParent().relativize(audioFile.toPath()));
5518             playlist.write(playlistFile);
5519 
5520             resolvePlaylistMembers(playlistUri);
5521 
5522             // Callers are interested in the actual ID we generated
5523             final Uri membersUri = Playlists.Members.getContentUri(
5524                     getVolumeName(playlistUri), ContentUris.parseId(playlistUri));
5525             try (Cursor c = query(membersUri, new String[] { BaseColumns._ID },
5526                     Playlists.Members.PLAY_ORDER + "=" + (playOrder + 1), null, null)) {
5527                 c.moveToFirst();
5528                 return c.getLong(0);
5529             }
5530         } catch (IOException e) {
5531             throw new FallbackException("Failed to update playlist", e,
5532                     android.os.Build.VERSION_CODES.R);
5533         }
5534     }
5535 
5536     /**
5537      * Move an audio item within the given playlist.
5538      */
5539     private int movePlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values,
5540             @NonNull Bundle queryArgs) throws FallbackException {
5541         final int fromIndex = resolvePlaylistIndex(playlistUri, queryArgs);
5542         final int toIndex = values.getAsInteger(Playlists.Members.PLAY_ORDER) - 1;
5543         if (fromIndex == -1) {
5544             throw new FallbackException("Failed to resolve playlist member " + queryArgs,
5545                     android.os.Build.VERSION_CODES.R);
5546         }
5547         try {
5548             final File playlistFile = queryForDataFile(playlistUri, null);
5549 
5550             final Playlist playlist = new Playlist();
5551             playlist.read(playlistFile);
5552             final int finalIndex = playlist.move(fromIndex, toIndex);
5553             playlist.write(playlistFile);
5554 
5555             resolvePlaylistMembers(playlistUri);
5556             return finalIndex;
5557         } catch (IOException e) {
5558             throw new FallbackException("Failed to update playlist", e,
5559                     android.os.Build.VERSION_CODES.R);
5560         }
5561     }
5562 
5563     /**
5564      * Remove an audio item from the given playlist.
5565      */
5566     private int removePlaylistMembers(@NonNull Uri playlistUri, @NonNull Bundle queryArgs)
5567             throws FallbackException {
5568         final int index = resolvePlaylistIndex(playlistUri, queryArgs);
5569         try {
5570             final File playlistFile = queryForDataFile(playlistUri, null);
5571 
5572             final Playlist playlist = new Playlist();
5573             playlist.read(playlistFile);
5574             final int count;
5575             if (index == -1) {
5576                 count = playlist.asList().size();
5577                 playlist.clear();
5578             } else {
5579                 count = 1;
5580                 playlist.remove(index);
5581             }
5582             playlist.write(playlistFile);
5583 
5584             resolvePlaylistMembers(playlistUri);
5585             return count;
5586         } catch (IOException e) {
5587             throw new FallbackException("Failed to update playlist", e,
5588                     android.os.Build.VERSION_CODES.R);
5589         }
5590     }
5591 
5592     /**
5593      * Resolve query arguments that are designed to select a specific playlist
5594      * item using its {@link Playlists.Members#PLAY_ORDER}.
5595      */
5596     private int resolvePlaylistIndex(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) {
5597         final Uri membersUri = Playlists.Members.getContentUri(
5598                 getVolumeName(playlistUri), ContentUris.parseId(playlistUri));
5599 
5600         final DatabaseHelper helper;
5601         final SQLiteQueryBuilder qb;
5602         try {
5603             helper = getDatabaseForUri(membersUri);
5604             qb = getQueryBuilder(TYPE_DELETE, AUDIO_PLAYLISTS_ID_MEMBERS,
5605                     membersUri, queryArgs, null);
5606         } catch (VolumeNotFoundException ignored) {
5607             return -1;
5608         }
5609 
5610         try (Cursor c = qb.query(helper,
5611                 new String[] { Playlists.Members.PLAY_ORDER }, queryArgs, null)) {
5612             if ((c.getCount() == 1) && c.moveToFirst()) {
5613                 return c.getInt(0) - 1;
5614             } else {
5615                 return -1;
5616             }
5617         }
5618     }
5619 
5620     @Override
5621     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
5622         return openFileCommon(uri, mode, null);
5623     }
5624 
5625     @Override
5626     public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
5627             throws FileNotFoundException {
5628         return openFileCommon(uri, mode, signal);
5629     }
5630 
5631     private ParcelFileDescriptor openFileCommon(Uri uri, String mode, CancellationSignal signal)
5632             throws FileNotFoundException {
5633         uri = safeUncanonicalize(uri);
5634 
5635         final boolean allowHidden = isCallingPackageAllowedHidden();
5636         final int match = matchUri(uri, allowHidden);
5637         final String volumeName = getVolumeName(uri);
5638 
5639         // Handle some legacy cases where we need to redirect thumbnails
5640         switch (match) {
5641             case AUDIO_ALBUMART_ID: {
5642                 final long albumId = Long.parseLong(uri.getPathSegments().get(3));
5643                 final Uri targetUri = ContentUris
5644                         .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId);
5645                 return ensureThumbnail(targetUri, signal);
5646             }
5647             case AUDIO_ALBUMART_FILE_ID: {
5648                 final long audioId = Long.parseLong(uri.getPathSegments().get(3));
5649                 final Uri targetUri = ContentUris
5650                         .withAppendedId(Audio.Media.getContentUri(volumeName), audioId);
5651                 return ensureThumbnail(targetUri, signal);
5652             }
5653             case VIDEO_MEDIA_ID_THUMBNAIL: {
5654                 final long videoId = Long.parseLong(uri.getPathSegments().get(3));
5655                 final Uri targetUri = ContentUris
5656                         .withAppendedId(Video.Media.getContentUri(volumeName), videoId);
5657                 return ensureThumbnail(targetUri, signal);
5658             }
5659             case IMAGES_MEDIA_ID_THUMBNAIL: {
5660                 final long imageId = Long.parseLong(uri.getPathSegments().get(3));
5661                 final Uri targetUri = ContentUris
5662                         .withAppendedId(Images.Media.getContentUri(volumeName), imageId);
5663                 return ensureThumbnail(targetUri, signal);
5664             }
5665         }
5666 
5667         return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal);
5668     }
5669 
5670     @Override
5671     public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
5672             throws FileNotFoundException {
5673         return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, null);
5674     }
5675 
5676     @Override
5677     public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
5678             CancellationSignal signal) throws FileNotFoundException {
5679         return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, signal);
5680     }
5681 
5682     private AssetFileDescriptor openTypedAssetFileCommon(Uri uri, String mimeTypeFilter,
5683             Bundle opts, CancellationSignal signal) throws FileNotFoundException {
5684         uri = safeUncanonicalize(uri);
5685 
5686         // TODO: enforce that caller has access to this uri
5687 
5688         // Offer thumbnail of media, when requested
5689         final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE)
5690                 && MimeUtils.startsWithIgnoreCase(mimeTypeFilter, "image/");
5691         if (wantsThumb) {
5692             final ParcelFileDescriptor pfd = ensureThumbnail(uri, signal);
5693             return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
5694         }
5695 
5696         // Worst case, return the underlying file
5697         return new AssetFileDescriptor(openFileCommon(uri, "r", signal), 0,
5698                 AssetFileDescriptor.UNKNOWN_LENGTH);
5699     }
5700 
5701     private ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
5702             throws FileNotFoundException {
5703         final boolean allowHidden = isCallingPackageAllowedHidden();
5704         final int match = matchUri(uri, allowHidden);
5705 
5706         Trace.beginSection("ensureThumbnail");
5707         final LocalCallingIdentity token = clearLocalCallingIdentity();
5708         try {
5709             switch (match) {
5710                 case AUDIO_ALBUMS_ID: {
5711                     final String volumeName = MediaStore.getVolumeName(uri);
5712                     final Uri baseUri = MediaStore.Audio.Media.getContentUri(volumeName);
5713                     final long albumId = ContentUris.parseId(uri);
5714                     try (Cursor c = query(baseUri, new String[] { MediaStore.Audio.Media._ID },
5715                             MediaStore.Audio.Media.ALBUM_ID + "=" + albumId, null, null, signal)) {
5716                         if (c.moveToFirst()) {
5717                             final long audioId = c.getLong(0);
5718                             final Uri targetUri = ContentUris.withAppendedId(baseUri, audioId);
5719                             return mAudioThumbnailer.ensureThumbnail(targetUri, signal);
5720                         } else {
5721                             throw new FileNotFoundException("No media for album " + uri);
5722                         }
5723                     }
5724                 }
5725                 case AUDIO_MEDIA_ID:
5726                     return mAudioThumbnailer.ensureThumbnail(uri, signal);
5727                 case VIDEO_MEDIA_ID:
5728                     return mVideoThumbnailer.ensureThumbnail(uri, signal);
5729                 case IMAGES_MEDIA_ID:
5730                     return mImageThumbnailer.ensureThumbnail(uri, signal);
5731                 case FILES_ID:
5732                 case DOWNLOADS_ID: {
5733                     // When item is referenced in a generic way, resolve to actual type
5734                     final int mediaType = MimeUtils.resolveMediaType(getType(uri));
5735                     switch (mediaType) {
5736                         case FileColumns.MEDIA_TYPE_AUDIO:
5737                             return mAudioThumbnailer.ensureThumbnail(uri, signal);
5738                         case FileColumns.MEDIA_TYPE_VIDEO:
5739                             return mVideoThumbnailer.ensureThumbnail(uri, signal);
5740                         case FileColumns.MEDIA_TYPE_IMAGE:
5741                             return mImageThumbnailer.ensureThumbnail(uri, signal);
5742                         default:
5743                             throw new FileNotFoundException();
5744                     }
5745                 }
5746                 default:
5747                     throw new FileNotFoundException();
5748             }
5749         } catch (IOException e) {
5750             Log.w(TAG, e);
5751             throw new FileNotFoundException(e.getMessage());
5752         } finally {
5753             restoreLocalCallingIdentity(token);
5754             Trace.endSection();
5755         }
5756     }
5757 
5758     /**
5759      * Update the metadata columns for the image residing at given {@link Uri}
5760      * by reading data from the underlying image.
5761      */
5762     private void updateImageMetadata(ContentValues values, File file) {
5763         final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options();
5764         bitmapOpts.inJustDecodeBounds = true;
5765         BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOpts);
5766 
5767         values.put(MediaColumns.WIDTH, bitmapOpts.outWidth);
5768         values.put(MediaColumns.HEIGHT, bitmapOpts.outHeight);
5769     }
5770 
5771     private void handleInsertedRowForFuse(long rowId) {
5772         if (isFuseThread()) {
5773             // Removes restored row ID saved list.
5774             mCallingIdentity.get().removeDeletedRowId(rowId);
5775         }
5776     }
5777 
5778     private void handleUpdatedRowForFuse(@NonNull String oldPath, @NonNull String ownerPackage,
5779             long oldRowId, long newRowId) {
5780         if (oldRowId == newRowId) {
5781             // Update didn't delete or add row ID. We don't need to save row ID or remove saved
5782             // deleted ID.
5783             return;
5784         }
5785 
5786         handleDeletedRowForFuse(oldPath, ownerPackage, oldRowId);
5787         handleInsertedRowForFuse(newRowId);
5788     }
5789 
5790     private void handleDeletedRowForFuse(@NonNull String path, @NonNull String ownerPackage,
5791             long rowId) {
5792         if (!isFuseThread()) {
5793             return;
5794         }
5795 
5796         // Invalidate saved owned ID's of the previous owner of the deleted path, this prevents old
5797         // owner from gaining access to newly created file with restored row ID.
5798         if (!ownerPackage.equals("null") && !ownerPackage.equals(getCallingPackageOrSelf())) {
5799             invalidateLocalCallingIdentityCache(ownerPackage, "owned_database_row_deleted:"
5800                     + path);
5801         }
5802         // Saves row ID corresponding to deleted path. Saved row ID will be restored on subsequent
5803         // create or rename.
5804         mCallingIdentity.get().addDeletedRowId(path, rowId);
5805     }
5806 
5807     private void handleOwnerPackageNameChange(@NonNull String oldPath,
5808             @NonNull String oldOwnerPackage, @NonNull String newOwnerPackage) {
5809         if (Objects.equals(oldOwnerPackage, newOwnerPackage)) {
5810             return;
5811         }
5812         // Invalidate saved owned ID's of the previous owner of the renamed path, this prevents old
5813         // owner from gaining access to replaced file.
5814         invalidateLocalCallingIdentityCache(oldOwnerPackage, "owner_package_changed:" + oldPath);
5815     }
5816 
5817     /**
5818      * Return the {@link MediaColumns#DATA} field for the given {@code Uri}.
5819      */
5820     File queryForDataFile(Uri uri, CancellationSignal signal)
5821             throws FileNotFoundException {
5822         return queryForDataFile(uri, null, null, signal);
5823     }
5824 
5825     /**
5826      * Return the {@link MediaColumns#DATA} field for the given {@code Uri}.
5827      */
5828     File queryForDataFile(Uri uri, String selection, String[] selectionArgs,
5829             CancellationSignal signal) throws FileNotFoundException {
5830         try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns.DATA },
5831                 selection, selectionArgs, signal)) {
5832             final String data = cursor.getString(0);
5833             if (TextUtils.isEmpty(data)) {
5834                 throw new FileNotFoundException("Missing path for " + uri);
5835             } else {
5836                 return new File(data);
5837             }
5838         }
5839     }
5840 
5841     /**
5842      * Return the {@link Uri} for the given {@code File}.
5843      */
5844     Uri queryForMediaUri(File file, CancellationSignal signal) throws FileNotFoundException {
5845         final String volumeName = FileUtils.getVolumeName(getContext(), file);
5846         final Uri uri = Files.getContentUri(volumeName);
5847         try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns._ID },
5848                 MediaColumns.DATA + "=?", new String[] { file.getAbsolutePath() }, signal)) {
5849             return ContentUris.withAppendedId(uri, cursor.getLong(0));
5850         }
5851     }
5852 
5853     /**
5854      * Query the given {@link Uri}, expecting only a single item to be found.
5855      *
5856      * @throws FileNotFoundException if no items were found, or multiple items
5857      *             were found, or there was trouble reading the data.
5858      */
5859     Cursor queryForSingleItem(Uri uri, String[] projection, String selection,
5860             String[] selectionArgs, CancellationSignal signal) throws FileNotFoundException {
5861         final Cursor c = query(uri, projection,
5862                 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null), signal);
5863         if (c == null) {
5864             throw new FileNotFoundException("Missing cursor for " + uri);
5865         } else if (c.getCount() < 1) {
5866             FileUtils.closeQuietly(c);
5867             throw new FileNotFoundException("No item at " + uri);
5868         } else if (c.getCount() > 1) {
5869             FileUtils.closeQuietly(c);
5870             throw new FileNotFoundException("Multiple items at " + uri);
5871         }
5872 
5873         if (c.moveToFirst()) {
5874             return c;
5875         } else {
5876             FileUtils.closeQuietly(c);
5877             throw new FileNotFoundException("Failed to read row from " + uri);
5878         }
5879     }
5880 
5881     /**
5882      * Compares {@code itemOwner} with package name of {@link LocalCallingIdentity} and throws
5883      * {@link IllegalStateException} if it doesn't match.
5884      * Make sure to set calling identity properly before calling.
5885      */
5886     private void requireOwnershipForItem(@Nullable String itemOwner, Uri item) {
5887         final boolean hasOwner = (itemOwner != null);
5888         final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), itemOwner);
5889         if (hasOwner && !callerIsOwner) {
5890             throw new IllegalStateException(
5891                     "Only owner is able to interact with pending item " + item);
5892         }
5893     }
5894 
5895     private File getFuseFile(File file) {
5896         String filePath = file.getPath().replaceFirst(
5897                 "/storage/", "/mnt/user/" + UserHandle.myUserId() + "/");
5898         return new File(filePath);
5899     }
5900 
5901     private @NonNull FuseDaemon getFuseDaemonForFile(@NonNull File file)
5902             throws FileNotFoundException {
5903         final FuseDaemon daemon = ExternalStorageServiceImpl.getFuseDaemon(getVolumeId(file));
5904         if (daemon == null) {
5905             throw new FileNotFoundException("Missing FUSE daemon for " + file);
5906         } else {
5907             return daemon;
5908         }
5909     }
5910 
5911     private void invalidateFuseDentry(@NonNull File file) {
5912         invalidateFuseDentry(file.getAbsolutePath());
5913     }
5914 
5915     private void invalidateFuseDentry(@NonNull String path) {
5916         try {
5917             final FuseDaemon daemon = getFuseDaemonForFile(new File(path));
5918             if (isFuseThread()) {
5919                 // If we are on a FUSE thread, we don't need to invalidate,
5920                 // (and *must* not, otherwise we'd crash) because the invalidation
5921                 // is already reflected in the lower filesystem
5922                 return;
5923             } else {
5924                 daemon.invalidateFuseDentryCache(path);
5925             }
5926         } catch (FileNotFoundException e) {
5927             Log.w(TAG, "Failed to invalidate FUSE dentry", e);
5928         }
5929     }
5930 
5931     /**
5932      * Replacement for {@link #openFileHelper(Uri, String)} which enforces any
5933      * permissions applicable to the path before returning.
5934      *
5935      * <p>This function should never be called from the fuse thread since it tries to open
5936      * a "/mnt/user" path.
5937      */
5938     private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match,
5939             String mode, CancellationSignal signal) throws FileNotFoundException {
5940         int modeBits = ParcelFileDescriptor.parseMode(mode);
5941         boolean forWrite = (modeBits & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0;
5942         if (forWrite) {
5943             // Upgrade 'w' only to 'rw'. This allows us acquire a WR_LOCK when calling
5944             // #shouldOpenWithFuse
5945             modeBits |= ParcelFileDescriptor.MODE_READ_WRITE;
5946         }
5947 
5948         final boolean hasOwnerPackageName = hasOwnerPackageName(uri);
5949         final String[] projection = new String[] {
5950                 MediaColumns.DATA,
5951                 hasOwnerPackageName ? MediaColumns.OWNER_PACKAGE_NAME : "NULL",
5952                 hasOwnerPackageName ? MediaColumns.IS_PENDING : "0",
5953         };
5954 
5955         final File file;
5956         final String ownerPackageName;
5957         final boolean isPending;
5958         final LocalCallingIdentity token = clearLocalCallingIdentity();
5959         try (Cursor c = queryForSingleItem(uri, projection, null, null, signal)) {
5960             final String data = c.getString(0);
5961             if (TextUtils.isEmpty(data)) {
5962                 throw new FileNotFoundException("Missing path for " + uri);
5963             } else {
5964                 file = new File(data).getCanonicalFile();
5965             }
5966             ownerPackageName = c.getString(1);
5967             isPending = c.getInt(2) != 0;
5968         } catch (IOException e) {
5969             throw new FileNotFoundException(e.toString());
5970         } finally {
5971             restoreLocalCallingIdentity(token);
5972         }
5973 
5974         checkAccess(uri, Bundle.EMPTY, file, forWrite);
5975 
5976         // We don't check ownership for files with IS_PENDING set by FUSE
5977         if (isPending && !isPendingFromFuse(file)) {
5978             requireOwnershipForItem(ownerPackageName, uri);
5979         }
5980 
5981         final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName);
5982         // Figure out if we need to redact contents
5983         final boolean redactionNeeded = callerIsOwner ? false : isRedactionNeeded(uri);
5984         final RedactionInfo redactionInfo;
5985         try {
5986             redactionInfo = redactionNeeded ? getRedactionRanges(file)
5987                     : new RedactionInfo(new long[0], new long[0]);
5988         } catch(IOException e) {
5989             throw new IllegalStateException(e);
5990         }
5991 
5992         // Yell if caller requires original, since we can't give it to them
5993         // unless they have access granted above
5994         if (redactionNeeded && MediaStore.getRequireOriginal(uri)) {
5995             throw new UnsupportedOperationException(
5996                     "Caller must hold ACCESS_MEDIA_LOCATION permission to access original");
5997         }
5998 
5999         // Kick off metadata update when writing is finished
6000         final OnCloseListener listener = (e) -> {
6001             // We always update metadata to reflect the state on disk, even when
6002             // the remote writer tried claiming an exception
6003             invalidateThumbnails(uri);
6004 
6005             try {
6006                 switch (match) {
6007                     case IMAGES_THUMBNAILS_ID:
6008                     case VIDEO_THUMBNAILS_ID:
6009                         final ContentValues values = new ContentValues();
6010                         updateImageMetadata(values, file);
6011                         update(uri, values, null, null);
6012                         break;
6013                     default:
6014                         mMediaScanner.scanFile(file, REASON_DEMAND);
6015                         break;
6016                 }
6017             } catch (Exception e2) {
6018                 Log.w(TAG, "Failed to update metadata for " + uri, e2);
6019             }
6020         };
6021 
6022         try {
6023             // First, handle any redaction that is needed for caller
6024             final ParcelFileDescriptor pfd;
6025             final String filePath = file.getPath();
6026             if (redactionInfo.redactionRanges.length > 0) {
6027                 if (SystemProperties.getBoolean(PROP_FUSE, false)) {
6028                     // If fuse is enabled, we can provide an fd that points to the fuse
6029                     // file system and handle redaction in the fuse handler when the caller reads.
6030                     Log.i(TAG, "Redacting with new FUSE for " + filePath);
6031                     long tid = android.os.Process.myTid();
6032                     synchronized (mShouldRedactThreadIds) {
6033                         mShouldRedactThreadIds.add(tid);
6034                     }
6035                     try {
6036                         pfd = FileUtils.openSafely(getFuseFile(file), modeBits);
6037                     } finally {
6038                         synchronized (mShouldRedactThreadIds) {
6039                             mShouldRedactThreadIds.remove(mShouldRedactThreadIds.indexOf(tid));
6040                         }
6041                     }
6042                 } else {
6043                     // TODO(b/135341978): Remove this and associated code
6044                     // when fuse is on by default.
6045                     Log.i(TAG, "Redacting with old FUSE for " + filePath);
6046                     pfd = RedactingFileDescriptor.open(
6047                             getContext(),
6048                             file,
6049                             modeBits,
6050                             redactionInfo.redactionRanges,
6051                             redactionInfo.freeOffsets);
6052                 }
6053             } else {
6054                 FuseDaemon daemon = null;
6055                 try {
6056                     daemon = getFuseDaemonForFile(file);
6057                 } catch (FileNotFoundException ignored) {
6058                 }
6059                 ParcelFileDescriptor lowerFsFd = FileUtils.openSafely(file, modeBits);
6060                 // Always acquire a readLock. This allows us make multiple opens via lower
6061                 // filesystem
6062                 boolean shouldOpenWithFuse = daemon != null
6063                         && daemon.shouldOpenWithFuse(filePath, true /* forRead */, lowerFsFd.getFd());
6064 
6065                 if (SystemProperties.getBoolean(PROP_FUSE, false) && shouldOpenWithFuse) {
6066                     // If the file is already opened on the FUSE mount with VFS caching enabled
6067                     // we return an upper filesystem fd (via FUSE) to avoid file corruption
6068                     // resulting from cache inconsistencies between the upper and lower
6069                     // filesystem caches
6070                     Log.w(TAG, "Using FUSE for " + filePath);
6071                     pfd = FileUtils.openSafely(getFuseFile(file), modeBits);
6072                     try {
6073                         lowerFsFd.close();
6074                     } catch (IOException e) {
6075                         Log.w(TAG, "Failed to close lower filesystem fd " + file.getPath(), e);
6076                     }
6077                 } else {
6078                     Log.i(TAG, "Using lower FS for " + filePath);
6079                     if (forWrite) {
6080                         // When opening for write on the lower filesystem, invalidate the VFS dentry
6081                         // so subsequent open/getattr calls will return correctly.
6082                         //
6083                         // A 'dirty' dentry with write back cache enabled can cause the kernel to
6084                         // ignore file attributes or even see stale page cache data when the lower
6085                         // filesystem has been modified outside of the FUSE driver
6086                         invalidateFuseDentry(file);
6087                     }
6088 
6089                     pfd = lowerFsFd;
6090                 }
6091             }
6092 
6093             // Second, wrap in any listener that we've requested
6094             if (!isPending && forWrite && listener != null) {
6095                 return ParcelFileDescriptor.wrap(pfd, BackgroundThread.getHandler(), listener);
6096             } else {
6097                 return pfd;
6098             }
6099         } catch (IOException e) {
6100             if (e instanceof FileNotFoundException) {
6101                 throw (FileNotFoundException) e;
6102             } else {
6103                 throw new IllegalStateException(e);
6104             }
6105         }
6106     }
6107 
6108     private void deleteAndInvalidate(@NonNull Path path) {
6109         deleteAndInvalidate(path.toFile());
6110     }
6111 
6112     private void deleteAndInvalidate(@NonNull File file) {
6113         file.delete();
6114         invalidateFuseDentry(file);
6115     }
6116 
6117     private void deleteIfAllowed(Uri uri, Bundle extras, String path) {
6118         try {
6119             final File file = new File(path);
6120             checkAccess(uri, extras, file, true);
6121             deleteAndInvalidate(file);
6122         } catch (Exception e) {
6123             Log.e(TAG, "Couldn't delete " + path, e);
6124         }
6125     }
6126 
6127     @Deprecated
6128     private boolean isPending(Uri uri) {
6129         final int match = matchUri(uri, true);
6130         switch (match) {
6131             case AUDIO_MEDIA_ID:
6132             case VIDEO_MEDIA_ID:
6133             case IMAGES_MEDIA_ID:
6134                 try (Cursor c = queryForSingleItem(uri,
6135                         new String[] { MediaColumns.IS_PENDING }, null, null, null)) {
6136                     return (c.getInt(0) != 0);
6137                 } catch (FileNotFoundException e) {
6138                     throw new IllegalStateException(e);
6139                 }
6140             default:
6141                 return false;
6142         }
6143     }
6144 
6145     @Deprecated
6146     private boolean isRedactionNeeded(Uri uri) {
6147         return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED);
6148     }
6149 
6150     private boolean isRedactionNeeded() {
6151         return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED);
6152     }
6153 
6154     private boolean isCallingPackageRequestingLegacy() {
6155         return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_GRANTED);
6156     }
6157 
6158     private static int getFileMediaType(String path) {
6159         final File file = new File(path);
6160         final String mimeType = MimeUtils.resolveMimeType(file);
6161         return MimeUtils.resolveMediaType(mimeType);
6162     }
6163 
6164     private boolean canAccessMediaFile(String filePath, boolean allowLegacy) {
6165         if (!allowLegacy && isCallingPackageRequestingLegacy()) {
6166             return false;
6167         }
6168         switch (getFileMediaType(filePath)) {
6169             case FileColumns.MEDIA_TYPE_IMAGE:
6170                 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES);
6171             case FileColumns.MEDIA_TYPE_VIDEO:
6172                 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO);
6173             default:
6174                 return false;
6175         }
6176     }
6177 
6178     /**
6179      * Returns true if:
6180      * <ul>
6181      * <li>the calling identity is an app targeting Q or older versions AND is requesting legacy
6182      * storage
6183      * <li>the calling identity holds {@code MANAGE_EXTERNAL_STORAGE}
6184      * <li>the calling identity owns filePath (eg /Android/data/com.foo)
6185      * <li>the calling identity has permission to write images and the given file is an image file
6186      * <li>the calling identity has permission to write video and the given file is an video file
6187      * </ul>
6188      */
6189     private boolean shouldBypassFuseRestrictions(boolean forWrite, String filePath) {
6190         boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite()
6191                 : isCallingPackageLegacyRead();
6192         if (isRequestingLegacyStorage) {
6193             return true;
6194         }
6195 
6196         if (isCallingPackageManager()) {
6197             return true;
6198         }
6199 
6200         // Files under the apps own private directory
6201         final String appSpecificDir = extractPathOwnerPackageName(filePath);
6202         if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) {
6203             return true;
6204         }
6205 
6206         // Apps with write access to images and/or videos can bypass our restrictions if all of the
6207         // the files they're accessing are of the compatible media type.
6208         if (canAccessMediaFile(filePath, /*allowLegacy*/ true)) {
6209             return true;
6210         }
6211 
6212         return false;
6213     }
6214 
6215     /**
6216      * Returns true if the passed in path is an application-private data directory
6217      * (such as Android/data/com.foo or Android/obb/com.foo) that does not belong to the caller.
6218      */
6219     private boolean isPrivatePackagePathNotOwnedByCaller(String path) {
6220         // Files under the apps own private directory
6221         final String appSpecificDir = extractPathOwnerPackageName(path);
6222 
6223         if (appSpecificDir == null) {
6224             return false;
6225         }
6226 
6227         final String relativePath = extractRelativePath(path);
6228         // Android/media is not considered private, because it contains media that is explicitly
6229         // scanned and shared by other apps
6230         if (relativePath.startsWith("Android/media")) {
6231             return false;
6232         }
6233 
6234         // This is a private-package path; return true if not owned by the caller
6235         return !isCallingIdentitySharedPackageName(appSpecificDir);
6236     }
6237 
6238     /**
6239      * Set of Exif tags that should be considered for redaction.
6240      */
6241     private static final String[] REDACTED_EXIF_TAGS = new String[] {
6242             ExifInterface.TAG_GPS_ALTITUDE,
6243             ExifInterface.TAG_GPS_ALTITUDE_REF,
6244             ExifInterface.TAG_GPS_AREA_INFORMATION,
6245             ExifInterface.TAG_GPS_DOP,
6246             ExifInterface.TAG_GPS_DATESTAMP,
6247             ExifInterface.TAG_GPS_DEST_BEARING,
6248             ExifInterface.TAG_GPS_DEST_BEARING_REF,
6249             ExifInterface.TAG_GPS_DEST_DISTANCE,
6250             ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
6251             ExifInterface.TAG_GPS_DEST_LATITUDE,
6252             ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
6253             ExifInterface.TAG_GPS_DEST_LONGITUDE,
6254             ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
6255             ExifInterface.TAG_GPS_DIFFERENTIAL,
6256             ExifInterface.TAG_GPS_IMG_DIRECTION,
6257             ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
6258             ExifInterface.TAG_GPS_LATITUDE,
6259             ExifInterface.TAG_GPS_LATITUDE_REF,
6260             ExifInterface.TAG_GPS_LONGITUDE,
6261             ExifInterface.TAG_GPS_LONGITUDE_REF,
6262             ExifInterface.TAG_GPS_MAP_DATUM,
6263             ExifInterface.TAG_GPS_MEASURE_MODE,
6264             ExifInterface.TAG_GPS_PROCESSING_METHOD,
6265             ExifInterface.TAG_GPS_SATELLITES,
6266             ExifInterface.TAG_GPS_SPEED,
6267             ExifInterface.TAG_GPS_SPEED_REF,
6268             ExifInterface.TAG_GPS_STATUS,
6269             ExifInterface.TAG_GPS_TIMESTAMP,
6270             ExifInterface.TAG_GPS_TRACK,
6271             ExifInterface.TAG_GPS_TRACK_REF,
6272             ExifInterface.TAG_GPS_VERSION_ID,
6273     };
6274 
6275     /**
6276      * Set of ISO boxes that should be considered for redaction.
6277      */
6278     private static final int[] REDACTED_ISO_BOXES = new int[] {
6279             IsoInterface.BOX_LOCI,
6280             IsoInterface.BOX_XYZ,
6281             IsoInterface.BOX_GPS,
6282             IsoInterface.BOX_GPS0,
6283     };
6284 
6285     public static final Set<String> sRedactedExifTags = new ArraySet<>(
6286             Arrays.asList(REDACTED_EXIF_TAGS));
6287 
6288     private static final class RedactionInfo {
6289         public final long[] redactionRanges;
6290         public final long[] freeOffsets;
6291         public RedactionInfo(long[] redactionRanges, long[] freeOffsets) {
6292             this.redactionRanges = redactionRanges;
6293             this.freeOffsets = freeOffsets;
6294         }
6295     }
6296 
6297     /**
6298      * Calculates the ranges that need to be redacted for the given file and user that wants to
6299      * access the file.
6300      *
6301      * @param uid UID of the package wanting to access the file
6302      * @param path File path
6303      * @param tid thread id making IO on the FUSE filesystem
6304      * @return Ranges that should be redacted.
6305      *
6306      * @throws IOException if an error occurs while calculating the redaction ranges
6307      *
6308      * Called from JNI in jni/MediaProviderWrapper.cpp
6309      */
6310     @Keep
6311     @NonNull
6312     public long[] getRedactionRangesForFuse(String path, int uid, int tid) throws IOException {
6313         final File file = new File(path);
6314 
6315         // When we're calculating redaction ranges for MediaProvider, it means we're actually
6316         // calculating redaction ranges for another app that called to MediaProvider through Binder.
6317         // If the tid is in mShouldRedactThreadIds, we should redact, otherwise, we don't redact
6318         if (uid == android.os.Process.myUid()) {
6319             boolean shouldRedact = false;
6320             synchronized (mShouldRedactThreadIds) {
6321                 shouldRedact = mShouldRedactThreadIds.indexOf(tid) != -1;
6322             }
6323             if (shouldRedact) {
6324                 return getRedactionRanges(file).redactionRanges;
6325             } else {
6326                 return new long[0];
6327             }
6328         }
6329 
6330         final LocalCallingIdentity token =
6331                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
6332 
6333         long[] res = new long[0];
6334         try {
6335             if (!isRedactionNeeded()
6336                     || shouldBypassFuseRestrictions(/*forWrite*/ false, path)) {
6337                 return res;
6338             }
6339 
6340             final Uri contentUri = FileUtils.getContentUriForPath(path);
6341             final String[] projection = new String[]{
6342                     MediaColumns.OWNER_PACKAGE_NAME, MediaColumns._ID };
6343             final String selection = MediaColumns.DATA + "=?";
6344             final String[] selectionArgs = new String[] { path };
6345             final String ownerPackageName;
6346             final Uri item;
6347             try (final Cursor c = queryForSingleItem(contentUri, projection, selection,
6348                     selectionArgs, null)) {
6349                 c.moveToFirst();
6350                 ownerPackageName = c.getString(0);
6351                 item = ContentUris.withAppendedId(contentUri, /*item id*/ c.getInt(1));
6352             } catch (FileNotFoundException e) {
6353                 // Ideally, this shouldn't happen unless the file was deleted after we checked its
6354                 // existence and before we get to the redaction logic here. In this case we throw
6355                 // and fail the operation and FuseDaemon should handle this and fail the whole open
6356                 // operation gracefully.
6357                 throw new FileNotFoundException(
6358                         path + " not found while calculating redaction ranges: " + e.getMessage());
6359             }
6360 
6361             final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(),
6362                     ownerPackageName);
6363             final boolean callerHasUriPermission = getContext().checkUriPermission(
6364                     item, mCallingIdentity.get().pid, mCallingIdentity.get().uid,
6365                     Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PERMISSION_GRANTED;
6366 
6367             if (!callerIsOwner && !callerHasUriPermission) {
6368                 res = getRedactionRanges(file).redactionRanges;
6369             }
6370         } finally {
6371             restoreLocalCallingIdentity(token);
6372         }
6373         return res;
6374     }
6375 
6376     /**
6377      * Calculates the ranges containing sensitive metadata that should be redacted if the caller
6378      * doesn't have the required permissions.
6379      *
6380      * @param file file to be redacted
6381      * @return the ranges to be redacted in a RedactionInfo object, could be empty redaction ranges
6382      * if there's sensitive metadata
6383      * @throws IOException if an IOException happens while calculating the redaction ranges
6384      */
6385     @VisibleForTesting
6386     public static RedactionInfo getRedactionRanges(File file) throws IOException {
6387         Trace.beginSection("getRedactionRanges");
6388         final LongArray res = new LongArray();
6389         final LongArray freeOffsets = new LongArray();
6390         try (FileInputStream is = new FileInputStream(file)) {
6391             final String mimeType = MimeUtils.resolveMimeType(file);
6392             if (ExifInterface.isSupportedMimeType(mimeType)) {
6393                 final ExifInterface exif = new ExifInterface(is.getFD());
6394                 for (String tag : REDACTED_EXIF_TAGS) {
6395                     final long[] range = exif.getAttributeRange(tag);
6396                     if (range != null) {
6397                         res.add(range[0]);
6398                         res.add(range[0] + range[1]);
6399                     }
6400                 }
6401                 // Redact xmp where present
6402                 final XmpInterface exifXmp = XmpInterface.fromContainer(exif);
6403                 res.addAll(exifXmp.getRedactionRanges());
6404             }
6405 
6406             if (IsoInterface.isSupportedMimeType(mimeType)) {
6407                 final IsoInterface iso = IsoInterface.fromFileDescriptor(is.getFD());
6408                 for (int box : REDACTED_ISO_BOXES) {
6409                     final long[] ranges = iso.getBoxRanges(box);
6410                     for (int i = 0; i < ranges.length; i += 2) {
6411                         long boxTypeOffset = ranges[i] - 4;
6412                         freeOffsets.add(boxTypeOffset);
6413                         res.add(boxTypeOffset);
6414                         res.add(ranges[i + 1]);
6415                     }
6416                 }
6417                 // Redact xmp where present
6418                 final XmpInterface isoXmp = XmpInterface.fromContainer(iso);
6419                 res.addAll(isoXmp.getRedactionRanges());
6420             }
6421         } catch (FileNotFoundException ignored) {
6422             // If file not found, then there's nothing to redact
6423         } catch (IOException e) {
6424             throw new IOException("Failed to redact " + file, e);
6425         }
6426         Trace.endSection();
6427         return new RedactionInfo(res.toArray(), freeOffsets.toArray());
6428     }
6429 
6430     /**
6431      * @return {@code true} if {@code file} is pending from FUSE, {@code false} otherwise.
6432      * Files pending from FUSE will not have pending file pattern.
6433      */
6434     private static boolean isPendingFromFuse(@NonNull File file) {
6435         final Matcher matcher =
6436                 FileUtils.PATTERN_EXPIRES_FILE.matcher(extractDisplayName(file.getName()));
6437         return !matcher.matches();
6438     }
6439 
6440     /**
6441      * Checks if the app identified by the given UID is allowed to open the given file for the given
6442      * access mode.
6443      *
6444      * @param path the path of the file to be opened
6445      * @param uid UID of the app requesting to open the file
6446      * @param forWrite specifies if the file is to be opened for write
6447      * @return 0 upon success. {@link OsConstants#EACCES} if the operation is illegal or not
6448      * permitted for the given {@code uid} or if the calling package is a legacy app that doesn't
6449      * have right storage permission.
6450      *
6451      * Called from JNI in jni/MediaProviderWrapper.cpp
6452      */
6453     @Keep
6454     public int isOpenAllowedForFuse(String path, int uid, boolean forWrite) {
6455         final LocalCallingIdentity token =
6456                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
6457 
6458         try {
6459             if (isPrivatePackagePathNotOwnedByCaller(path)) {
6460                 Log.e(TAG, "Can't open a file in another app's external directory!");
6461                 return OsConstants.ENOENT;
6462             }
6463 
6464             if (shouldBypassFuseRestrictions(forWrite, path)) {
6465                 return 0;
6466             }
6467             // Legacy apps that made is this far don't have the right storage permission and hence
6468             // are not allowed to access anything other than their external app directory
6469             if (isCallingPackageRequestingLegacy()) {
6470                 return OsConstants.EACCES;
6471             }
6472 
6473             final Uri contentUri = FileUtils.getContentUriForPath(path);
6474             final String[] projection = new String[]{
6475                     MediaColumns._ID,
6476                     MediaColumns.OWNER_PACKAGE_NAME,
6477                     MediaColumns.IS_PENDING};
6478             final String selection = MediaColumns.DATA + "=?";
6479             final String[] selectionArgs = new String[] { path };
6480             final Uri fileUri;
6481             final boolean isPending;
6482             String ownerPackageName = null;
6483             try (final Cursor c = queryForSingleItem(contentUri, projection, selection,
6484                     selectionArgs, null)) {
6485                 fileUri = ContentUris.withAppendedId(contentUri, c.getInt(0));
6486                 ownerPackageName = c.getString(1);
6487                 isPending = c.getInt(2) != 0;
6488             }
6489 
6490             final File file = new File(path);
6491             checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
6492 
6493             // We don't check ownership for files with IS_PENDING set by FUSE
6494             if (isPending && !isPendingFromFuse(new File(path))) {
6495                 requireOwnershipForItem(ownerPackageName, fileUri);
6496             }
6497             return 0;
6498         } catch (FileNotFoundException e) {
6499             // We are here because
6500             // * App doesn't have read permission to the requested path, hence queryForSingleItem
6501             //   couldn't return a valid db row, or,
6502             // * There is no db row corresponding to the requested path, which is more unlikely.
6503             // In both of these cases, it means that app doesn't have access permission to the file.
6504             Log.e(TAG, "Couldn't find file: " + path);
6505             return OsConstants.EACCES;
6506         } catch (IllegalStateException | SecurityException e) {
6507             Log.e(TAG, "Permission to access file: " + path + " is denied");
6508             return OsConstants.EACCES;
6509         } finally {
6510             restoreLocalCallingIdentity(token);
6511         }
6512     }
6513 
6514     /**
6515      * Returns {@code true} if {@link #mCallingIdentity#getSharedPackages(String)} contains the
6516      * given package name, {@code false} otherwise.
6517      * <p> Assumes that {@code mCallingIdentity} has been properly set to reflect the calling
6518      * package.
6519      */
6520     private boolean isCallingIdentitySharedPackageName(@NonNull String packageName) {
6521         for (String sharedPkgName : mCallingIdentity.get().getSharedPackageNames()) {
6522             if (packageName.toLowerCase(Locale.ROOT)
6523                     .equals(sharedPkgName.toLowerCase(Locale.ROOT))) {
6524                 return true;
6525             }
6526         }
6527         return false;
6528     }
6529 
6530     /**
6531      * @throws IllegalStateException if path is invalid or doesn't match a volume.
6532      */
6533     @NonNull
6534     private Uri getContentUriForFile(@NonNull String filePath, @NonNull String mimeType) {
6535         final String volName = FileUtils.getVolumeName(getContext(), new File(filePath));
6536         Uri uri = Files.getContentUri(volName);
6537         final String topLevelDir = extractTopLevelDir(filePath);
6538         if (topLevelDir == null) {
6539             // If the file path doesn't match the external storage directory, we use the files URI
6540             // as default and let #insert enforce the restrictions
6541             return uri;
6542         }
6543         switch (topLevelDir) {
6544             case DIRECTORY_PODCASTS:
6545             case DIRECTORY_RINGTONES:
6546             case DIRECTORY_ALARMS:
6547             case DIRECTORY_NOTIFICATIONS:
6548             case DIRECTORY_AUDIOBOOKS:
6549                 uri = Audio.Media.getContentUri(volName);
6550                 break;
6551             case DIRECTORY_MUSIC:
6552                 if (MimeUtils.isPlaylistMimeType(mimeType)) {
6553                     uri = Audio.Playlists.getContentUri(volName);
6554                 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) {
6555                     // Send Files uri for media type subtitle
6556                     uri = Audio.Media.getContentUri(volName);
6557                 }
6558                 break;
6559             case DIRECTORY_MOVIES:
6560                 if (MimeUtils.isPlaylistMimeType(mimeType)) {
6561                     uri = Audio.Playlists.getContentUri(volName);
6562                 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) {
6563                     // Send Files uri for media type subtitle
6564                     uri = Video.Media.getContentUri(volName);
6565                 }
6566                 break;
6567             case DIRECTORY_DCIM:
6568             case DIRECTORY_PICTURES:
6569                 if (MimeUtils.isImageMimeType(mimeType)) {
6570                     uri = Images.Media.getContentUri(volName);
6571                 } else {
6572                     uri = Video.Media.getContentUri(volName);
6573                 }
6574                 break;
6575             case DIRECTORY_DOWNLOADS:
6576             case DIRECTORY_DOCUMENTS:
6577                 break;
6578             default:
6579                 Log.w(TAG, "Forgot to handle a top level directory in getContentUriForFile?");
6580         }
6581         return uri;
6582     }
6583 
6584     private boolean fileExists(@NonNull String absolutePath) {
6585         // We don't care about specific columns in the match,
6586         // we just want to check IF there's a match
6587         final String[] projection = {};
6588         final String selection = FileColumns.DATA + " = ?";
6589         final String[] selectionArgs = {absolutePath};
6590         final Uri uri = FileUtils.getContentUriForPath(absolutePath);
6591 
6592         final LocalCallingIdentity token = clearLocalCallingIdentity();
6593         try {
6594             try (final Cursor c = query(uri, projection, selection, selectionArgs, null)) {
6595                 // Shouldn't return null
6596                 return c.getCount() > 0;
6597             }
6598         } finally {
6599             clearLocalCallingIdentity(token);
6600         }
6601     }
6602 
6603     private boolean isExternalMediaDirectory(@NonNull String path) {
6604         final String relativePath = extractRelativePath(path);
6605         if (relativePath != null) {
6606             return relativePath.startsWith("Android/media");
6607         }
6608         return false;
6609     }
6610 
6611     private Uri insertFileForFuse(@NonNull String path, @NonNull Uri uri, @NonNull String mimeType,
6612             boolean useData) {
6613         ContentValues values = new ContentValues();
6614         values.put(FileColumns.OWNER_PACKAGE_NAME, getCallingPackageOrSelf());
6615         values.put(MediaColumns.MIME_TYPE, mimeType);
6616         values.put(FileColumns.IS_PENDING, 1);
6617 
6618         if (useData) {
6619             values.put(FileColumns.DATA, path);
6620         } else {
6621             values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
6622             values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
6623             values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
6624         }
6625         return insert(uri, values, Bundle.EMPTY);
6626     }
6627 
6628     /**
6629      * Enforces file creation restrictions (see return values) for the given file on behalf of the
6630      * app with the given {@code uid}. If the file is is added to the shared storage, creates a
6631      * database entry for it.
6632      * <p> Does NOT create file.
6633      *
6634      * @param path the path of the file
6635      * @param uid UID of the app requesting to create the file
6636      * @return In case of success, 0. If the operation is illegal or not permitted, returns the
6637      * appropriate {@code errno} value:
6638      * <ul>
6639      * <li>{@link OsConstants#ENOENT} if the app tries to create file in other app's external dir
6640      * <li>{@link OsConstants#EEXIST} if the file already exists
6641      * <li>{@link OsConstants#EPERM} if the file type doesn't match the relative path, or if the
6642      * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission.
6643      * <li>{@link OsConstants#EIO} in case of any other I/O exception
6644      * </ul>
6645      *
6646      * @throws IllegalStateException if given path is invalid.
6647      *
6648      * Called from JNI in jni/MediaProviderWrapper.cpp
6649      */
6650     @Keep
6651     public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) {
6652         final LocalCallingIdentity token =
6653                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
6654 
6655         try {
6656             if (isPrivatePackagePathNotOwnedByCaller(path)) {
6657                 Log.e(TAG, "Can't create a file in another app's external directory");
6658                 return OsConstants.ENOENT;
6659             }
6660 
6661             if (!path.equals(getAbsoluteSanitizedPath(path))) {
6662                 Log.e(TAG, "File name contains invalid characters");
6663                 return OsConstants.EPERM;
6664             }
6665 
6666             final String mimeType = MimeUtils.resolveMimeType(new File(path));
6667 
6668             if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) {
6669                 final boolean callerRequestingLegacy = isCallingPackageRequestingLegacy();
6670                 if (!fileExists(path)) {
6671                     // If app has already inserted the db row, inserting the row again might set
6672                     // IS_PENDING=1. We shouldn't overwrite existing entry as part of FUSE
6673                     // operation, hence, insert the db row only when it doesn't exist.
6674                     try {
6675                         insertFileForFuse(path, FileUtils.getContentUriForPath(path),
6676                                 mimeType, /*useData*/ callerRequestingLegacy);
6677                     } catch (Exception ignored) {
6678                     }
6679                 } else {
6680                     // Upon creating a file via FUSE, if a row matching the path already exists
6681                     // but a file doesn't exist on the filesystem, we transfer ownership to the
6682                     // app attempting to create the file. If we don't update ownership, then the
6683                     // app that inserted the original row may be able to observe the contents of
6684                     // written file even though they don't hold the right permissions to do so.
6685                     if (callerRequestingLegacy) {
6686                         final String owner = getCallingPackageOrSelf();
6687                         if (owner != null && !updateOwnerForPath(path, owner)) {
6688                             return OsConstants.EPERM;
6689                         }
6690                     }
6691                 }
6692 
6693                 return 0;
6694             }
6695 
6696             // Legacy apps that made is this far don't have the right storage permission and hence
6697             // are not allowed to access anything other than their external app directory
6698             if (isCallingPackageRequestingLegacy()) {
6699                 return OsConstants.EPERM;
6700             }
6701 
6702             if (fileExists(path)) {
6703                 // If the file already exists in the db, we shouldn't allow the file creation.
6704                 return OsConstants.EEXIST;
6705             }
6706 
6707             final Uri contentUri = getContentUriForFile(path, mimeType);
6708             final Uri item = insertFileForFuse(path, contentUri, mimeType, /*useData*/ false);
6709             if (item == null) {
6710                 return OsConstants.EPERM;
6711             }
6712             return 0;
6713         } catch (IllegalArgumentException e) {
6714             Log.e(TAG, "insertFileIfNecessary failed", e);
6715             return OsConstants.EPERM;
6716         } finally {
6717             restoreLocalCallingIdentity(token);
6718         }
6719     }
6720 
6721     private boolean updateOwnerForPath(@NonNull String path, @NonNull String newOwner) {
6722         final DatabaseHelper helper;
6723         try {
6724             helper = getDatabaseForUri(FileUtils.getContentUriForPath(path));
6725         } catch (VolumeNotFoundException e) {
6726             // Cannot happen, as this is a path that we already resolved.
6727             throw new AssertionError("Path must already be resolved", e);
6728         }
6729 
6730         ContentValues values = new ContentValues(1);
6731         values.put(FileColumns.OWNER_PACKAGE_NAME, newOwner);
6732 
6733         return helper.runWithoutTransaction((db) -> {
6734             return db.update("files", values, "_data=?", new String[] { path });
6735         }) == 1;
6736     }
6737 
6738     private static int deleteFileUnchecked(@NonNull String path) {
6739         final File toDelete = new File(path);
6740         if (toDelete.delete()) {
6741             return 0;
6742         } else {
6743             return OsConstants.ENOENT;
6744         }
6745     }
6746 
6747     /**
6748      * Deletes file with the given {@code path} on behalf of the app with the given {@code uid}.
6749      * <p>Before deleting, checks if app has permissions to delete this file.
6750      *
6751      * @param path the path of the file
6752      * @param uid UID of the app requesting to delete the file
6753      * @return 0 upon success.
6754      * In case of error, return the appropriate negated {@code errno} value:
6755      * <ul>
6756      * <li>{@link OsConstants#ENOENT} if the file does not exist or if the app tries to delete file
6757      * in another app's external dir
6758      * <li>{@link OsConstants#EPERM} a security exception was thrown by {@link #delete}, or if the
6759      * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission.
6760      * </ul>
6761      *
6762      * Called from JNI in jni/MediaProviderWrapper.cpp
6763      */
6764     @Keep
6765     public int deleteFileForFuse(@NonNull String path, int uid) throws IOException {
6766         final LocalCallingIdentity token =
6767                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
6768         try {
6769             if (isPrivatePackagePathNotOwnedByCaller(path)) {
6770                 Log.e(TAG, "Can't delete a file in another app's external directory!");
6771                 return OsConstants.ENOENT;
6772             }
6773 
6774             final boolean shouldBypass = shouldBypassFuseRestrictions(/*forWrite*/ true, path);
6775 
6776             // Legacy apps that made is this far don't have the right storage permission and hence
6777             // are not allowed to access anything other than their external app directory
6778             if (!shouldBypass && isCallingPackageRequestingLegacy()) {
6779                 return OsConstants.EPERM;
6780             }
6781 
6782             final Uri contentUri = FileUtils.getContentUriForPath(path);
6783             final String where = FileColumns.DATA + " = ?";
6784             final String[] whereArgs = {path};
6785 
6786             if (delete(contentUri, where, whereArgs) == 0) {
6787                 if (shouldBypass) {
6788                     return deleteFileUnchecked(path);
6789                 }
6790                 return OsConstants.ENOENT;
6791             } else {
6792                 // success - 1 file was deleted
6793                 return 0;
6794             }
6795 
6796         } catch (SecurityException e) {
6797             Log.e(TAG, "File deletion not allowed", e);
6798             return OsConstants.EPERM;
6799         } finally {
6800             restoreLocalCallingIdentity(token);
6801         }
6802     }
6803 
6804     /**
6805      * Checks if the app with the given UID is allowed to create or delete the directory with the
6806      * given path.
6807      *
6808      * @param path File path of the directory that the app wants to create/delete
6809      * @param uid UID of the app that wants to create/delete the directory
6810      * @param forCreate denotes whether the operation is directory creation or deletion
6811      * @return 0 if the operation is allowed, or the following {@code errno} values:
6812      * <ul>
6813      * <li>{@link OsConstants#EACCES} if the app tries to create/delete a dir in another app's
6814      * external directory, or if the calling package is a legacy app that doesn't have
6815      * WRITE_EXTERNAL_STORAGE permission.
6816      * <li>{@link OsConstants#EPERM} if the app tries to create/delete a top-level directory.
6817      * </ul>
6818      *
6819      * Called from JNI in jni/MediaProviderWrapper.cpp
6820      */
6821     @Keep
6822     public int isDirectoryCreationOrDeletionAllowedForFuse(
6823             @NonNull String path, int uid, boolean forCreate) {
6824         final LocalCallingIdentity token =
6825                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
6826 
6827         try {
6828             // App dirs are not indexed, so we don't create an entry for the file.
6829             if (isPrivatePackagePathNotOwnedByCaller(path)) {
6830                 Log.e(TAG, "Can't modify another app's external directory!");
6831                 return OsConstants.EACCES;
6832             }
6833 
6834             if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) {
6835                 return 0;
6836             }
6837             // Legacy apps that made is this far don't have the right storage permission and hence
6838             // are not allowed to access anything other than their external app directory
6839             if (isCallingPackageRequestingLegacy()) {
6840                 return OsConstants.EACCES;
6841             }
6842 
6843             final String[] relativePath = sanitizePath(extractRelativePath(path));
6844             final boolean isTopLevelDir =
6845                     relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
6846             if (isTopLevelDir) {
6847                 // We allow creating the default top level directories only, all other operations on
6848                 // top level directories are not allowed.
6849                 if (forCreate && isDefaultDirectoryName(extractDisplayName(path))) {
6850                     return 0;
6851                 }
6852                 Log.e(TAG,
6853                         "Creating a non-default top level directory or deleting an existing"
6854                                 + " one is not allowed!");
6855                 return OsConstants.EPERM;
6856             }
6857             return 0;
6858         } finally {
6859             restoreLocalCallingIdentity(token);
6860         }
6861     }
6862 
6863     /**
6864      * Checks whether the app with the given UID is allowed to open the directory denoted by the
6865      * given path.
6866      *
6867      * @param path directory's path
6868      * @param uid UID of the requesting app
6869      * @return 0 if it's allowed to open the diretory, {@link OsConstants#EACCES} if the calling
6870      * package is a legacy app that doesn't have READ_EXTERNAL_STORAGE permission,
6871      * {@link OsConstants#ENOENT}  otherwise.
6872      *
6873      * Called from JNI in jni/MediaProviderWrapper.cpp
6874      */
6875     @Keep
6876     public int isOpendirAllowedForFuse(@NonNull String path, int uid, boolean forWrite) {
6877         final LocalCallingIdentity token =
6878                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
6879         try {
6880             if ("/storage/emulated".equals(path)) {
6881                 return OsConstants.EPERM;
6882             }
6883             if (isPrivatePackagePathNotOwnedByCaller(path)) {
6884                 Log.e(TAG, "Can't access another app's external directory!");
6885                 return OsConstants.ENOENT;
6886             }
6887 
6888             // Do not allow apps to open Android/data or Android/obb dirs. Installer and
6889             // MOUNT_EXTERNAL_ANDROID_WRITABLE apps won't be blocked by this, as their OBB dirs
6890             // are mounted to lowerfs directly.
6891             if (isDataOrObbPath(path)) {
6892                 return OsConstants.EACCES;
6893             }
6894 
6895             if (shouldBypassFuseRestrictions(forWrite, path)) {
6896                 return 0;
6897             }
6898             // Legacy apps that made is this far don't have the right storage permission and hence
6899             // are not allowed to access anything other than their external app directory
6900             if (isCallingPackageRequestingLegacy()) {
6901                 return OsConstants.EACCES;
6902             }
6903             // This is a non-legacy app. Rest of the directories are generally writable
6904             // except for non-default top-level directories.
6905             if (forWrite) {
6906                 final String[] relativePath = sanitizePath(extractRelativePath(path));
6907                 if (relativePath.length == 0) {
6908                     Log.e(TAG, "Directoy write not allowed on invalid relative path for " + path);
6909                     return OsConstants.EPERM;
6910                 }
6911                 final boolean isTopLevelDir =
6912                         relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
6913                 if (isTopLevelDir) {
6914                     if (isDefaultDirectoryName(extractDisplayName(path))) {
6915                         return 0;
6916                     } else {
6917                         Log.e(TAG,
6918                                 "Writing to a non-default top level directory is not allowed!");
6919                         return OsConstants.EACCES;
6920                     }
6921                 }
6922             }
6923 
6924             return 0;
6925         } finally {
6926             restoreLocalCallingIdentity(token);
6927         }
6928     }
6929 
6930     @Keep
6931     public boolean isUidForPackageForFuse(@NonNull String packageName, int uid) {
6932         final LocalCallingIdentity token =
6933                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
6934         try {
6935             return isCallingIdentitySharedPackageName(packageName);
6936         } finally {
6937             restoreLocalCallingIdentity(token);
6938         }
6939     }
6940 
6941     private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
6942         // System internals can work with all media
6943         if (isCallingPackageSelf() || isCallingPackageShell()) {
6944             return true;
6945         }
6946 
6947         // Apps that have permission to manage external storage can work with all files
6948         if (isCallingPackageManager()) {
6949             return true;
6950         }
6951 
6952         // Check if caller is known to be owner of this item, to speed up
6953         // performance of our permission checks
6954         final int table = matchUri(uri, true);
6955         switch (table) {
6956             case AUDIO_MEDIA_ID:
6957             case VIDEO_MEDIA_ID:
6958             case IMAGES_MEDIA_ID:
6959             case FILES_ID:
6960             case DOWNLOADS_ID:
6961                 final long id = ContentUris.parseId(uri);
6962                 if (mCallingIdentity.get().isOwned(id)) {
6963                     return true;
6964                 }
6965         }
6966 
6967         // Outstanding grant means they get access
6968         if (getContext().checkUriPermission(uri, mCallingIdentity.get().pid,
6969                 mCallingIdentity.get().uid, forWrite
6970                         ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION
6971                         : Intent.FLAG_GRANT_READ_URI_PERMISSION) == PERMISSION_GRANTED) {
6972             return true;
6973         }
6974 
6975         return false;
6976     }
6977 
6978     @VisibleForTesting
6979     public boolean isFuseThread() {
6980         return FuseDaemon.native_is_fuse_thread();
6981     }
6982 
6983     @Deprecated
6984     private boolean checkCallingPermissionAudio(boolean forWrite, String callingPackage) {
6985         if (forWrite) {
6986             return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_AUDIO);
6987         } else {
6988             // write permission should be enough for reading as well
6989             return mCallingIdentity.get().hasPermission(PERMISSION_READ_AUDIO)
6990                     || mCallingIdentity.get().hasPermission(PERMISSION_WRITE_AUDIO);
6991         }
6992     }
6993 
6994     @Deprecated
6995     private boolean checkCallingPermissionVideo(boolean forWrite, String callingPackage) {
6996         if (forWrite) {
6997             return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO);
6998         } else {
6999             // write permission should be enough for reading as well
7000             return mCallingIdentity.get().hasPermission(PERMISSION_READ_VIDEO)
7001                     || mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO);
7002         }
7003     }
7004 
7005     @Deprecated
7006     private boolean checkCallingPermissionImages(boolean forWrite, String callingPackage) {
7007         if (forWrite) {
7008             return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES);
7009         } else {
7010             // write permission should be enough for reading as well
7011             return mCallingIdentity.get().hasPermission(PERMISSION_READ_IMAGES)
7012                     || mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES);
7013         }
7014     }
7015 
7016     /**
7017      * Enforce that caller has access to the given {@link Uri}.
7018      *
7019      * @throws SecurityException if access isn't allowed.
7020      */
7021     private void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras,
7022             boolean forWrite) {
7023         Trace.beginSection("enforceCallingPermission");
7024         try {
7025             enforceCallingPermissionInternal(uri, extras, forWrite);
7026         } finally {
7027             Trace.endSection();
7028         }
7029     }
7030 
7031     private void enforceCallingPermissionInternal(@NonNull Uri uri, @NonNull Bundle extras,
7032             boolean forWrite) {
7033         Objects.requireNonNull(uri);
7034         Objects.requireNonNull(extras);
7035 
7036         // Try a simple global check first before falling back to performing a
7037         // simple query to probe for access.
7038         if (checkCallingPermissionGlobal(uri, forWrite)) {
7039             // Access allowed, yay!
7040             return;
7041         }
7042 
7043         final DatabaseHelper helper;
7044         try {
7045             helper = getDatabaseForUri(uri);
7046         } catch (VolumeNotFoundException e) {
7047             throw e.rethrowAsIllegalArgumentException();
7048         }
7049 
7050         final boolean allowHidden = isCallingPackageAllowedHidden();
7051         final int table = matchUri(uri, allowHidden);
7052 
7053         // First, check to see if caller has direct write access
7054         if (forWrite) {
7055             final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, null);
7056             try (Cursor c = qb.query(helper, new String[0],
7057                     null, null, null, null, null, null, null)) {
7058                 if (c.moveToFirst()) {
7059                     // Direct write access granted, yay!
7060                     return;
7061                 }
7062             }
7063         }
7064 
7065         // We only allow the user to grant access to specific media items in
7066         // strongly typed collections; never to broad collections
7067         boolean allowUserGrant = false;
7068         final int matchUri = matchUri(uri, true);
7069         switch (matchUri) {
7070             case IMAGES_MEDIA_ID:
7071             case AUDIO_MEDIA_ID:
7072             case VIDEO_MEDIA_ID:
7073                 allowUserGrant = true;
7074                 break;
7075         }
7076 
7077         // Second, check to see if caller has direct read access
7078         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null);
7079         try (Cursor c = qb.query(helper, new String[0],
7080                 null, null, null, null, null, null, null)) {
7081             if (c.moveToFirst()) {
7082                 if (!forWrite) {
7083                     // Direct read access granted, yay!
7084                     return;
7085                 } else if (allowUserGrant) {
7086                     // Caller has read access, but they wanted to write, and
7087                     // they'll need to get the user to grant that access
7088                     final Context context = getContext();
7089                     final Collection<Uri> uris = Arrays.asList(uri);
7090                     final PendingIntent intent = MediaStore
7091                             .createWriteRequest(ContentResolver.wrap(this), uris);
7092 
7093                     final Icon icon = getCollectionIcon(uri);
7094                     final RemoteAction action = new RemoteAction(icon,
7095                             context.getText(R.string.permission_required_action),
7096                             context.getText(R.string.permission_required_action),
7097                             intent);
7098 
7099                     throw new RecoverableSecurityException(new SecurityException(
7100                             getCallingPackageOrSelf() + " has no access to " + uri),
7101                             context.getText(R.string.permission_required), action);
7102                 }
7103             }
7104         }
7105 
7106         throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri);
7107     }
7108 
7109     private Icon getCollectionIcon(Uri uri) {
7110         final PackageManager pm = getContext().getPackageManager();
7111         final String type = uri.getPathSegments().get(1);
7112         final String groupName;
7113         switch (type) {
7114             default: groupName = android.Manifest.permission_group.STORAGE; break;
7115         }
7116         try {
7117             final PermissionGroupInfo perm = pm.getPermissionGroupInfo(groupName, 0);
7118             return Icon.createWithResource(perm.packageName, perm.icon);
7119         } catch (NameNotFoundException e) {
7120             throw new RuntimeException(e);
7121         }
7122     }
7123 
7124     private void checkAccess(@NonNull Uri uri, @NonNull Bundle extras, @NonNull File file,
7125             boolean isWrite) throws FileNotFoundException {
7126         // First, does caller have the needed row-level access?
7127         enforceCallingPermission(uri, extras, isWrite);
7128 
7129         // Second, does the path look sane?
7130         if (!FileUtils.contains(Environment.getStorageDirectory(), file)) {
7131             checkWorldReadAccess(file.getAbsolutePath());
7132         }
7133     }
7134 
7135     /**
7136      * Check whether the path is a world-readable file
7137      */
7138     @VisibleForTesting
7139     public static void checkWorldReadAccess(String path) throws FileNotFoundException {
7140         // Path has already been canonicalized, and we relax the check to look
7141         // at groups to support runtime storage permissions.
7142         final int accessBits = path.startsWith("/storage/") ? OsConstants.S_IRGRP
7143                 : OsConstants.S_IROTH;
7144         try {
7145             StructStat stat = Os.stat(path);
7146             if (OsConstants.S_ISREG(stat.st_mode) &&
7147                 ((stat.st_mode & accessBits) == accessBits)) {
7148                 checkLeadingPathComponentsWorldExecutable(path);
7149                 return;
7150             }
7151         } catch (ErrnoException e) {
7152             // couldn't stat the file, either it doesn't exist or isn't
7153             // accessible to us
7154         }
7155 
7156         throw new FileNotFoundException("Can't access " + path);
7157     }
7158 
7159     private static void checkLeadingPathComponentsWorldExecutable(String filePath)
7160             throws FileNotFoundException {
7161         File parent = new File(filePath).getParentFile();
7162 
7163         // Path has already been canonicalized, and we relax the check to look
7164         // at groups to support runtime storage permissions.
7165         final int accessBits = filePath.startsWith("/storage/") ? OsConstants.S_IXGRP
7166                 : OsConstants.S_IXOTH;
7167 
7168         while (parent != null) {
7169             if (! parent.exists()) {
7170                 // parent dir doesn't exist, give up
7171                 throw new FileNotFoundException("access denied");
7172             }
7173             try {
7174                 StructStat stat = Os.stat(parent.getPath());
7175                 if ((stat.st_mode & accessBits) != accessBits) {
7176                     // the parent dir doesn't have the appropriate access
7177                     throw new FileNotFoundException("Can't access " + filePath);
7178                 }
7179             } catch (ErrnoException e1) {
7180                 // couldn't stat() parent
7181                 throw new FileNotFoundException("Can't access " + filePath);
7182             }
7183             parent = parent.getParentFile();
7184         }
7185     }
7186 
7187     @VisibleForTesting
7188     static class FallbackException extends Exception {
7189         private final int mThrowSdkVersion;
7190 
7191         public FallbackException(String message, int throwSdkVersion) {
7192             super(message);
7193             mThrowSdkVersion = throwSdkVersion;
7194         }
7195 
7196         public FallbackException(String message, Throwable cause, int throwSdkVersion) {
7197             super(message, cause);
7198             mThrowSdkVersion = throwSdkVersion;
7199         }
7200 
7201         @Override
7202         public String getMessage() {
7203             if (getCause() != null) {
7204                 return super.getMessage() + ": " + getCause().getMessage();
7205             } else {
7206                 return super.getMessage();
7207             }
7208         }
7209 
7210         public IllegalArgumentException rethrowAsIllegalArgumentException() {
7211             throw new IllegalArgumentException(getMessage());
7212         }
7213 
7214         public Cursor translateForQuery(int targetSdkVersion) {
7215             if (targetSdkVersion >= mThrowSdkVersion) {
7216                 throw new IllegalArgumentException(getMessage());
7217             } else {
7218                 Log.w(TAG, getMessage());
7219                 return null;
7220             }
7221         }
7222 
7223         public Uri translateForInsert(int targetSdkVersion) {
7224             if (targetSdkVersion >= mThrowSdkVersion) {
7225                 throw new IllegalArgumentException(getMessage());
7226             } else {
7227                 Log.w(TAG, getMessage());
7228                 return null;
7229             }
7230         }
7231 
7232         public int translateForUpdateDelete(int targetSdkVersion) {
7233             if (targetSdkVersion >= mThrowSdkVersion) {
7234                 throw new IllegalArgumentException(getMessage());
7235             } else {
7236                 Log.w(TAG, getMessage());
7237                 return 0;
7238             }
7239         }
7240     }
7241 
7242     @VisibleForTesting
7243     static class VolumeNotFoundException extends FallbackException {
7244         public VolumeNotFoundException(String volumeName) {
7245             super("Volume " + volumeName + " not found", Build.VERSION_CODES.Q);
7246         }
7247     }
7248 
7249     @VisibleForTesting
7250     static class VolumeArgumentException extends FallbackException {
7251         public VolumeArgumentException(File actual, Collection<File> allowed) {
7252             super("Requested path " + actual + " doesn't appear under " + allowed,
7253                     Build.VERSION_CODES.Q);
7254         }
7255     }
7256 
7257     private @NonNull DatabaseHelper getDatabaseForUri(Uri uri) throws VolumeNotFoundException {
7258         final String volumeName = resolveVolumeName(uri);
7259         synchronized (mAttachedVolumeNames) {
7260             if (!mAttachedVolumeNames.contains(volumeName)) {
7261                 throw new VolumeNotFoundException(volumeName);
7262             }
7263         }
7264         if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
7265             return mInternalDatabase;
7266         } else {
7267             return mExternalDatabase;
7268         }
7269     }
7270 
7271     static boolean isMediaDatabaseName(String name) {
7272         if (INTERNAL_DATABASE_NAME.equals(name)) {
7273             return true;
7274         }
7275         if (EXTERNAL_DATABASE_NAME.equals(name)) {
7276             return true;
7277         }
7278         if (name.startsWith("external-") && name.endsWith(".db")) {
7279             return true;
7280         }
7281         return false;
7282     }
7283 
7284     static boolean isInternalMediaDatabaseName(String name) {
7285         if (INTERNAL_DATABASE_NAME.equals(name)) {
7286             return true;
7287         }
7288         return false;
7289     }
7290 
7291     private @NonNull Uri getBaseContentUri(@NonNull String volumeName) {
7292         return MediaStore.AUTHORITY_URI.buildUpon().appendPath(volumeName).build();
7293     }
7294 
7295     public Uri attachVolume(String volume, boolean validate) {
7296         if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
7297             throw new SecurityException(
7298                     "Opening and closing databases not allowed.");
7299         }
7300 
7301         // Quick sanity check for shady volume names
7302         MediaStore.checkArgumentVolumeName(volume);
7303 
7304         // Quick sanity check that volume actually exists
7305         if (!MediaStore.VOLUME_INTERNAL.equals(volume) && validate) {
7306             try {
7307                 getVolumePath(volume);
7308             } catch (IOException e) {
7309                 throw new IllegalArgumentException(
7310                         "Volume " + volume + " currently unavailable", e);
7311             }
7312         }
7313 
7314         synchronized (mAttachedVolumeNames) {
7315             mAttachedVolumeNames.add(volume);
7316         }
7317 
7318         final ContentResolver resolver = getContext().getContentResolver();
7319         final Uri uri = getBaseContentUri(volume);
7320         resolver.notifyChange(getBaseContentUri(volume), null);
7321 
7322         if (LOGV) Log.v(TAG, "Attached volume: " + volume);
7323         if (!MediaStore.VOLUME_INTERNAL.equals(volume)) {
7324             // Also notify on synthetic view of all devices
7325             resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
7326 
7327             ForegroundThread.getExecutor().execute(() -> {
7328                 final DatabaseHelper helper = MediaStore.VOLUME_INTERNAL.equals(volume)
7329                         ? mInternalDatabase : mExternalDatabase;
7330                 helper.runWithTransaction((db) -> {
7331                     ensureDefaultFolders(volume, db);
7332                     ensureThumbnailsValid(volume, db);
7333                     return null;
7334                 });
7335 
7336                 // We just finished the database operation above, we know that
7337                 // it's ready to answer queries, so notify our DocumentProvider
7338                 // so it can answer queries without risking ANR
7339                 MediaDocumentsProvider.onMediaStoreReady(getContext(), volume);
7340             });
7341         }
7342         return uri;
7343     }
7344 
7345     private void detachVolume(Uri uri) {
7346         detachVolume(MediaStore.getVolumeName(uri));
7347     }
7348 
7349     public void detachVolume(String volume) {
7350         if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
7351             throw new SecurityException(
7352                     "Opening and closing databases not allowed.");
7353         }
7354 
7355         // Quick sanity check for shady volume names
7356         MediaStore.checkArgumentVolumeName(volume);
7357 
7358         if (MediaStore.VOLUME_INTERNAL.equals(volume)) {
7359             throw new UnsupportedOperationException(
7360                     "Deleting the internal volume is not allowed");
7361         }
7362 
7363         // Signal any scanning to shut down
7364         mMediaScanner.onDetachVolume(volume);
7365 
7366         synchronized (mAttachedVolumeNames) {
7367             mAttachedVolumeNames.remove(volume);
7368         }
7369 
7370         final ContentResolver resolver = getContext().getContentResolver();
7371         final Uri uri = getBaseContentUri(volume);
7372         resolver.notifyChange(getBaseContentUri(volume), null);
7373 
7374         if (!MediaStore.VOLUME_INTERNAL.equals(volume)) {
7375             // Also notify on synthetic view of all devices
7376             resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
7377         }
7378 
7379         if (LOGV) Log.v(TAG, "Detached volume: " + volume);
7380     }
7381 
7382     @GuardedBy("mAttachedVolumeNames")
7383     private final ArraySet<String> mAttachedVolumeNames = new ArraySet<>();
7384     @GuardedBy("mCustomCollators")
7385     private final ArraySet<String> mCustomCollators = new ArraySet<>();
7386 
7387     private MediaScanner mMediaScanner;
7388 
7389     private DatabaseHelper mInternalDatabase;
7390     private DatabaseHelper mExternalDatabase;
7391 
7392     // name of the volume currently being scanned by the media scanner (or null)
7393     private String mMediaScannerVolume;
7394 
7395     // current FAT volume ID
7396     private int mVolumeId = -1;
7397 
7398     // WARNING: the values of IMAGES_MEDIA, AUDIO_MEDIA, and VIDEO_MEDIA and AUDIO_PLAYLISTS
7399     // are stored in the "files" table, so do not renumber them unless you also add
7400     // a corresponding database upgrade step for it.
7401     static final int IMAGES_MEDIA = 1;
7402     static final int IMAGES_MEDIA_ID = 2;
7403     static final int IMAGES_MEDIA_ID_THUMBNAIL = 3;
7404     static final int IMAGES_THUMBNAILS = 4;
7405     static final int IMAGES_THUMBNAILS_ID = 5;
7406 
7407     static final int AUDIO_MEDIA = 100;
7408     static final int AUDIO_MEDIA_ID = 101;
7409     static final int AUDIO_MEDIA_ID_GENRES = 102;
7410     static final int AUDIO_MEDIA_ID_GENRES_ID = 103;
7411     static final int AUDIO_GENRES = 106;
7412     static final int AUDIO_GENRES_ID = 107;
7413     static final int AUDIO_GENRES_ID_MEMBERS = 108;
7414     static final int AUDIO_GENRES_ALL_MEMBERS = 109;
7415     static final int AUDIO_PLAYLISTS = 110;
7416     static final int AUDIO_PLAYLISTS_ID = 111;
7417     static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112;
7418     static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113;
7419     static final int AUDIO_ARTISTS = 114;
7420     static final int AUDIO_ARTISTS_ID = 115;
7421     static final int AUDIO_ALBUMS = 116;
7422     static final int AUDIO_ALBUMS_ID = 117;
7423     static final int AUDIO_ARTISTS_ID_ALBUMS = 118;
7424     static final int AUDIO_ALBUMART = 119;
7425     static final int AUDIO_ALBUMART_ID = 120;
7426     static final int AUDIO_ALBUMART_FILE_ID = 121;
7427 
7428     static final int VIDEO_MEDIA = 200;
7429     static final int VIDEO_MEDIA_ID = 201;
7430     static final int VIDEO_MEDIA_ID_THUMBNAIL = 202;
7431     static final int VIDEO_THUMBNAILS = 203;
7432     static final int VIDEO_THUMBNAILS_ID = 204;
7433 
7434     static final int VOLUMES = 300;
7435     static final int VOLUMES_ID = 301;
7436 
7437     static final int MEDIA_SCANNER = 500;
7438 
7439     static final int FS_ID = 600;
7440     static final int VERSION = 601;
7441 
7442     static final int FILES = 700;
7443     static final int FILES_ID = 701;
7444 
7445     static final int DOWNLOADS = 800;
7446     static final int DOWNLOADS_ID = 801;
7447 
7448     private LocalUriMatcher mUriMatcher;
7449 
7450     private static final String[] PATH_PROJECTION = new String[] {
7451         MediaStore.MediaColumns._ID,
7452             MediaStore.MediaColumns.DATA,
7453     };
7454 
7455     private int matchUri(Uri uri, boolean allowHidden) {
7456         return mUriMatcher.matchUri(uri, allowHidden);
7457     }
7458 
7459     static class LocalUriMatcher {
7460         private final UriMatcher mPublic = new UriMatcher(UriMatcher.NO_MATCH);
7461         private final UriMatcher mHidden = new UriMatcher(UriMatcher.NO_MATCH);
7462 
7463         public int matchUri(Uri uri, boolean allowHidden) {
7464             final int publicMatch = mPublic.match(uri);
7465             if (publicMatch != UriMatcher.NO_MATCH) {
7466                 return publicMatch;
7467             }
7468 
7469             final int hiddenMatch = mHidden.match(uri);
7470             if (hiddenMatch != UriMatcher.NO_MATCH) {
7471                 // Detect callers asking about hidden behavior by looking closer when
7472                 // the matchers diverge; we only care about apps that are explicitly
7473                 // targeting a specific public API level.
7474                 if (!allowHidden) {
7475                     throw new IllegalStateException("Unknown URL: " + uri + " is hidden API");
7476                 }
7477                 return hiddenMatch;
7478             }
7479 
7480             return UriMatcher.NO_MATCH;
7481         }
7482 
7483         public LocalUriMatcher(String auth) {
7484             mPublic.addURI(auth, "*/images/media", IMAGES_MEDIA);
7485             mPublic.addURI(auth, "*/images/media/#", IMAGES_MEDIA_ID);
7486             mPublic.addURI(auth, "*/images/media/#/thumbnail", IMAGES_MEDIA_ID_THUMBNAIL);
7487             mPublic.addURI(auth, "*/images/thumbnails", IMAGES_THUMBNAILS);
7488             mPublic.addURI(auth, "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID);
7489 
7490             mPublic.addURI(auth, "*/audio/media", AUDIO_MEDIA);
7491             mPublic.addURI(auth, "*/audio/media/#", AUDIO_MEDIA_ID);
7492             mPublic.addURI(auth, "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES);
7493             mPublic.addURI(auth, "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID);
7494             mPublic.addURI(auth, "*/audio/genres", AUDIO_GENRES);
7495             mPublic.addURI(auth, "*/audio/genres/#", AUDIO_GENRES_ID);
7496             mPublic.addURI(auth, "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS);
7497             // TODO: not actually defined in API, but CTS tested
7498             mPublic.addURI(auth, "*/audio/genres/all/members", AUDIO_GENRES_ALL_MEMBERS);
7499             mPublic.addURI(auth, "*/audio/playlists", AUDIO_PLAYLISTS);
7500             mPublic.addURI(auth, "*/audio/playlists/#", AUDIO_PLAYLISTS_ID);
7501             mPublic.addURI(auth, "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS);
7502             mPublic.addURI(auth, "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID);
7503             mPublic.addURI(auth, "*/audio/artists", AUDIO_ARTISTS);
7504             mPublic.addURI(auth, "*/audio/artists/#", AUDIO_ARTISTS_ID);
7505             mPublic.addURI(auth, "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS);
7506             mPublic.addURI(auth, "*/audio/albums", AUDIO_ALBUMS);
7507             mPublic.addURI(auth, "*/audio/albums/#", AUDIO_ALBUMS_ID);
7508             // TODO: not actually defined in API, but CTS tested
7509             mPublic.addURI(auth, "*/audio/albumart", AUDIO_ALBUMART);
7510             // TODO: not actually defined in API, but CTS tested
7511             mPublic.addURI(auth, "*/audio/albumart/#", AUDIO_ALBUMART_ID);
7512             // TODO: not actually defined in API, but CTS tested
7513             mPublic.addURI(auth, "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID);
7514 
7515             mPublic.addURI(auth, "*/video/media", VIDEO_MEDIA);
7516             mPublic.addURI(auth, "*/video/media/#", VIDEO_MEDIA_ID);
7517             mPublic.addURI(auth, "*/video/media/#/thumbnail", VIDEO_MEDIA_ID_THUMBNAIL);
7518             mPublic.addURI(auth, "*/video/thumbnails", VIDEO_THUMBNAILS);
7519             mPublic.addURI(auth, "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID);
7520 
7521             mPublic.addURI(auth, "*/media_scanner", MEDIA_SCANNER);
7522 
7523             // NOTE: technically hidden, since Uri is never exposed
7524             mPublic.addURI(auth, "*/fs_id", FS_ID);
7525             // NOTE: technically hidden, since Uri is never exposed
7526             mPublic.addURI(auth, "*/version", VERSION);
7527 
7528             mHidden.addURI(auth, "*", VOLUMES_ID);
7529             mHidden.addURI(auth, null, VOLUMES);
7530 
7531             mPublic.addURI(auth, "*/file", FILES);
7532             mPublic.addURI(auth, "*/file/#", FILES_ID);
7533 
7534             mPublic.addURI(auth, "*/downloads", DOWNLOADS);
7535             mPublic.addURI(auth, "*/downloads/#", DOWNLOADS_ID);
7536         }
7537     }
7538 
7539     /**
7540      * Set of columns that can be safely mutated by external callers; all other
7541      * columns are treated as read-only, since they reflect what the media
7542      * scanner found on disk, and any mutations would be overwritten the next
7543      * time the media was scanned.
7544      */
7545     private static final ArraySet<String> sMutableColumns = new ArraySet<>();
7546 
7547     {
7548         sMutableColumns.add(MediaStore.MediaColumns.DATA);
7549         sMutableColumns.add(MediaStore.MediaColumns.RELATIVE_PATH);
7550         sMutableColumns.add(MediaStore.MediaColumns.DISPLAY_NAME);
7551         sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING);
7552         sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED);
7553         sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE);
7554         sMutableColumns.add(MediaStore.MediaColumns.OWNER_PACKAGE_NAME);
7555 
7556         sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK);
7557 
7558         sMutableColumns.add(MediaStore.Video.VideoColumns.TAGS);
7559         sMutableColumns.add(MediaStore.Video.VideoColumns.CATEGORY);
7560         sMutableColumns.add(MediaStore.Video.VideoColumns.BOOKMARK);
7561 
7562         sMutableColumns.add(MediaStore.Audio.Playlists.NAME);
7563         sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID);
7564         sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
7565 
7566         sMutableColumns.add(MediaStore.DownloadColumns.DOWNLOAD_URI);
7567         sMutableColumns.add(MediaStore.DownloadColumns.REFERER_URI);
7568 
7569         sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE);
7570         sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE);
7571     }
7572 
7573     /**
7574      * Set of columns that affect placement of files on disk.
7575      */
7576     private static final ArraySet<String> sPlacementColumns = new ArraySet<>();
7577 
7578     {
7579         sPlacementColumns.add(MediaStore.MediaColumns.DATA);
7580         sPlacementColumns.add(MediaStore.MediaColumns.RELATIVE_PATH);
7581         sPlacementColumns.add(MediaStore.MediaColumns.DISPLAY_NAME);
7582         sPlacementColumns.add(MediaStore.MediaColumns.MIME_TYPE);
7583         sPlacementColumns.add(MediaStore.MediaColumns.IS_PENDING);
7584         sPlacementColumns.add(MediaStore.MediaColumns.IS_TRASHED);
7585         sPlacementColumns.add(MediaStore.MediaColumns.DATE_EXPIRES);
7586     }
7587 
7588     /**
7589      * List of abusive custom columns that we're willing to allow via
7590      * {@link SQLiteQueryBuilder#setProjectionGreylist(List)}.
7591      */
7592     static final ArrayList<Pattern> sGreylist = new ArrayList<>();
7593 
7594     private static void addGreylistPattern(String pattern) {
7595         sGreylist.add(Pattern.compile(" *" + pattern + " *"));
7596     }
7597 
7598     static {
7599         final String maybeAs = "( (as )?[_a-z0-9]+)?";
7600         addGreylistPattern("(?i)[_a-z0-9]+" + maybeAs);
7601         addGreylistPattern("audio\\._id AS _id");
7602         addGreylistPattern("(?i)(min|max|sum|avg|total|count|cast)\\(([_a-z0-9]+" + maybeAs + "|\\*)\\)" + maybeAs);
7603         addGreylistPattern("case when case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added \\* \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added / \\d+ else \\d+ end > case when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified \\* \\d+ when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified / \\d+ else \\d+ end then case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added \\* \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added / \\d+ else \\d+ end else case when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified \\* \\d+ when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified / \\d+ else \\d+ end end as corrected_added_modified");
7604         addGreylistPattern("MAX\\(case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\* \\d+ when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else \\d+ end\\)");
7605         addGreylistPattern("MAX\\(case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added \\* \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added / \\d+ else \\d+ end\\)");
7606         addGreylistPattern("MAX\\(case when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified \\* \\d+ when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified / \\d+ else \\d+ end\\)");
7607         addGreylistPattern("\"content://media/[a-z]+/audio/media\"");
7608         addGreylistPattern("substr\\(_data, length\\(_data\\)-length\\(_display_name\\), 1\\) as filename_prevchar");
7609         addGreylistPattern("\\*" + maybeAs);
7610         addGreylistPattern("case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\* \\d+ when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else \\d+ end");
7611     }
7612 
7613     public ArrayMap<String, String> getProjectionMap(Class<?>... clazzes) {
7614         return mExternalDatabase.getProjectionMap(clazzes);
7615     }
7616 
7617     static <T> boolean containsAny(Set<T> a, Set<T> b) {
7618         for (T i : b) {
7619             if (a.contains(i)) {
7620                 return true;
7621             }
7622         }
7623         return false;
7624     }
7625 
7626     @VisibleForTesting
7627     static @Nullable Uri computeCommonPrefix(@NonNull List<Uri> uris) {
7628         if (uris.isEmpty()) return null;
7629 
7630         final Uri base = uris.get(0);
7631         final List<String> basePath = new ArrayList<>(base.getPathSegments());
7632         for (int i = 1; i < uris.size(); i++) {
7633             final List<String> probePath = uris.get(i).getPathSegments();
7634             for (int j = 0; j < basePath.size() && j < probePath.size(); j++) {
7635                 if (!Objects.equals(basePath.get(j), probePath.get(j))) {
7636                     // Trim away all remaining common elements
7637                     while (basePath.size() > j) {
7638                         basePath.remove(j);
7639                     }
7640                 }
7641             }
7642 
7643             final int probeSize = probePath.size();
7644             while (basePath.size() > probeSize) {
7645                 basePath.remove(probeSize);
7646             }
7647         }
7648 
7649         final Uri.Builder builder = base.buildUpon().path(null);
7650         for (int i = 0; i < basePath.size(); i++) {
7651             builder.appendPath(basePath.get(i));
7652         }
7653         return builder.build();
7654     }
7655 
7656     @Deprecated
7657     private String getCallingPackageOrSelf() {
7658         return mCallingIdentity.get().getPackageName();
7659     }
7660 
7661     @Deprecated
7662     @VisibleForTesting
7663     public int getCallingPackageTargetSdkVersion() {
7664         return mCallingIdentity.get().getTargetSdkVersion();
7665     }
7666 
7667     @Deprecated
7668     private boolean isCallingPackageAllowedHidden() {
7669         return isCallingPackageSelf();
7670     }
7671 
7672     @Deprecated
7673     private boolean isCallingPackageSelf() {
7674         return mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF);
7675     }
7676 
7677     @Deprecated
7678     private boolean isCallingPackageShell() {
7679         return mCallingIdentity.get().hasPermission(PERMISSION_IS_SHELL);
7680     }
7681 
7682     @Deprecated
7683     private boolean isCallingPackageManager() {
7684         return mCallingIdentity.get().hasPermission(PERMISSION_IS_MANAGER);
7685     }
7686 
7687     @Deprecated
7688     private boolean isCallingPackageDelegator() {
7689         return mCallingIdentity.get().hasPermission(PERMISSION_IS_DELEGATOR);
7690     }
7691 
7692     @Deprecated
7693     private boolean isCallingPackageLegacyRead() {
7694         return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_READ);
7695     }
7696 
7697     @Deprecated
7698     private boolean isCallingPackageLegacyWrite() {
7699         return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE);
7700     }
7701 
7702     @Override
7703     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
7704         writer.println("mThumbSize=" + mThumbSize);
7705         synchronized (mAttachedVolumeNames) {
7706             writer.println("mAttachedVolumeNames=" + mAttachedVolumeNames);
7707         }
7708         writer.println();
7709 
7710         Logging.dumpPersistent(writer);
7711     }
7712 }
7713