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