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