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