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