• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media;
18 
19 import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
20 import static android.app.AppOpsManager.permissionToOp;
21 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
22 import static android.app.PendingIntent.FLAG_IMMUTABLE;
23 import static android.app.PendingIntent.FLAG_ONE_SHOT;
24 import static android.content.ContentResolver.QUERY_ARG_SQL_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.MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION;
33 import static android.provider.CloudMediaProviderContract.METHOD_GET_ASYNC_CONTENT_PROVIDER;
34 import static android.provider.MediaStore.EXTRA_IS_STABLE_URIS_ENABLED;
35 import static android.provider.MediaStore.EXTRA_OPEN_ASSET_FILE_REQUEST;
36 import static android.provider.MediaStore.EXTRA_OPEN_FILE_REQUEST;
37 import static android.provider.MediaStore.EXTRA_URI_LIST;
38 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE;
39 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE;
40 import static android.provider.MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO;
41 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT;
42 import static android.provider.MediaStore.Files.FileColumns._SPECIAL_FORMAT_NONE;
43 import static android.provider.MediaStore.GET_BACKUP_FILES;
44 import static android.provider.MediaStore.GET_OWNER_PACKAGE_NAME;
45 import static android.provider.MediaStore.Images.ImageColumns.LATITUDE;
46 import static android.provider.MediaStore.Images.ImageColumns.LONGITUDE;
47 import static android.provider.MediaStore.MATCH_DEFAULT;
48 import static android.provider.MediaStore.MATCH_EXCLUDE;
49 import static android.provider.MediaStore.MATCH_INCLUDE;
50 import static android.provider.MediaStore.MATCH_ONLY;
51 import static android.provider.MediaStore.MEDIA_IGNORE_FILENAME;
52 import static android.provider.MediaStore.MY_UID;
53 import static android.provider.MediaStore.MediaColumns.OEM_METADATA;
54 import static android.provider.MediaStore.MediaColumns.OWNER_PACKAGE_NAME;
55 import static android.provider.MediaStore.PER_USER_RANGE;
56 import static android.provider.MediaStore.QUERY_ARG_DEFER_SCAN;
57 import static android.provider.MediaStore.QUERY_ARG_LATEST_SELECTION_ONLY;
58 import static android.provider.MediaStore.QUERY_ARG_MATCH_FAVORITE;
59 import static android.provider.MediaStore.QUERY_ARG_MATCH_PENDING;
60 import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED;
61 import static android.provider.MediaStore.QUERY_ARG_MEDIA_STANDARD_SORT_ORDER;
62 import static android.provider.MediaStore.QUERY_ARG_REDACTED_URI;
63 import static android.provider.MediaStore.QUERY_ARG_RELATED_URI;
64 import static android.provider.MediaStore.READ_BACKUP;
65 import static android.provider.MediaStore.REVOKED_ALL_READ_GRANTS_FOR_PACKAGE_CALL;
66 import static android.provider.MediaStore.VOLUME_EXTERNAL_PRIMARY;
67 import static android.provider.MediaStore.getVolumeName;
68 import static android.system.OsConstants.F_GETFL;
69 
70 import static com.android.providers.media.AccessChecker.getWhereForConstrainedAccess;
71 import static com.android.providers.media.AccessChecker.getWhereForLatestSelection;
72 import static com.android.providers.media.AccessChecker.getWhereForOwnerPackageMatch;
73 import static com.android.providers.media.AccessChecker.getWhereForUserSelectedAccess;
74 import static com.android.providers.media.AccessChecker.hasAccessToCollection;
75 import static com.android.providers.media.AccessChecker.hasUserSelectedAccess;
76 import static com.android.providers.media.AccessChecker.isRedactionNeededForPickerUri;
77 import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME;
78 import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
79 import static com.android.providers.media.LocalCallingIdentity.APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID;
80 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_ACCESS_MTP;
81 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_INSTALL_PACKAGES;
82 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_DELEGATOR;
83 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED;
84 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_READ;
85 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE;
86 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_MANAGER;
87 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED;
88 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SELF;
89 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SHELL;
90 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SYSTEM_GALLERY;
91 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES;
92 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO;
93 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_EXTERNAL_STORAGE;
94 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART;
95 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART_FILE_ID;
96 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMART_ID;
97 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMS;
98 import static com.android.providers.media.LocalUriMatcher.AUDIO_ALBUMS_ID;
99 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS;
100 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS_ID;
101 import static com.android.providers.media.LocalUriMatcher.AUDIO_ARTISTS_ID_ALBUMS;
102 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES;
103 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ALL_MEMBERS;
104 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ID;
105 import static com.android.providers.media.LocalUriMatcher.AUDIO_GENRES_ID_MEMBERS;
106 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA;
107 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID;
108 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID_GENRES;
109 import static com.android.providers.media.LocalUriMatcher.AUDIO_MEDIA_ID_GENRES_ID;
110 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS;
111 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID;
112 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID_MEMBERS;
113 import static com.android.providers.media.LocalUriMatcher.AUDIO_PLAYLISTS_ID_MEMBERS_ID;
114 import static com.android.providers.media.LocalUriMatcher.CLI;
115 import static com.android.providers.media.LocalUriMatcher.DOWNLOADS;
116 import static com.android.providers.media.LocalUriMatcher.DOWNLOADS_ID;
117 import static com.android.providers.media.LocalUriMatcher.FILES;
118 import static com.android.providers.media.LocalUriMatcher.FILES_ID;
119 import static com.android.providers.media.LocalUriMatcher.FS_ID;
120 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA;
121 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA_ID;
122 import static com.android.providers.media.LocalUriMatcher.IMAGES_MEDIA_ID_THUMBNAIL;
123 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS;
124 import static com.android.providers.media.LocalUriMatcher.IMAGES_THUMBNAILS_ID;
125 import static com.android.providers.media.LocalUriMatcher.MEDIA_GRANTS;
126 import static com.android.providers.media.LocalUriMatcher.MEDIA_SCANNER;
127 import static com.android.providers.media.LocalUriMatcher.PICKER_GET_CONTENT_ID;
128 import static com.android.providers.media.LocalUriMatcher.PICKER_ID;
129 import static com.android.providers.media.LocalUriMatcher.PICKER_INTERNAL_V2;
130 import static com.android.providers.media.LocalUriMatcher.PICKER_TRANSCODED_ID;
131 import static com.android.providers.media.LocalUriMatcher.VERSION;
132 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA;
133 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA_ID;
134 import static com.android.providers.media.LocalUriMatcher.VIDEO_MEDIA_ID_THUMBNAIL;
135 import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS;
136 import static com.android.providers.media.LocalUriMatcher.VIDEO_THUMBNAILS_ID;
137 import static com.android.providers.media.LocalUriMatcher.VOLUMES;
138 import static com.android.providers.media.LocalUriMatcher.VOLUMES_ID;
139 import static com.android.providers.media.PickerUriResolver.PICKER_GET_CONTENT_SEGMENT;
140 import static com.android.providers.media.PickerUriResolver.PICKER_SEGMENT;
141 import static com.android.providers.media.PickerUriResolver.PICKER_TRANSCODED_SEGMENT;
142 import static com.android.providers.media.PickerUriResolver.getMediaUri;
143 import static com.android.providers.media.flags.Flags.indexMediaLatitudeLongitude;
144 import static com.android.providers.media.flags.Flags.versionLockdown;
145 import static com.android.providers.media.photopicker.data.ItemsProvider.EXTRA_MIME_TYPE_SELECTION;
146 import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND;
147 import static com.android.providers.media.scan.MediaScanner.REASON_IDLE;
148 import static com.android.providers.media.util.DatabaseUtils.bindList;
149 import static com.android.providers.media.util.FileUtils.DEFAULT_FOLDER_NAMES;
150 import static com.android.providers.media.util.FileUtils.PATTERN_PENDING_FILEPATH_FOR_SQL;
151 import static com.android.providers.media.util.FileUtils.buildPrimaryVolumeFile;
152 import static com.android.providers.media.util.FileUtils.extractDisplayName;
153 import static com.android.providers.media.util.FileUtils.extractFileExtension;
154 import static com.android.providers.media.util.FileUtils.extractFileName;
155 import static com.android.providers.media.util.FileUtils.extractOwnerPackageNameFromRelativePath;
156 import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName;
157 import static com.android.providers.media.util.FileUtils.extractRelativePath;
158 import static com.android.providers.media.util.FileUtils.extractRelativePathWithDisplayName;
159 import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
160 import static com.android.providers.media.util.FileUtils.extractVolumeName;
161 import static com.android.providers.media.util.FileUtils.extractVolumePath;
162 import static com.android.providers.media.util.FileUtils.fromFuseFile;
163 import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath;
164 import static com.android.providers.media.util.FileUtils.isCrossUserEnabled;
165 import static com.android.providers.media.util.FileUtils.isDataOrObbPath;
166 import static com.android.providers.media.util.FileUtils.isDataOrObbRelativePath;
167 import static com.android.providers.media.util.FileUtils.isDownload;
168 import static com.android.providers.media.util.FileUtils.isExternalMediaDirectory;
169 import static com.android.providers.media.util.FileUtils.isObbOrChildRelativePath;
170 import static com.android.providers.media.util.FileUtils.sanitizePath;
171 import static com.android.providers.media.util.FileUtils.toFuseFile;
172 import static com.android.providers.media.util.Logging.LOGV;
173 import static com.android.providers.media.util.Logging.TAG;
174 import static com.android.providers.media.util.PermissionUtils.checkPermissionSelf;
175 import static com.android.providers.media.util.PermissionUtils.checkPermissionShell;
176 import static com.android.providers.media.util.PermissionUtils.checkPermissionSystem;
177 import static com.android.providers.media.util.StringUtils.componentStateToString;
178 import static com.android.providers.media.util.SyntheticPathUtils.REDACTED_URI_ID_PREFIX;
179 import static com.android.providers.media.util.SyntheticPathUtils.REDACTED_URI_ID_SIZE;
180 import static com.android.providers.media.util.SyntheticPathUtils.createSparseFile;
181 import static com.android.providers.media.util.SyntheticPathUtils.extractSyntheticRelativePathSegements;
182 import static com.android.providers.media.util.SyntheticPathUtils.getRedactedRelativePath;
183 import static com.android.providers.media.util.SyntheticPathUtils.isPickerPath;
184 import static com.android.providers.media.util.SyntheticPathUtils.isRedactedPath;
185 import static com.android.providers.media.util.SyntheticPathUtils.isSyntheticPath;
186 
187 import android.Manifest;
188 import android.annotation.IntDef;
189 import android.app.ActivityOptions;
190 import android.app.AppOpsManager;
191 import android.app.AppOpsManager.OnOpActiveChangedListener;
192 import android.app.AppOpsManager.OnOpChangedListener;
193 import android.app.DownloadManager;
194 import android.app.PendingIntent;
195 import android.app.RecoverableSecurityException;
196 import android.app.RemoteAction;
197 import android.app.compat.CompatChanges;
198 import android.compat.annotation.ChangeId;
199 import android.compat.annotation.EnabledAfter;
200 import android.compat.annotation.EnabledSince;
201 import android.content.BroadcastReceiver;
202 import android.content.ClipData;
203 import android.content.ClipDescription;
204 import android.content.ComponentName;
205 import android.content.ContentProvider;
206 import android.content.ContentProviderClient;
207 import android.content.ContentProviderOperation;
208 import android.content.ContentProviderResult;
209 import android.content.ContentResolver;
210 import android.content.ContentUris;
211 import android.content.ContentValues;
212 import android.content.Context;
213 import android.content.Intent;
214 import android.content.IntentFilter;
215 import android.content.OperationApplicationException;
216 import android.content.SharedPreferences;
217 import android.content.pm.ApplicationInfo;
218 import android.content.pm.PackageInstaller.SessionInfo;
219 import android.content.pm.PackageManager;
220 import android.content.pm.PackageManager.NameNotFoundException;
221 import android.content.pm.PermissionGroupInfo;
222 import android.content.pm.ProviderInfo;
223 import android.content.res.AssetFileDescriptor;
224 import android.content.res.Configuration;
225 import android.content.res.Resources;
226 import android.database.Cursor;
227 import android.database.MatrixCursor;
228 import android.database.sqlite.SQLiteConstraintException;
229 import android.database.sqlite.SQLiteDatabase;
230 import android.graphics.Bitmap;
231 import android.graphics.BitmapFactory;
232 import android.graphics.drawable.Icon;
233 import android.icu.util.ULocale;
234 import android.media.ThumbnailUtils;
235 import android.mtp.MtpConstants;
236 import android.net.Uri;
237 import android.os.Binder;
238 import android.os.Binder.ProxyTransactListener;
239 import android.os.Build;
240 import android.os.Bundle;
241 import android.os.CancellationSignal;
242 import android.os.Environment;
243 import android.os.IBinder;
244 import android.os.ParcelFileDescriptor;
245 import android.os.ParcelFileDescriptor.OnCloseListener;
246 import android.os.Parcelable;
247 import android.os.Process;
248 import android.os.RemoteException;
249 import android.os.SystemClock;
250 import android.os.Trace;
251 import android.os.UserHandle;
252 import android.os.UserManager;
253 import android.os.storage.StorageManager;
254 import android.os.storage.StorageManager.StorageVolumeCallback;
255 import android.os.storage.StorageVolume;
256 import android.preference.PreferenceManager;
257 import android.provider.AsyncContentProvider;
258 import android.provider.BaseColumns;
259 import android.provider.Column;
260 import android.provider.DocumentsContract;
261 import android.provider.ExportedSince;
262 import android.provider.IAsyncContentProvider;
263 import android.provider.MediaStore;
264 import android.provider.MediaStore.Audio;
265 import android.provider.MediaStore.Audio.AudioColumns;
266 import android.provider.MediaStore.Audio.Playlists;
267 import android.provider.MediaStore.Downloads;
268 import android.provider.MediaStore.Files;
269 import android.provider.MediaStore.Files.FileColumns;
270 import android.provider.MediaStore.Images;
271 import android.provider.MediaStore.MediaColumns;
272 import android.provider.MediaStore.Video;
273 import android.provider.OpenAssetFileRequest;
274 import android.provider.OpenFileRequest;
275 import android.provider.Settings;
276 import android.system.ErrnoException;
277 import android.system.Os;
278 import android.system.OsConstants;
279 import android.system.StructStat;
280 import android.text.TextUtils;
281 import android.text.format.DateUtils;
282 import android.util.ArrayMap;
283 import android.util.ArraySet;
284 import android.util.DisplayMetrics;
285 import android.util.Log;
286 import android.util.LongSparseArray;
287 import android.util.Pair;
288 import android.util.Size;
289 import android.util.SparseArray;
290 import android.webkit.MimeTypeMap;
291 
292 import androidx.annotation.ChecksSdkIntAtLeast;
293 import androidx.annotation.GuardedBy;
294 import androidx.annotation.Keep;
295 import androidx.annotation.NonNull;
296 import androidx.annotation.Nullable;
297 import androidx.annotation.RequiresApi;
298 import androidx.annotation.VisibleForTesting;
299 
300 import com.android.modules.utils.BackgroundThread;
301 import com.android.modules.utils.build.SdkLevel;
302 import com.android.providers.media.DatabaseHelper.OnFilesChangeListener;
303 import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener;
304 import com.android.providers.media.backupandrestore.BackupAndRestoreUtils;
305 import com.android.providers.media.backupandrestore.BackupExecutor;
306 import com.android.providers.media.dao.FileRow;
307 import com.android.providers.media.flags.Flags;
308 import com.android.providers.media.fuse.ExternalStorageServiceImpl;
309 import com.android.providers.media.fuse.FuseDaemon;
310 import com.android.providers.media.metrics.PulledMetrics;
311 import com.android.providers.media.photopicker.PhotoPickerActivity;
312 import com.android.providers.media.photopicker.PickerDataLayer;
313 import com.android.providers.media.photopicker.PickerSyncController;
314 import com.android.providers.media.photopicker.data.ExternalDbFacade;
315 import com.android.providers.media.photopicker.data.PickerDbFacade;
316 import com.android.providers.media.photopicker.data.PickerSyncRequestExtras;
317 import com.android.providers.media.photopicker.sync.PickerSyncLockManager;
318 import com.android.providers.media.photopicker.util.exceptions.UnableToAcquireLockException;
319 import com.android.providers.media.photopicker.v2.PickerDataLayerV2;
320 import com.android.providers.media.photopicker.v2.PickerUriResolverV2;
321 import com.android.providers.media.playlist.Playlist;
322 import com.android.providers.media.scan.MediaScanner;
323 import com.android.providers.media.scan.MediaScanner.ScanReason;
324 import com.android.providers.media.scan.ModernMediaScanner;
325 import com.android.providers.media.stableuris.dao.BackupIdRow;
326 import com.android.providers.media.util.CachedSupplier;
327 import com.android.providers.media.util.DatabaseUtils;
328 import com.android.providers.media.util.FileUtils;
329 import com.android.providers.media.util.ForegroundThread;
330 import com.android.providers.media.util.Logging;
331 import com.android.providers.media.util.LongArray;
332 import com.android.providers.media.util.Metrics;
333 import com.android.providers.media.util.MimeTypeFixHandler;
334 import com.android.providers.media.util.MimeUtils;
335 import com.android.providers.media.util.PermissionUtils;
336 import com.android.providers.media.util.Preconditions;
337 import com.android.providers.media.util.RedactionUtils;
338 import com.android.providers.media.util.SQLiteQueryBuilder;
339 import com.android.providers.media.util.SpecialFormatDetector;
340 import com.android.providers.media.util.StringUtils;
341 import com.android.providers.media.util.UserCache;
342 
343 import com.google.common.hash.HashCode;
344 import com.google.common.hash.Hashing;
345 
346 import org.jetbrains.annotations.NotNull;
347 
348 import java.io.File;
349 import java.io.FileDescriptor;
350 import java.io.FileInputStream;
351 import java.io.FileNotFoundException;
352 import java.io.FileOutputStream;
353 import java.io.IOException;
354 import java.io.OutputStream;
355 import java.io.PrintWriter;
356 import java.lang.annotation.Retention;
357 import java.lang.annotation.RetentionPolicy;
358 import java.lang.reflect.InvocationTargetException;
359 import java.lang.reflect.Method;
360 import java.nio.charset.StandardCharsets;
361 import java.nio.file.Path;
362 import java.util.ArrayList;
363 import java.util.Arrays;
364 import java.util.Collection;
365 import java.util.Collections;
366 import java.util.HashSet;
367 import java.util.LinkedHashMap;
368 import java.util.List;
369 import java.util.Locale;
370 import java.util.Map;
371 import java.util.Objects;
372 import java.util.Optional;
373 import java.util.Set;
374 import java.util.UUID;
375 import java.util.concurrent.CountDownLatch;
376 import java.util.concurrent.ExecutionException;
377 import java.util.concurrent.TimeUnit;
378 import java.util.concurrent.TimeoutException;
379 import java.util.function.Consumer;
380 import java.util.function.Supplier;
381 import java.util.function.UnaryOperator;
382 import java.util.regex.Matcher;
383 import java.util.regex.Pattern;
384 import java.util.stream.Collectors;
385 
386 /**
387  * Media content provider. See {@link android.provider.MediaStore} for details.
388  * A single database keep track of media files on external storage
389  * The content visible at content://media/external/... is a combined view of all media files on all
390  * available external storage devices
391  */
392 public class MediaProvider extends ContentProvider {
393     /**
394      * Enables checks to stop apps from inserting and updating to private files via media provider.
395      */
396     @ChangeId
397     @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.R)
398     static final long ENABLE_CHECKS_FOR_PRIVATE_FILES = 172100307L;
399 
400     /**
401      * Regex of a selection string that matches a specific ID.
402      */
403     static final Pattern PATTERN_SELECTION_ID = Pattern.compile(
404             "(?:image_id|video_id)\\s*=\\s*(\\d+)");
405 
406     /** File access by uid requires the transcoding transform */
407     private static final int FLAG_TRANSFORM_TRANSCODING = 1 << 0;
408 
409     /** File access by uid is a synthetic path corresponding to a redacted URI */
410     private static final int FLAG_TRANSFORM_REDACTION = 1 << 1;
411 
412     /** File access by uid is a synthetic path corresponding to a picker URI */
413     private static final int FLAG_TRANSFORM_PICKER = 1 << 2;
414 
415     /**
416      * These directory names aren't declared in Environment as final variables, and so we need to
417      * have the same values in separate final variables in order to have them considered constant
418      * expressions.
419      * These directory names are intentionally in lower case to ease the case insensitive path
420      * comparison.
421      */
422     private static final String DIRECTORY_MUSIC_LOWER_CASE = "music";
423     private static final String DIRECTORY_PODCASTS_LOWER_CASE = "podcasts";
424     private static final String DIRECTORY_RINGTONES_LOWER_CASE = "ringtones";
425     private static final String DIRECTORY_ALARMS_LOWER_CASE = "alarms";
426     private static final String DIRECTORY_NOTIFICATIONS_LOWER_CASE = "notifications";
427     private static final String DIRECTORY_PICTURES_LOWER_CASE = "pictures";
428     private static final String DIRECTORY_MOVIES_LOWER_CASE = "movies";
429     private static final String DIRECTORY_DOWNLOADS_LOWER_CASE = "download";
430     private static final String DIRECTORY_DCIM_LOWER_CASE = "dcim";
431     private static final String DIRECTORY_DOCUMENTS_LOWER_CASE = "documents";
432     private static final String DIRECTORY_AUDIOBOOKS_LOWER_CASE = "audiobooks";
433     private static final String DIRECTORY_RECORDINGS_LOWER_CASE = "recordings";
434     private static final String DIRECTORY_ANDROID_LOWER_CASE = "android";
435 
436     private static final String DIRECTORY_MEDIA = "media";
437     private static final String DIRECTORY_THUMBNAILS = ".thumbnails";
438 
439     /**
440      * Hard-coded filename where the current value of
441      * {@link DatabaseHelper#getOrCreateUuid} is persisted on a physical SD card
442      * to help identify stale thumbnail collections.
443      */
444     private static final String FILE_DATABASE_UUID = ".database_uuid";
445 
446     /**
447      * Specify what default directories the caller gets full access to. By default, the caller
448      * shouldn't get full access to any default dirs.
449      * But for example, we do an exception for System Gallery apps and allow them full access to:
450      * DCIM, Pictures, Movies.
451      */
452     static final String INCLUDED_DEFAULT_DIRECTORIES =
453             "android:included-default-directories";
454 
455     /**
456      * Value indicating that operations should include database rows matching the criteria defined
457      * by this key only when calling package has write permission to the database row or column is
458      * {@column MediaColumns#IS_PENDING} and is set by FUSE.
459      * <p>
460      * Note that items <em>not</em> matching the criteria will also be included, and as part of this
461      * match no additional write permission checks are carried out for those items.
462      */
463     private static final int MATCH_VISIBLE_FOR_FILEPATH = 32;
464 
465     private static final int NON_HIDDEN_CACHE_SIZE = 50;
466 
467     /**
468      * This is required as idle maintenance maybe stopped anytime; we do not want to query
469      * and accumulate values to update for a long time, instead we want to batch query and update
470      * by a limited number.
471      */
472     private static final int IDLE_MAINTENANCE_ROWS_LIMIT = 1000;
473 
474     /**
475      * Where clause to match pending files from FUSE. Pending files from FUSE will not have
476      * PATTERN_PENDING_FILEPATH_FOR_SQL pattern.
477      */
478     private static final String MATCH_PENDING_FROM_FUSE = String.format("lower(%s) NOT REGEXP '%s'",
479             MediaColumns.DATA, PATTERN_PENDING_FILEPATH_FOR_SQL);
480 
481     /**
482      * This flag is replaced with {@link MediaStore#QUERY_ARG_DEFER_SCAN} from S onwards and only
483      * kept around for app compatibility in R.
484      */
485     private static final String QUERY_ARG_DO_ASYNC_SCAN = "android:query-arg-do-async-scan";
486 
487     /**
488      * Time between two polling attempts for availability of FuseDaemon thread.
489      */
490     private static final long POLLING_TIME_IN_MILLIS = 100;
491 
492     /**
493      * Enable option to defer the scan triggered as part of MediaProvider#update()
494      */
495     @ChangeId
496     @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.R)
497     static final long ENABLE_DEFERRED_SCAN = 180326732L;
498 
499     /**
500      * Enable option to include database rows of files from recently unmounted
501      * volume in MediaProvider#query
502      */
503     @ChangeId
504     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.R)
505     static final long ENABLE_INCLUDE_ALL_VOLUMES = 182734110L;
506 
507     /**
508      * Enables allowing the user to revoke the app access to its created photos and videos.
509      * If the app target sdk is >= {@link android.os.Build.VERSION_CODES#BAKLAVA},
510      * then they should expect that they may lose access to photos or videos they have created
511      * while they could still be on the device.
512      */
513     @ChangeId
514     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
515     public static final long ENABLE_OWNED_PHOTOS = 310703690L;
516 
517 
518     /**
519      * Excludes unreliable storage volumes from being included in
520      * {@link MediaStore#getExternalVolumeNames(Context)}.
521      */
522     @ChangeId
523     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT)
524     @VisibleForTesting
525     // TODO: b/402623169 Set CUR_DEVELOPMENT as the latest version once available
526     static final long EXCLUDE_UNRELIABLE_STORAGE_VOLUMES = 391360514L;
527 
528     /**
529      * Set of {@link Cursor} columns that refer to raw filesystem paths.
530      */
531     private static final ArrayMap<String, Object> sDataColumns = new ArrayMap<>();
532 
533     static {
sDataColumns.put(MediaStore.MediaColumns.DATA, null)534         sDataColumns.put(MediaStore.MediaColumns.DATA, null);
sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null)535         sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null);
sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null)536         sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null);
sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null)537         sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null);
sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null)538         sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null);
539     }
540 
541     private static final int sUserId = UserHandle.myUserId();
542 
543     /**
544      * Please use {@link getDownloadsProviderAuthority()} instead of using this directly.
545      */
546     private static final String DOWNLOADS_PROVIDER_AUTHORITY = "downloads";
547 
548     private static final String DEFAULT_FOLDER_CREATED_KEY_PREFIX = "created_default_folders_";
549 
550     /**
551      * This value should match android.os.Trace.MAX_SECTION_NAME_LEN , not accessible from this
552      * class
553      */
554     private static final int MAX_SECTION_NAME_LEN = 127;
555 
556     /**
557      * This string is a copy of
558      * {@link com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY}
559      */
560     private static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
561 
562     private static final String MEDIAPROVIDER_PREFS = "mediaprovider_prefs";
563 
564     private static final String IS_MIME_TYPE_FIXED_IN_ANDROID_15 =
565             "is_mime_type_fixed_in_android_15";
566 
567     /**
568      * Updates the MediaStore versioning schema and format to reduce identifying properties.
569      */
570     @ChangeId
571     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA)
572     static final long LOCKDOWN_MEDIASTORE_VERSION = 343977174L;
573 
574     /**
575      * Number of uris sent to bulk write/delete/trash/favorite requests restricted at 2000.
576      * Attempting to send more than 2000 uris will result in an IllegalArgumentException.
577      */
578     @ChangeId
579     @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
580     static final long LIMIT_CREATE_REQUEST_URIS = 203408344L;
581 
582     @GuardedBy("mPendingOpenInfo")
583     private final Map<Integer, PendingOpenInfo> mPendingOpenInfo = new ArrayMap<>();
584 
585     @GuardedBy("mNonHiddenPaths")
586     private final LRUCache<String, Integer> mNonHiddenPaths = new LRUCache<>(NON_HIDDEN_CACHE_SIZE);
587 
updateVolumes()588     public void updateVolumes() {
589         mVolumeCache.update();
590         // Update filters to reflect mounted volumes so users don't get
591         // confused by metadata from ejected volumes
592         ForegroundThread.getExecutor().execute(() -> {
593             mExternalDatabase.setFilterVolumeNames(mVolumeCache.getExternalVolumeNames());
594         });
595     }
596 
597     @NonNull
getVolume(@onNull String volumeName)598     public MediaVolume getVolume(@NonNull String volumeName) throws FileNotFoundException {
599         return mVolumeCache.findVolume(volumeName, mCallingIdentity.get().getUser());
600     }
601 
602     @NonNull
getVolumePath(@onNull String volumeName)603     public File getVolumePath(@NonNull String volumeName) throws FileNotFoundException {
604         // Ugly hack to keep unit tests passing, where we don't always have a
605         // Context to discover volumes with
606         if (getContext() == null) {
607             return Environment.getExternalStorageDirectory();
608         }
609 
610         return mVolumeCache.getVolumePath(volumeName, mCallingIdentity.get().getUser());
611     }
612 
613     @NonNull
getAllowedVolumePaths(String volumeName)614     private Collection<File> getAllowedVolumePaths(String volumeName)
615             throws FileNotFoundException {
616         // This method is used to verify whether a path belongs to a certain volume name;
617         // we can't always use the calling user's identity here to determine exactly which
618         // volume is meant, because the MediaScanner may scan paths belonging to another user,
619         // eg a clone user.
620         // So, for volumes like external_primary, just return allowed paths for all users.
621         List<UserHandle> users = mUserCache.getUsersCached();
622         ArrayList<File> allowedPaths = new ArrayList<>();
623         for (UserHandle user : users) {
624             try {
625                 Collection<File> volumeScanPaths = mVolumeCache.getVolumeScanPaths(volumeName,
626                         user);
627                 allowedPaths.addAll(volumeScanPaths);
628             } catch (FileNotFoundException e) {
629                 Log.e(TAG, volumeName + " has no associated path for user: " + user);
630             }
631         }
632 
633         return allowedPaths;
634     }
635 
636     /**
637      * Frees any cache held by MediaProvider.
638      *
639      * @param bytes number of bytes which need to be freed
640      */
freeCache(long bytes)641     public void freeCache(long bytes) {
642         bytes -= mPhotoPickerTranscodeHelper.freeCache(bytes);
643         if (bytes > 0) {
644             mTranscodeHelper.freeCache(bytes);
645         }
646     }
647 
onAnrDelayStarted(@onNull String packageName, int uid, int tid, int reason)648     public void onAnrDelayStarted(@NonNull String packageName, int uid, int tid, int reason) {
649         mTranscodeHelper.onAnrDelayStarted(packageName, uid, tid, reason);
650     }
651 
652     private volatile Locale mLastLocale = Locale.getDefault();
653 
654     private StorageManager mStorageManager;
655     private PackageManager mPackageManager;
656     private UserManager mUserManager;
657     private PickerUriResolver mPickerUriResolver;
658     private AsyncPickerFileOpener mAsyncPickerFileOpener;
659 
660     private UserCache mUserCache;
661     private VolumeCache mVolumeCache;
662 
663     private int mExternalStorageAuthorityAppId;
664     private int mDownloadsAuthorityAppId;
665     private Size mThumbSize;
666     private MaliciousAppDetector mMaliciousAppDetector;
667 
668     /**
669      * Map from UID to cached {@link LocalCallingIdentity}. Values are only
670      * maintained in this map while the UID is actively working with a
671      * performance-critical component, such as camera.
672      */
673     @GuardedBy("mCachedCallingIdentity")
674     private final SparseArray<LocalCallingIdentity> mCachedCallingIdentity = new SparseArray<>();
675 
676     private final OnOpActiveChangedListener mActiveListener = (code, uid, packageName, active) -> {
677         synchronized (mCachedCallingIdentity) {
678             if (active) {
679                 // TODO moltmann: Set correct featureId
680                 mCachedCallingIdentity.put(uid,
681                         LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid,
682                             packageName, null));
683             } else {
684                 mCachedCallingIdentity.remove(uid);
685             }
686         }
687     };
688 
689     /**
690      * Utility function if owned photos features is enabled.
691      * @return boolean value indicating whether feature is enabled or not
692      */
isOwnedPhotosEnabled(int uid)693     public static boolean isOwnedPhotosEnabled(int uid) {
694         // TODO change this to SdkLevel.isAtLeastB() once method is available
695         return ((Build.VERSION.CODENAME.equals("Baklava")
696                 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA)
697                 && CompatChanges.isChangeEnabled(ENABLE_OWNED_PHOTOS, uid)
698                 && Flags.revokeAccessOwnedPhotos());
699     }
700 
701     /**
702      * Map from UID to cached {@link LocalCallingIdentity}. Values are only
703      * maintained in this map until there's any change in the appops needed or packages
704      * used in the {@link LocalCallingIdentity}.
705      */
706     @GuardedBy("mCachedCallingIdentityForFuse")
707     private final SparseArray<LocalCallingIdentity> mCachedCallingIdentityForFuse =
708             new SparseArray<>();
709 
710     private final OnOpChangedListener mModeListener = new OnOpChangedListener() {
711 
712         /**
713          * Callback method called as part of {@link OnOpChangedListener}.
714          * Calls {@link #onOpChanged(String, String, int)} with cached userId(s).
715          *
716          * @param packageName - package for which AppOp changed
717          * @param op - AppOp for which the mode changed.
718          */
719         public void onOpChanged(String op, String packageName) {
720             // In case no userId is supplied, we drop grants for all cached users.
721             List<UserHandle> userHandles = mUserCache.getUsersCached();
722             for (UserHandle user : userHandles) {
723                 onOpChanged(op, packageName, user.getIdentifier());
724             }
725         }
726 
727         /**
728          * Callback method called as part of {@link OnOpChangedListener}.
729          * When an AppOp is written -
730          * 1. We invalidate saved LocalCallingIdentity object for the package. This
731          *    is needed to ensure we read the new permission state
732          * 2. If the AppOp change was on the read media appOps, we clear any stale
733          *    grants,
734          *
735          * @param packageName - package for which AppOp changed
736          * @param op - AppOp for which the mode changed.
737          * @param userId - userSpace where the package is located
738          */
739         public void onOpChanged(String op, String packageName, int userId) {
740             invalidateLocalCallingIdentityCache(packageName, "op " + op /* reason */);
741             removeMediaGrantsOnModeChange(packageName, op, userId);
742         }
743     };
744 
745     /**
746      * Removes media_grants for the given {@code packageName} and {@code userId} if the AppOp
747      * change resulted in a state of "Allow All" or "Deny All" for read
748      * permission.
749      */
removeMediaGrantsOnModeChange(String packageName, String op, int userId)750     private void removeMediaGrantsOnModeChange(String packageName, String op, int userId) {
751         // b/265963379: onModeChanged is always called with op=OPSTR_READ_EXTERNAL_STORAGE even if
752         // the appOp mode changed for other read media app ops. Handle all read media app op changes
753         // until the bug is fixed.
754         if (!SdkLevel.isAtLeastU() || !isReadMediaAppOp(op)) {
755             return;
756         }
757         Context context = getContext();
758         PackageManager packageManager = context.getPackageManager();
759         try {
760             int uid =
761                     packageManager.getPackageUidAsUser(
762                             packageName, PackageManager.PackageInfoFlags.of(0), userId);
763             LocalCallingIdentity lci = LocalCallingIdentity.fromExternal(context, mUserCache, uid);
764             if (!lci.checkCallingPermissionUserSelected(/* forDataDelivery */ false)) {
765                 String[] packages = lci.getSharedPackageNamesArray();
766                 mMediaGrants.removeAllMediaGrantsForPackages(
767                         packages, /* reason= */ "Mode changed: " + op, userId);
768             }
769         } catch (NameNotFoundException e) {
770             Log.d(
771                     TAG,
772                     "Unable to resolve uid. Ignoring the AppOp change for "
773                             + packageName
774                             + ", User : "
775                             + userId);
776         }
777     }
778 
779     /**
780      * Returns {@code true} if the given {@code op} is one of the appOp
781      * related to read media appOps
782      */
isReadMediaAppOp(String op)783     private boolean isReadMediaAppOp(String op) {
784         return AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE.equals(op)
785                 || AppOpsManager.OPSTR_READ_MEDIA_IMAGES.equals(op)
786                 || AppOpsManager.OPSTR_READ_MEDIA_VIDEO.equals(op)
787                 || AppOpsManager.OPSTR_READ_MEDIA_VISUAL_USER_SELECTED.equals(op);
788     }
789 
790     /**
791      * Retrieves a cached calling identity or creates a new one. Also, always sets the app-op
792      * description for the calling identity.
793      */
getCachedCallingIdentityForFuse(int uid)794     private LocalCallingIdentity getCachedCallingIdentityForFuse(int uid) {
795         synchronized (mCachedCallingIdentityForFuse) {
796             PermissionUtils.setOpDescription("via FUSE");
797             LocalCallingIdentity identity = mCachedCallingIdentityForFuse.get(uid);
798             if (identity == null) {
799                identity = LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid);
800                if (uidToUserId(uid) == sUserId) {
801                    mCachedCallingIdentityForFuse.put(uid, identity);
802                } else {
803                    // In some app cloning designs, MediaProvider user 0 may
804                    // serve requests for apps running as a "clone" user; in
805                    // those cases, don't keep a cache for the clone user, since
806                    // we don't get any invalidation events for these users.
807                }
808             }
809             return identity;
810         }
811     }
812 
813     /**
814      * Calling identity state about on the current thread. Populated on demand,
815      * and invalidated by {@link #onCallingPackageChanged()} when each remote
816      * call is finished.
817      */
818     private final ThreadLocal<LocalCallingIdentity> mCallingIdentity = ThreadLocal
819             .withInitial(() -> {
820                 PermissionUtils.setOpDescription("via MediaProvider");
821                 synchronized (mCachedCallingIdentity) {
822                     final LocalCallingIdentity cached = mCachedCallingIdentity
823                             .get(Binder.getCallingUid());
824                     return (cached != null) ? cached
825                             : LocalCallingIdentity.fromBinder(getContext(), this, mUserCache);
826                 }
827             });
828 
829     /**
830      * We simply propagate the UID that is being tracked by
831      * {@link LocalCallingIdentity}, which means we accurately blame both
832      * incoming Binder calls and FUSE calls.
833      */
834     private final ProxyTransactListener mTransactListener = new ProxyTransactListener() {
835         @Override
836         public Object onTransactStarted(IBinder binder, int transactionCode) {
837             if (LOGV) Trace.beginSection(Thread.currentThread().getStackTrace()[5].getMethodName());
838             // Check if mCallindIdentity was created within a fuse or content provider transaction
839             if (mCallingIdentity.get().isValidProviderOrFuseCallingIdentity()) {
840                 return Binder.setCallingWorkSourceUid(mCallingIdentity.get().uid);
841             }
842             // If mCallingIdentity was not created for a fuse or content provider transaction,
843             // we should reset it, the next time it is retrieved it will be created for the
844             // appropriate caller.
845             mCallingIdentity.remove();
846             return Binder.setCallingWorkSourceUid(Binder.getCallingUid());
847         }
848 
849         @Override
850         public void onTransactEnded(Object session) {
851             final long token = (long) session;
852             Binder.restoreCallingWorkSource(token);
853             if (LOGV) Trace.endSection();
854         }
855     };
856 
857     // In memory cache of path<->id mappings, to speed up inserts during media scan
858     @GuardedBy("mDirectoryCache")
859     private final ArrayMap<String, Long> mDirectoryCache = new ArrayMap<>();
860 
861     private static final String[] sDataOnlyColumn = new String[] {
862         FileColumns.DATA
863     };
864 
865     private static final String ID_NOT_PARENT_CLAUSE =
866             "_id NOT IN (SELECT parent FROM files WHERE parent IS NOT NULL)";
867 
868     private static final String CANONICAL = "canonical";
869 
870     private static final String ALL_VOLUMES = "all_volumes";
871 
872     private final BroadcastReceiver mPackageReceiver = new BroadcastReceiver() {
873         @Override
874         public void onReceive(Context context, Intent intent) {
875             switch (intent.getAction()) {
876                 case Intent.ACTION_PACKAGE_REMOVED:
877                 case Intent.ACTION_PACKAGE_CHANGED:
878                 case Intent.ACTION_PACKAGE_ADDED:
879                     Uri uri = intent.getData();
880                     String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
881                     int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
882                     if (pkg != null) {
883                         invalidateLocalCallingIdentityCache(uid, "package " + intent.getAction());
884                         if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) {
885                             mUserCache.invalidateWorkProfileOwnerApps(pkg);
886                             mPickerSyncController.notifyPackageRemoval(pkg);
887                             invalidateDentryForExternalStorage(pkg);
888                         } else if (Intent.ACTION_PACKAGE_CHANGED.equals(intent.getAction())) {
889                             try {
890                                 // If package has been modified e.g. has been enabled or disabled,
891                                 // it should be checked against current set of providers.
892                                 // Hence if a modified package is disable, attempt to remove it from
893                                 // pickerSyncController.
894                                 if (!getContext().getPackageManager().getApplicationInfo(pkg,
895                                         /* flags */ 0).enabled) {
896                                     Log.d(TAG, "Removing disabled package: " + pkg
897                                             + " from providers list if required.");
898                                     mPickerSyncController.notifyPackageRemoval(pkg);
899                                 }
900                             } catch (NameNotFoundException ignored) {
901                                 // no-op
902                             }
903                         }
904                     } else {
905                         Log.w(TAG, "Failed to retrieve package from intent: " + intent.getAction());
906                     }
907                     break;
908             }
909         }
910     };
911 
invalidateDentryForExternalStorage(String packageName)912     private void invalidateDentryForExternalStorage(String packageName) {
913         for (MediaVolume vol : mVolumeCache.getExternalVolumes()) {
914             try {
915                 invalidateFuseDentry(String.format(Locale.ROOT,
916                         "%s/Android/media/%s/", getVolumePath(vol.getName()).getAbsolutePath(),
917                         packageName));
918             } catch (FileNotFoundException e) {
919                 Log.e(TAG, "External volume path not found for " + vol.getName(), e);
920             }
921         }
922     }
923 
924     private final BroadcastReceiver mUserIntentReceiver = new BroadcastReceiver() {
925         @Override
926         public void onReceive(Context context, Intent intent) {
927             switch (intent.getAction()) {
928                 case Intent.ACTION_USER_REMOVED:
929                     /**
930                      * Removing media files for user being deleted. This would impact if the deleted
931                      * user have been using same MediaProvider as the current user i.e. when
932                      * isMediaSharedWithParent is true.On removal of such user profile,
933                      * the owner's MediaProvider would need to clean any media files stored
934                      * by the removed user profile.
935                      * We also remove the default folder key for the cloned user (just removed)
936                      * from user 0's SharedPreferences. Usually, the next clone user would be
937                      * created with a different key (as user-id would be incremented), however, if
938                      * device is restarted, the next clone-user can use the user-id previously
939                      * assigned, causing stale entries in user 0's SharedPreferences
940                      */
941                     UserHandle userToBeRemoved  = intent.getParcelableExtra(Intent.EXTRA_USER);
942                     if(userToBeRemoved.getIdentifier() != sUserId){
943                         mExternalDatabase.runWithTransaction((db) -> {
944                             db.execSQL("delete from files where _user_id=?",
945                                     new String[]{String.valueOf(userToBeRemoved.getIdentifier())});
946                             return null ;
947                         });
948                         String userToBeRemovedVolId = null;
949                         synchronized (mAttachedVolumes) {
950                           for (MediaVolume volume : mAttachedVolumes) {
951                               if (userToBeRemoved.equals(volume.getUser())) {
952                                   userToBeRemovedVolId = volume.getId();
953                                   break;
954                               }
955                           }
956                         }
957                         //The clone user volume may be unmounted at this time (userToBeRemovedVolId
958                         // will be null then), we construct the volId of unmounted vol from userId.
959                         String key = DEFAULT_FOLDER_CREATED_KEY_PREFIX
960                                 + getPrimaryVolumeId(userToBeRemovedVolId, userToBeRemoved);
961                         final SharedPreferences prefs = PreferenceManager
962                                 .getDefaultSharedPreferences(getContext());
963                         if (prefs.getInt(key, /* default */ 0) == 1) {
964                             SharedPreferences.Editor editor = prefs.edit();
965                             editor.remove(key);
966                             editor.commit();
967                         }
968                     }
969 
970                     boolean isDeviceInDemoMode = false;
971                     try {
972                         isDeviceInDemoMode = Settings.Global.getInt(
973                                 getContext().getContentResolver(), Settings.Global.DEVICE_DEMO_MODE)
974                                 > 0;
975                     } catch (Settings.SettingNotFoundException e) {
976                         Log.w(TAG, "Exception in reading DEVICE_DEMO_MODE setting", e);
977                     }
978 
979                     Log.i(TAG, "isDeviceInDemoMode: " + isDeviceInDemoMode);
980                     // Only allow default system user 0 to update xattrs on /data/media/0 and
981                     // only on retail demo devices
982                     if (sUserId == UserHandle.SYSTEM.getIdentifier() && isDeviceInDemoMode) {
983                         mDatabaseBackupAndRecovery.removeRecoveryDataForUserId(
984                                 userToBeRemoved.getIdentifier());
985                     }
986                     break;
987             }
988         }
989     };
990 
invalidateLocalCallingIdentityCache(String packageName, String reason)991     private void invalidateLocalCallingIdentityCache(String packageName, String reason) {
992         try {
993             int packageUid = getContext().getPackageManager().getPackageUid(packageName, 0);
994             invalidateLocalCallingIdentityCache(packageUid, reason);
995         } catch (NameNotFoundException e) {
996             Log.d(TAG, "Couldn't get uid for package: " + packageName);
997         }
998     }
999 
invalidateLocalCallingIdentityCache(int packageUid, String reason)1000     private void invalidateLocalCallingIdentityCache(int packageUid, String reason) {
1001         synchronized (mCachedCallingIdentityForFuse) {
1002             if (mCachedCallingIdentityForFuse.contains(packageUid)) {
1003                 mCachedCallingIdentityForFuse.get(packageUid).dump(reason);
1004                 mCachedCallingIdentityForFuse.remove(packageUid);
1005             }
1006         }
1007     }
1008 
updateQuotaTypeForUri(@onNull FileRow row)1009     protected void updateQuotaTypeForUri(@NonNull FileRow row) {
1010         final String volumeName = row.getVolumeName();
1011         final String path = row.getPath();
1012 
1013         // Quota type is only updated for external primary volume
1014         if (!MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volumeName)) {
1015             return;
1016         }
1017 
1018         int mediaType = row.getMediaType();
1019         Trace.beginSection("MP.updateQuotaTypeForUri");
1020         File file;
1021         try {
1022             if (path != null) {
1023                 file = new File(path);
1024             } else {
1025                 // This can happen in case of renames, where the path isn't
1026                 // part of the 'new' FileRow data. Fall back to querying
1027                 // the path directly.
1028                 final Uri uri = MediaStore.Files.getContentUri(row.getVolumeName(),
1029                         row.getId());
1030                 if (uri == null) {
1031                     // Row could have been deleted
1032                     return;
1033                 }
1034                 file = queryForDataFile(uri, null);
1035             }
1036             if (!file.exists()) {
1037                 // This can happen if an item is inserted in MediaStore before it is created
1038                 return;
1039             }
1040 
1041             if (mediaType == FileColumns.MEDIA_TYPE_NONE) {
1042                 // This might be because the file is hidden; but we still want to
1043                 // attribute its quota to the correct type, so get the type from
1044                 // the extension instead.
1045                 mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
1046             }
1047 
1048             updateQuotaTypeForFileInternal(file, mediaType);
1049         } catch (FileNotFoundException | IllegalArgumentException e) {
1050             // Ignore
1051             Log.w(TAG, "Failed to update quota", e);
1052         } finally {
1053             Trace.endSection();
1054         }
1055     }
1056 
updateQuotaTypeForFileInternal(File file, int mediaType)1057     private void updateQuotaTypeForFileInternal(File file, int mediaType) {
1058         try {
1059             switch (mediaType) {
1060                 case FileColumns.MEDIA_TYPE_AUDIO:
1061                     mStorageManager.updateExternalStorageFileQuotaType(file,
1062                             StorageManager.QUOTA_TYPE_MEDIA_AUDIO);
1063                     break;
1064                 case FileColumns.MEDIA_TYPE_VIDEO:
1065                     mStorageManager.updateExternalStorageFileQuotaType(file,
1066                             StorageManager.QUOTA_TYPE_MEDIA_VIDEO);
1067                     break;
1068                 case FileColumns.MEDIA_TYPE_IMAGE:
1069                     mStorageManager.updateExternalStorageFileQuotaType(file,
1070                             StorageManager.QUOTA_TYPE_MEDIA_IMAGE);
1071                     break;
1072                 default:
1073                     mStorageManager.updateExternalStorageFileQuotaType(file,
1074                             StorageManager.QUOTA_TYPE_MEDIA_NONE);
1075                     break;
1076             }
1077         } catch (IOException e) {
1078             Log.w(TAG, "Failed to update quota type for " + file.getPath(), e);
1079         }
1080     }
1081 
1082     /**
1083      * Since these operations are in the critical path of apps working with
1084      * media, we only collect the {@link Uri} that need to be notified, and all
1085      * other side-effect operations are delegated to {@link BackgroundThread} so
1086      * that we return as quickly as possible.
1087      */
1088     private final OnFilesChangeListener mFilesListener = new OnFilesChangeListener() {
1089         @Override
1090         public void onInsert(@NonNull DatabaseHelper helper, @NonNull FileRow insertedRow) {
1091             if (helper.isDatabaseRecovering()) {
1092                 // Do not perform any trigger operation if database is recovering
1093                 return;
1094             }
1095 
1096             handleInsertedRowForFuse(insertedRow.getId());
1097             acceptWithExpansion(helper::notifyInsert, insertedRow.getVolumeName(),
1098                     insertedRow.getId(), insertedRow.getMediaType(), insertedRow.isDownload());
1099 
1100             mDatabaseBackupAndRecovery.updateNextRowIdXattr(helper, insertedRow.getId());
1101 
1102             helper.postBackground(() -> {
1103                 if (helper.isExternal() && !isFuseThread()) {
1104                     // Update the quota type on the filesystem
1105                     Uri fileUri = MediaStore.Files.getContentUri(insertedRow.getVolumeName(),
1106                             insertedRow.getId());
1107                     updateQuotaTypeForUri(insertedRow);
1108                 }
1109 
1110                 // Tell our SAF provider so it knows when views are no longer empty
1111                 MediaDocumentsProvider.onMediaStoreInsert(getContext(), insertedRow.getVolumeName(),
1112                         insertedRow.getMediaType(), insertedRow.getId());
1113 
1114                 if (mExternalDbFacade.onFileInserted(insertedRow.getMediaType(),
1115                         insertedRow.isPending())) {
1116                     mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true,
1117                             PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, null);
1118                 }
1119 
1120                 mDatabaseBackupAndRecovery.backupVolumeDbData(helper, insertedRow);
1121 
1122 
1123                 // check for potentially malicious file creation activity
1124                 // to prevent excessive file creation that could exhaust system inodes,
1125                 // this check periodically monitors the number of files created by an app.
1126                 // if an app exceeds a defined threshold, it is flagged as potentially malicious
1127                 if (shouldCheckForMaliciousActivity()
1128                         && insertedRow.getVolumeName().equals(MediaStore.VOLUME_EXTERNAL_PRIMARY)
1129                         && insertedRow.getId()
1130                         % mMaliciousAppDetector.getFrequencyOfMaliciousInsertionCheck()
1131                         == 0) {
1132                     mMaliciousAppDetector.detectFileCreationByMaliciousApp(getContext(), helper,
1133                             insertedRow.getOwnerPackageName());
1134                 }
1135             });
1136         }
1137 
1138         @Override
1139         public void onUpdate(@NonNull DatabaseHelper helper, @NonNull FileRow oldRow,
1140                 @NonNull FileRow newRow) {
1141             if (helper.isDatabaseRecovering()) {
1142                 // Do not perform any trigger operation if database is recovering
1143                 return;
1144             }
1145 
1146             final boolean isDownload = oldRow.isDownload() || newRow.isDownload();
1147             final Uri fileUri = MediaStore.Files.getContentUri(oldRow.getVolumeName(),
1148                     oldRow.getId());
1149             handleUpdatedRowForFuse(oldRow.getPath(), oldRow.getOwnerPackageName(), oldRow.getId(),
1150                     newRow.getId());
1151             handleOwnerPackageNameChange(oldRow.getPath(), oldRow.getOwnerPackageName(),
1152                     newRow.getOwnerPackageName());
1153             acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(),
1154                     oldRow.getMediaType(), isDownload);
1155 
1156             mDatabaseBackupAndRecovery.updateNextRowIdAndSetDirty(helper, oldRow, newRow);
1157 
1158             helper.postBackground(() -> {
1159                 if (helper.isExternal()) {
1160                     // Update the quota type on the filesystem
1161                     updateQuotaTypeForUri(newRow);
1162                 }
1163 
1164                 if (mExternalDbFacade.onFileUpdated(oldRow.getId(),
1165                         oldRow.getMediaType(), newRow.getMediaType(),
1166                         oldRow.isTrashed(), newRow.isTrashed(),
1167                         oldRow.isPending(), newRow.isPending(),
1168                         oldRow.isFavorite(), newRow.isFavorite(),
1169                         oldRow.getSpecialFormat(), newRow.getSpecialFormat())) {
1170                     mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true,
1171                             PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, null);
1172                 }
1173 
1174                 mDatabaseBackupAndRecovery.updateBackup(helper, oldRow, newRow);
1175             });
1176 
1177             if (newRow.getMediaType() != oldRow.getMediaType()) {
1178                 acceptWithExpansion(helper::notifyUpdate, oldRow.getVolumeName(), oldRow.getId(),
1179                         newRow.getMediaType(), isDownload);
1180 
1181                 helper.postBackground(() -> {
1182                     // Invalidate any thumbnails when the media type changes
1183                     invalidateThumbnails(fileUri);
1184                 });
1185             }
1186         }
1187 
1188         @Override
1189         public void onDelete(@NonNull DatabaseHelper helper, @NonNull FileRow deletedRow) {
1190             if (helper.isDatabaseRecovering()) {
1191                 // Do not perform any trigger operation if database is recovering
1192                 return;
1193             }
1194 
1195             handleDeletedRowForFuse(deletedRow.getPath(), deletedRow.getOwnerPackageName(),
1196                     deletedRow.getId());
1197             acceptWithExpansion(helper::notifyDelete, deletedRow.getVolumeName(),
1198                     deletedRow.getId(), deletedRow.getMediaType(), deletedRow.isDownload());
1199 
1200             // Remove cached transcoded file if any
1201             mTranscodeHelper.deleteCachedTranscodeFile(deletedRow.getId());
1202             mPhotoPickerTranscodeHelper.deleteCachedTranscodedFile(
1203                     PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, deletedRow.getId());
1204 
1205             helper.postBackground(() -> {
1206                 // Item no longer exists, so revoke all access to it
1207                 Trace.beginSection("MP.revokeUriPermission");
1208                 try {
1209                     acceptWithExpansion((uri) -> getContext().revokeUriPermission(uri, ~0),
1210                             deletedRow.getVolumeName(), deletedRow.getId(),
1211                             deletedRow.getMediaType(), deletedRow.isDownload());
1212                 } finally {
1213                     Trace.endSection();
1214                 }
1215 
1216                 switch (deletedRow.getMediaType()) {
1217                     case FileColumns.MEDIA_TYPE_PLAYLIST:
1218                     case FileColumns.MEDIA_TYPE_AUDIO:
1219                         if (helper.isExternal()) {
1220                             removePlaylistMembers(deletedRow.getMediaType(), deletedRow.getId());
1221                         }
1222                 }
1223 
1224                 // Invalidate any thumbnails now that media is gone
1225                 invalidateThumbnails(MediaStore.Files.getContentUri(deletedRow.getVolumeName(),
1226                         deletedRow.getId()));
1227 
1228                 // Tell our SAF provider so it can revoke too
1229                 MediaDocumentsProvider.onMediaStoreDelete(getContext(), deletedRow.getVolumeName(),
1230                         deletedRow.getMediaType(), deletedRow.getId());
1231 
1232                 if (mExternalDbFacade.onFileDeleted(deletedRow.getId(),
1233                         deletedRow.getMediaType())) {
1234                     mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ true,
1235                             PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY, null);
1236                 }
1237 
1238                 mDatabaseBackupAndRecovery.deleteFromDbBackup(helper, deletedRow);
1239                 if (deletedRow.getVolumeName() != null
1240                         && deletedRow.getVolumeName().equalsIgnoreCase(VOLUME_EXTERNAL_PRIMARY)) {
1241                     mExternalPrimaryBackupExecutor.deleteBackupForPath(deletedRow.getPath());
1242                 }
1243             });
1244         }
1245     };
1246 
1247     private final UnaryOperator<String> mIdGenerator = path -> {
1248         final long rowId = mCallingIdentity.get().getDeletedRowId(path);
1249         if (rowId != -1 && isFuseThread()) {
1250             return String.valueOf(rowId);
1251         }
1252         return null;
1253     };
1254 
1255     /** {@hide} */
1256     public static final OnLegacyMigrationListener MIGRATION_LISTENER =
1257             new OnLegacyMigrationListener() {
1258         @Override
1259         public void onStarted(ContentProviderClient client, String volumeName) {
1260             MediaStore.startLegacyMigration(ContentResolver.wrap(client), volumeName);
1261         }
1262 
1263         @Override
1264         public void onProgress(ContentProviderClient client, String volumeName,
1265                 long progress, long total) {
1266             // TODO: notify blocked threads of progress once we can change APIs
1267         }
1268 
1269         @Override
1270         public void onFinished(ContentProviderClient client, String volumeName) {
1271             MediaStore.finishLegacyMigration(ContentResolver.wrap(client), volumeName);
1272         }
1273     };
1274 
1275     /**
1276      * Apply {@link Consumer#accept} to the given item.
1277      * <p>
1278      * Since media items can be exposed through multiple collections or views,
1279      * this method expands the single item being accepted to also accept all
1280      * relevant views.
1281      */
acceptWithExpansion(@onNull Consumer<Uri> consumer, @NonNull String volumeName, long id, int mediaType, boolean isDownload)1282     private void acceptWithExpansion(@NonNull Consumer<Uri> consumer, @NonNull String volumeName,
1283             long id, int mediaType, boolean isDownload) {
1284         switch (mediaType) {
1285             case FileColumns.MEDIA_TYPE_AUDIO:
1286                 consumer.accept(MediaStore.Audio.Media.getContentUri(volumeName, id));
1287 
1288                 // Any changing audio items mean we probably need to invalidate all
1289                 // indexed views built from that media
1290                 consumer.accept(Audio.Genres.getContentUri(volumeName));
1291                 consumer.accept(Audio.Playlists.getContentUri(volumeName));
1292                 consumer.accept(Audio.Artists.getContentUri(volumeName));
1293                 consumer.accept(Audio.Albums.getContentUri(volumeName));
1294                 break;
1295 
1296             case FileColumns.MEDIA_TYPE_VIDEO:
1297                 consumer.accept(MediaStore.Video.Media.getContentUri(volumeName, id));
1298                 break;
1299 
1300             case FileColumns.MEDIA_TYPE_IMAGE:
1301                 consumer.accept(MediaStore.Images.Media.getContentUri(volumeName, id));
1302                 break;
1303 
1304             case FileColumns.MEDIA_TYPE_PLAYLIST:
1305                 consumer.accept(ContentUris.withAppendedId(
1306                         MediaStore.Audio.Playlists.getContentUri(volumeName), id));
1307                 break;
1308         }
1309 
1310         // Also notify through any generic views
1311         consumer.accept(MediaStore.Files.getContentUri(volumeName, id));
1312         if (isDownload) {
1313             consumer.accept(MediaStore.Downloads.getContentUri(volumeName, id));
1314         }
1315 
1316         // Rinse and repeat through any synthetic views
1317         switch (volumeName) {
1318             case MediaStore.VOLUME_INTERNAL:
1319             case MediaStore.VOLUME_EXTERNAL:
1320                 // Already a top-level view, no need to expand
1321                 break;
1322             default:
1323                 acceptWithExpansion(consumer, MediaStore.VOLUME_EXTERNAL,
1324                         id, mediaType, isDownload);
1325                 break;
1326         }
1327     }
1328 
1329     @VisibleForTesting
getDefaultFolderNames()1330     protected String[] getDefaultFolderNames() {
1331         return DEFAULT_FOLDER_NAMES;
1332     }
1333 
1334     @VisibleForTesting
getFoldersToSkipInDefaultCreation()1335     protected List<String> getFoldersToSkipInDefaultCreation() {
1336         return StringUtils.getStringArrayConfig(getContext(),
1337                 R.array.config_foldersToSkipInDefaultCreation);
1338     }
1339 
1340     /**
1341      * Ensure that default folders are created on mounted storage devices.
1342      * We only do this once per volume so we don't annoy the user if deleted
1343      * manually. Folders in the exclusion list are not created.
1344      */
1345     @VisibleForTesting
ensureDefaultFolders(@onNull MediaVolume volume, @NonNull SQLiteDatabase db)1346     protected void ensureDefaultFolders(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) {
1347         if (volume.shouldSkipDefaultDirCreation()) {
1348             // Default folders should not be automatically created inside volumes managed from
1349             // outside Android.
1350             return;
1351         }
1352         final String volumeName = volume.getName();
1353         String key;
1354         if (volumeName.equals(MediaStore.VOLUME_EXTERNAL_PRIMARY)) {
1355             // For the primary volume, we use the ID, because we may be handling
1356             // the primary volume for multiple users
1357             key = DEFAULT_FOLDER_CREATED_KEY_PREFIX
1358                     + getPrimaryVolumeId(volume.getId(), volume.getUser());
1359         } else {
1360             // For others, like public volumes, just use the name, because the id
1361             // might not change when re-formatted
1362             key = DEFAULT_FOLDER_CREATED_KEY_PREFIX + volumeName;
1363         }
1364 
1365         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
1366         if (prefs.getInt(key, 0) == 0) {
1367             // Get case insensitive exclusion list.
1368             List<String> exclusionList =
1369                     Flags.enableExclusionListForDefaultFolders()
1370                             ? getFoldersToSkipInDefaultCreation().stream().map(
1371                             String::toLowerCase).collect(Collectors.toList())
1372                             : List.of();
1373             if (exclusionList.size() > getDefaultFolderNames().length) {
1374                 Log.e(TAG, "Exclusion list has " + exclusionList.size()
1375                         + " items which exceeds the size of default folders list which has size "
1376                         + getDefaultFolderNames().length);
1377                 exclusionList = List.of();
1378             }
1379             for (String folderName : getDefaultFolderNames()) {
1380                 final File folder = new File(volume.getPath(), folderName);
1381                 if (folder.exists()) {
1382                     continue;
1383                 }
1384                 if (Flags.enableExclusionListForDefaultFolders() && exclusionList.contains(
1385                         folderName.toLowerCase(Locale.ROOT))) {
1386                     // Do not create mobile-centric folders for PC.
1387                     Log.d(TAG, "Excluding " + folder + " from default creation");
1388                     continue;
1389                 }
1390                 folder.mkdirs();
1391                 insertDirectory(db, folder.getAbsolutePath());
1392             }
1393 
1394             SharedPreferences.Editor editor = prefs.edit();
1395             editor.putInt(key, 1);
1396             editor.commit();
1397         }
1398     }
1399 
1400     /**
1401      * Returns the volume id for Primary External Volumes.
1402      * If volId is supplied, it is returned as-is, in case it is not, user-id is used to
1403      * construct the id for Primary External Volume.
1404      *
1405      * @param volId the id of the Volume in consideration.
1406      * @param userId userId for which primary volume id needs to be determined.
1407      * @return the primary volume id.
1408      */
getPrimaryVolumeId(String volId, UserHandle userId)1409     private String getPrimaryVolumeId(String volId, UserHandle userId) {
1410         if (volId == null) {
1411             // The construction is based upon system/vold/model/EmulatedVolume.cpp
1412             // Should be kept in sync with the same.
1413             return "emulated;" + userId.getIdentifier();
1414         }
1415         return volId;
1416     }
1417 
1418     /**
1419      * Ensure that any thumbnail collections on the given storage volume can be
1420      * used with the given {@link DatabaseHelper}. If the
1421      * {@link DatabaseHelper#getOrCreateUuid} doesn't match the UUID found on
1422      * disk, then all thumbnails will be considered stable and will be deleted.
1423      */
ensureThumbnailsValid(@onNull MediaVolume volume, @NonNull SQLiteDatabase db)1424     private void ensureThumbnailsValid(@NonNull MediaVolume volume, @NonNull SQLiteDatabase db) {
1425         if (volume.shouldSkipDefaultDirCreation()) {
1426             // Default folders and thumbnail directories should not be automatically created inside
1427             // volumes managed from outside Android, and there is no need to ensure the validity of
1428             // their thumbnails here.
1429             return;
1430         }
1431         final String uuidFromDatabase = DatabaseHelper.getOrCreateUuid(db);
1432         try {
1433             for (File dir : getThumbnailDirectories(volume)) {
1434                 if (!dir.exists()) {
1435                     dir.mkdirs();
1436                 }
1437 
1438                 final File file = new File(dir, FILE_DATABASE_UUID);
1439                 final Optional<String> uuidFromDisk = FileUtils.readString(file);
1440 
1441                 final boolean updateUuid;
1442                 if (!uuidFromDisk.isPresent()) {
1443                     // For newly inserted volumes or upgrading of existing volumes,
1444                     // assume that our current UUID is valid
1445                     updateUuid = true;
1446                 } else if (!Objects.equals(uuidFromDatabase, uuidFromDisk.get())) {
1447                     // The UUID of database disagrees with the one on disk,
1448                     // which means we can't trust any thumbnails
1449                     Log.d(TAG, "Invalidating all thumbnails under " + dir);
1450                     FileUtils.walkFileTreeContents(dir.toPath(), this::deleteAndInvalidate);
1451                     updateUuid = true;
1452                 } else {
1453                     updateUuid = false;
1454                 }
1455 
1456                 if (updateUuid) {
1457                     FileUtils.writeString(file, Optional.of(uuidFromDatabase));
1458                 }
1459             }
1460         } catch (IOException e) {
1461             Log.w(TAG, "Failed to ensure thumbnails valid for " + volume.getName(), e);
1462         }
1463     }
1464 
1465     @Override
attachInfo(Context context, ProviderInfo info)1466     public void attachInfo(Context context, ProviderInfo info) {
1467         Log.v(TAG, "Attached " + info.authority + " from " + info.applicationInfo.packageName);
1468 
1469         mUriMatcher = new LocalUriMatcher(info.authority);
1470 
1471         super.attachInfo(context, info);
1472     }
1473 
1474     @Nullable
1475     private static MediaProvider sInstance;
1476 
1477     @Nullable
getInstance()1478     static synchronized MediaProvider getInstance() {
1479         return sInstance;
1480     }
1481 
1482     @Override
onCreate()1483     public boolean onCreate() {
1484         synchronized (MediaProvider.class) {
1485             sInstance = this;
1486         }
1487 
1488         final Context context = getContext();
1489 
1490         mUserCache = new UserCache(context);
1491 
1492         // Shift call statistics back to the original caller
1493         Binder.setProxyTransactListener(mTransactListener);
1494 
1495         mStorageManager = context.getSystemService(StorageManager.class);
1496         AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
1497         mPackageManager = context.getPackageManager();
1498         mUserManager = context.getSystemService(UserManager.class);
1499         mVolumeCache = new VolumeCache(context, mUserCache);
1500 
1501         // Reasonable thumbnail size is half of the smallest screen edge width
1502         final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
1503         final int thumbSize = Math.min(metrics.widthPixels, metrics.heightPixels) / 2;
1504         mThumbSize = new Size(thumbSize, thumbSize);
1505 
1506         mConfigStore = createConfigStore();
1507         mDatabaseBackupAndRecovery = createDatabaseBackupAndRecovery();
1508 
1509         mMediaScanner = new ModernMediaScanner(context, mConfigStore);
1510         mProjectionHelper = new ProjectionHelper(Column.class, ExportedSince.class);
1511         mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, false, false,
1512                 mProjectionHelper, Metrics::logSchemaChange, mFilesListener,
1513                 MIGRATION_LISTENER, mIdGenerator, true, mDatabaseBackupAndRecovery);
1514         mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, false,
1515                 mProjectionHelper, Metrics::logSchemaChange, mFilesListener,
1516                 MIGRATION_LISTENER, mIdGenerator, true, mDatabaseBackupAndRecovery);
1517         mExternalDbFacade = new ExternalDbFacade(getContext(), mExternalDatabase, mVolumeCache);
1518 
1519         mMediaGrants = new MediaGrants(mExternalDatabase);
1520         mFilesOwnershipUtils = new FilesOwnershipUtils(mExternalDatabase);
1521 
1522         PickerSyncLockManager pickerSyncLockManager = new PickerSyncLockManager();
1523         mPickerDbFacade = new PickerDbFacade(context, pickerSyncLockManager);
1524         mPickerSyncController = PickerSyncController.initialize(context, mPickerDbFacade,
1525                 mConfigStore, pickerSyncLockManager);
1526         mPickerDataLayer = PickerDataLayer.create(context, mPickerDbFacade, mPickerSyncController,
1527                 mConfigStore);
1528         mPhotoPickerTranscodeHelper = new PhotoPickerTranscodeHelper();
1529         mPickerUriResolver = new PickerUriResolver(context, mPickerDbFacade, mProjectionHelper,
1530                 mUriMatcher);
1531         mAsyncPickerFileOpener = new AsyncPickerFileOpener(this, mPickerUriResolver);
1532 
1533         mExternalPrimaryBackupExecutor = new BackupExecutor(getContext(), mExternalDatabase);
1534 
1535         if (SdkLevel.isAtLeastS()) {
1536             mTranscodeHelper = new TranscodeHelperImpl(context, this, mConfigStore);
1537         } else {
1538             mTranscodeHelper = new TranscodeHelperNoOp();
1539         }
1540 
1541         final IntentFilter packageFilter = new IntentFilter();
1542         packageFilter.setPriority(10);
1543         packageFilter.addDataScheme("package");
1544         packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
1545         packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
1546         packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
1547         context.registerReceiver(mPackageReceiver, packageFilter);
1548 
1549         // Creating intent broadcast receiver for user actions like Intent.ACTION_USER_REMOVED,
1550         // where we would need to remove files stored by removed user.
1551         final IntentFilter userIntentFilter = new IntentFilter();
1552         userIntentFilter.addAction(Intent.ACTION_USER_REMOVED);
1553         context.registerReceiver(mUserIntentReceiver, userIntentFilter);
1554 
1555         // Watch for invalidation of cached volumes
1556         mStorageManager.registerStorageVolumeCallback(context.getMainExecutor(),
1557                 new StorageVolumeCallback() {
1558                     @Override
1559                     public void onStateChanged(@NonNull StorageVolume volume) {
1560                         updateVolumes();
1561                     }
1562                 });
1563 
1564         if (SdkLevel.isAtLeastT()) {
1565             try {
1566                 mStorageManager.setCloudMediaProvider(mPickerSyncController.getCloudProvider());
1567             } catch (SecurityException e) {
1568                 // This can happen in unit tests
1569                 Log.w(TAG, "Failed to update the system_server with the latest cloud provider", e);
1570             }
1571         }
1572 
1573         updateVolumes();
1574         attachVolume(MediaVolume.fromInternal(), /* validate */ false, /* volumeState */ null);
1575         for (MediaVolume volume : mVolumeCache.getExternalVolumes()) {
1576             attachVolume(volume, /* validate */ false, /* volumeState */ null);
1577         }
1578 
1579         // Watch for performance-sensitive activity
1580         appOpsManager.startWatchingActive(new String[] {
1581                 AppOpsManager.OPSTR_CAMERA
1582         }, context.getMainExecutor(), mActiveListener);
1583 
1584         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE,
1585                 null /* all packages */, mModeListener);
1586         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_AUDIO,
1587                 null /* all packages */, mModeListener);
1588         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_IMAGES,
1589                 null /* all packages */, mModeListener);
1590         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_VIDEO,
1591                 null /* all packages */, mModeListener);
1592         if (SdkLevel.isAtLeastU()) {
1593             appOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_MEDIA_VISUAL_USER_SELECTED,
1594                     null /* all packages */, mModeListener);
1595         }
1596         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE,
1597                 null /* all packages */, mModeListener);
1598         appOpsManager.startWatchingMode(permissionToOp(ACCESS_MEDIA_LOCATION),
1599                 null /* all packages */, mModeListener);
1600         // Legacy apps
1601         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_LEGACY_STORAGE,
1602                 null /* all packages */, mModeListener);
1603         // File managers
1604         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE,
1605                 null /* all packages */, mModeListener);
1606         // Default gallery changes
1607         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES,
1608                 null /* all packages */, mModeListener);
1609         appOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO,
1610                 null /* all packages */, mModeListener);
1611         try {
1612             // Here we are forced to depend on the non-public API of AppOpsManager. If
1613             // OPSTR_NO_ISOLATED_STORAGE app op is not defined in AppOpsManager, then this call will
1614             // throw an IllegalArgumentException during MediaProvider startup. In combination with
1615             // MediaProvider's CTS tests it should give us guarantees that OPSTR_NO_ISOLATED_STORAGE
1616             // is defined.
1617             appOpsManager.startWatchingMode(AppOpsManager.OPSTR_NO_ISOLATED_STORAGE,
1618                     null /* all packages */, mModeListener);
1619         } catch (IllegalArgumentException e) {
1620             Log.w(TAG, "Failed to start watching " + AppOpsManager.OPSTR_NO_ISOLATED_STORAGE, e);
1621         }
1622 
1623         ProviderInfo provider = mPackageManager.resolveContentProvider(
1624                 getDownloadsProviderAuthority(), PackageManager.MATCH_DIRECT_BOOT_AWARE
1625                 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
1626         if (provider != null) {
1627             mDownloadsAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
1628         }
1629 
1630         provider = mPackageManager.resolveContentProvider(getExternalStorageProviderAuthority(),
1631                 PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
1632         if (provider != null) {
1633             mExternalStorageAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
1634         }
1635 
1636         storageNativeBootPropertyChangeListener();
1637         mConfigStore.addOnChangeListener(
1638                 BackgroundThread.getExecutor(), this::storageNativeBootPropertyChangeListener);
1639 
1640         PulledMetrics.initialize(context);
1641         mMaliciousAppDetector = createMaliciousAppDetector();
1642 
1643         initializeMimeTypeFixHandlerForAndroid15(getContext());
1644 
1645         return true;
1646     }
1647 
1648     @VisibleForTesting
storageNativeBootPropertyChangeListener()1649     protected void storageNativeBootPropertyChangeListener() {
1650 
1651         // Notify the Photopicker that DeviceConfig has changed for T+ devices.
1652         Intent intent = new Intent(Intent.ACTION_MAIN);
1653         if (SdkLevel.isAtLeastT()) {
1654             getContext().sendBroadcast(intent, MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION);
1655         }
1656 
1657         boolean isGetContentTakeoverEnabled = false;
1658 
1659         if (SdkLevel.isAtLeastT()) {
1660             isGetContentTakeoverEnabled = true;
1661         } else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
1662             isGetContentTakeoverEnabled = true;
1663         } else {
1664             isGetContentTakeoverEnabled = mConfigStore.isGetContentTakeOverEnabled();
1665         }
1666         setComponentEnabledSetting(
1667                 "PhotoPickerGetContentActivity", isGetContentTakeoverEnabled);
1668 
1669         // Always make sure PhotoPickerActivity is enabled.
1670         setComponentEnabledSetting(
1671                 "PhotoPickerActivity", true);
1672 
1673         // Always make sure PhotoPickerUserSelectActivity is enabled.
1674         setComponentEnabledSetting(
1675                 "PhotoPickerUserSelectActivity", true);
1676     }
1677 
getDatabaseBackupAndRecovery()1678     public DatabaseBackupAndRecovery getDatabaseBackupAndRecovery() {
1679         return mDatabaseBackupAndRecovery;
1680     }
1681 
setComponentEnabledSetting(@onNull String activityName, boolean isEnabled)1682     private void setComponentEnabledSetting(@NonNull String activityName, boolean isEnabled) {
1683         final String activityFullName =
1684                 PhotoPickerActivity.class.getPackage().getName() + "." + activityName;
1685         final ComponentName componentName = new ComponentName(getContext().getPackageName(),
1686                 activityFullName);
1687 
1688         final int expectedState = isEnabled
1689                 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
1690                 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
1691 
1692         Log.i(TAG, "Changed " + activityName + " component state to "
1693                 + componentStateToString(expectedState));
1694 
1695         getContext().getPackageManager().setComponentEnabledSetting(componentName, expectedState,
1696                 PackageManager.DONT_KILL_APP);
1697     }
1698 
getDatabaseHelper(String dbName)1699     Optional<DatabaseHelper> getDatabaseHelper(String dbName) {
1700         if (dbName.equalsIgnoreCase(INTERNAL_DATABASE_NAME)) {
1701             return Optional.of(mInternalDatabase);
1702         } else if (dbName.equalsIgnoreCase(EXTERNAL_DATABASE_NAME)) {
1703             return Optional.of(mExternalDatabase);
1704         }
1705 
1706         return Optional.empty();
1707     }
1708 
1709     @Override
onCallingPackageChanged()1710     public void onCallingPackageChanged() {
1711         // Identity of the current thread has changed, so invalidate caches
1712         mCallingIdentity.remove();
1713     }
1714 
clearLocalCallingIdentity()1715     public LocalCallingIdentity clearLocalCallingIdentity() {
1716         // We retain the user part of the calling identity, since we are executing
1717         // the call on behalf of that user, and we need to maintain the user context
1718         // to correctly resolve things like volumes
1719         UserHandle user = mCallingIdentity.get().getUser();
1720         return clearLocalCallingIdentity(LocalCallingIdentity.fromSelfAsUser(getContext(), user));
1721     }
1722 
clearLocalCallingIdentity(LocalCallingIdentity replacement)1723     public LocalCallingIdentity clearLocalCallingIdentity(LocalCallingIdentity replacement) {
1724         final LocalCallingIdentity token = mCallingIdentity.get();
1725         mCallingIdentity.set(replacement);
1726         return token;
1727     }
1728 
restoreLocalCallingIdentity(LocalCallingIdentity token)1729     public void restoreLocalCallingIdentity(LocalCallingIdentity token) {
1730         mCallingIdentity.set(token);
1731     }
1732 
1733     /**
1734      * Adds the mapping from thread id to uid in PendingOpen map.
1735      */
addToPendingOpenMap(int tid, int uid)1736     public void addToPendingOpenMap(int tid, int uid) {
1737         synchronized (mPendingOpenInfo) {
1738             mPendingOpenInfo.put(tid, new PendingOpenInfo(uid, /* mediaCapabilitiesUid */ 0,
1739                     /* shouldRedact */ false, /* transcodeReason */ 0));
1740         }
1741     }
1742 
1743     /**
1744      * Removes the pending open info for the passed thread i from PendingOpen map.
1745      */
removeFromPendingOpenMap(int tid)1746     public void removeFromPendingOpenMap(int tid) {
1747         synchronized (mPendingOpenInfo) {
1748             mPendingOpenInfo.remove(tid);
1749         }
1750     }
1751 
isPackageKnown(@onNull String packageName, int userId)1752     private boolean isPackageKnown(@NonNull String packageName, int userId) {
1753         final Context context = mUserCache.getContextForUser(UserHandle.of(userId));
1754         final PackageManager pm = context.getPackageManager();
1755 
1756         // First, is the app actually installed?
1757         try {
1758             pm.getPackageInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES);
1759             return true;
1760         } catch (NameNotFoundException ignored) {
1761         }
1762 
1763         // Second, is the app pending, probably from a backup/restore operation?
1764         // Cloned app installations do not have a linked install session, so skipping the check in
1765         // case the user-id is a clone profile.
1766         if (!isAppCloneUserForFuse(userId)) {
1767             if (sUserId != userId) {
1768                 // Skip the package check and ensure media provider doesn't crash
1769                 // Returning true since we are unsure what caused the cross-user entries to be in
1770                 // the database and want to avoid deleting data that might be required.
1771                 Log.e(TAG, "Skip pruning cross-user entries stored in database for package: "
1772                         + packageName + " userId: " + userId + " processUserId: " + sUserId);
1773                 return true;
1774             }
1775             for (SessionInfo si : pm.getPackageInstaller().getAllSessions()) {
1776                 if (Objects.equals(packageName, si.getAppPackageName())) {
1777                     return true;
1778                 }
1779             }
1780         } else {
1781             Log.e(TAG, "Cross-user entries found in database for package " + packageName
1782                     + " userId: " + userId + " processUserId: " + sUserId);
1783         }
1784 
1785         // I've never met this package in my life
1786         return false;
1787     }
1788 
onIdleMaintenance(@onNull CancellationSignal signal)1789     public void onIdleMaintenance(@NonNull CancellationSignal signal) {
1790         final long startTime = SystemClock.elapsedRealtime();
1791 
1792         // Print # of deleted files
1793         synchronized (mCachedCallingIdentityForFuse) {
1794             for (int i = 0; i < mCachedCallingIdentityForFuse.size(); i++) {
1795                 mCachedCallingIdentityForFuse.valueAt(i).dump("Idle maintenance");
1796             }
1797         }
1798 
1799         // Trim any stale log files before we emit new events below
1800         Logging.trimPersistent();
1801 
1802         // Scan all volumes to resolve any staleness
1803         for (MediaVolume volume : mVolumeCache.getExternalVolumes()) {
1804             // Possibly bail before digging into each volume
1805             signal.throwIfCanceled();
1806 
1807             try {
1808                 MediaService.onScanVolume(getContext(), volume, REASON_IDLE);
1809             } catch (IOException | IllegalArgumentException e) {
1810                 Log.w(TAG, "Failure in " + volume.getName() + " volume scan", e);
1811             }
1812 
1813             // Ensure that our thumbnails are valid
1814             mExternalDatabase.runWithTransaction((db) -> {
1815                 ensureThumbnailsValid(volume, db);
1816                 return null;
1817             });
1818         }
1819         BackupAndRestoreUtils.doCleanUpAfterRestoreIfRequired(getContext());
1820 
1821         // Delete any stale thumbnails
1822         final int staleThumbnails = mExternalDatabase.runWithTransaction((db) -> {
1823             return pruneThumbnails(db, signal);
1824         });
1825         Log.d(TAG, "Pruned " + staleThumbnails + " unknown thumbnails");
1826 
1827         // Finished orphaning any content whose package no longer exists
1828         pruneStalePackages(signal);
1829 
1830         // Delete the expired items or extend them on mounted volumes
1831         final int[] result = deleteOrExtendExpiredItems(signal);
1832         final int deletedExpiredMedia = result[0];
1833         Log.d(TAG, "Deleted " + deletedExpiredMedia + " expired items");
1834         Log.d(TAG, "Extended " + result[1] + " expired items");
1835 
1836         // Forget any stale volumes
1837         deleteStaleVolumes(signal);
1838 
1839         final long itemCount = mExternalDatabase.runWithTransaction(DatabaseHelper::getItemCount);
1840 
1841         // Clean picker transcoded media cache.
1842         mPhotoPickerTranscodeHelper.cleanAllTranscodedFiles(signal);
1843 
1844         // Cleaning media files for users that have been removed
1845         cleanMediaFilesForRemovedUser(signal);
1846 
1847         // Calculate standard_mime_type_extension column for files which have SPECIAL_FORMAT column
1848         // value as NULL, and update the same in the picker db
1849         detectSpecialFormat(signal);
1850 
1851         mExternalPrimaryBackupExecutor.doBackup(signal);
1852 
1853         // In Android 15, certain MIME types were introduced that are not supported, this fixes
1854         // existing data with these unsupported MIME types
1855         fixUnsupportedMimeTypesForAndroid15(getContext());
1856 
1857         final long durationMillis = (SystemClock.elapsedRealtime() - startTime);
1858         Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount,
1859                 durationMillis, staleThumbnails, deletedExpiredMedia);
1860     }
1861 
1862     /**
1863      * This function find and clean the files related to user who have been removed
1864      */
cleanMediaFilesForRemovedUser(CancellationSignal signal)1865     private void cleanMediaFilesForRemovedUser(CancellationSignal signal) {
1866         //Finding userIds that are available in database
1867         final List<String> userIds = mExternalDatabase.runWithTransaction((db) -> {
1868             final List<String> userIdsPresent = new ArrayList<>();
1869             try (Cursor c = db.query(true, "files", new String[] { "_user_id" },
1870                     null, null, null, null, null,
1871                     null, signal)) {
1872                 while (c.moveToNext()) {
1873                     final String userId = c.getString(0);
1874                     userIdsPresent.add(userId);
1875                 }
1876             }
1877             return userIdsPresent;
1878         });
1879 
1880         // removing calling userId
1881         userIds.remove(String.valueOf(sUserId));
1882 
1883         List<String> validUserProfiles = mUserManager.getEnabledProfiles().stream()
1884                 .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect(
1885                         Collectors.toList());
1886         // removing all the valid/existing user, remaining userIds would be users who would have
1887         // been removed
1888         userIds.removeAll(validUserProfiles);
1889 
1890         // Cleaning media files of users who have been removed
1891         mExternalDatabase.runWithTransaction((db) -> {
1892             userIds.stream().forEach(userId ->{
1893                 Log.d(TAG, "Removing media files associated with user : " + userId);
1894                 db.execSQL("delete from files where _user_id=?",
1895                         new String[]{String.valueOf(userId)});
1896             });
1897             return null ;
1898         });
1899 
1900         boolean isDeviceInDemoMode = false;
1901         try {
1902             isDeviceInDemoMode = Settings.Global.getInt(getContext().getContentResolver(),
1903                     Settings.Global.DEVICE_DEMO_MODE) > 0;
1904         } catch (Settings.SettingNotFoundException e) {
1905             Log.w(TAG, "Exception in reading DEVICE_DEMO_MODE setting", e);
1906         }
1907 
1908         Log.i(TAG, "isDeviceInDemoMode: " + isDeviceInDemoMode);
1909         // Only allow default system user 0 to update xattrs on /data/media/0 and only when
1910         // device is in retail mode
1911         if (sUserId == UserHandle.SYSTEM.getIdentifier() && isDeviceInDemoMode) {
1912             List<String> validUsers = mUserManager.getUserHandles(/* excludeDying */ true).stream()
1913                     .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect(
1914                             Collectors.toList());
1915             Log.i(TAG, "Active user ids are:" + validUsers);
1916             mDatabaseBackupAndRecovery.removeRecoveryDataExceptValidUsers(validUsers);
1917         }
1918     }
1919 
pruneStalePackages(CancellationSignal signal)1920     private void pruneStalePackages(CancellationSignal signal) {
1921         final int stalePackages = mExternalDatabase.runWithTransaction((db) -> {
1922             final ArraySet<Pair<String, Integer>> unknownPackages = new ArraySet<>();
1923             try (Cursor c = db.query(true, "files",
1924                     new String[] { "owner_package_name", "_user_id" },
1925                     null, null, null, null, null, null, signal)) {
1926                 while (c.moveToNext()) {
1927                     final String packageName = c.getString(0);
1928                     if (TextUtils.isEmpty(packageName)) continue;
1929 
1930                     final int userId = c.getInt(1);
1931 
1932                     if (!isPackageKnown(packageName, userId)) {
1933                         unknownPackages.add(Pair.create(packageName, userId));
1934                     }
1935                 }
1936             }
1937             for (Pair<String, Integer> pair : unknownPackages) {
1938                 onPackageOrphaned(db, pair.first, pair.second);
1939             }
1940             return unknownPackages.size();
1941         });
1942         Log.d(TAG, "Pruned " + stalePackages + " unknown packages");
1943     }
1944 
deleteStaleVolumes(CancellationSignal signal)1945     private void deleteStaleVolumes(CancellationSignal signal) {
1946         mExternalDatabase.runWithTransaction((db) -> {
1947             final Set<String> recentVolumeNames = MediaStore
1948                     .getRecentExternalVolumeNames(getContext());
1949             final Set<String> knownVolumeNames = new ArraySet<>();
1950             try (Cursor c = db.query(true, "files", new String[] { MediaColumns.VOLUME_NAME },
1951                     null, null, null, null, null, null, signal)) {
1952                 while (c.moveToNext()) {
1953                     knownVolumeNames.add(c.getString(0));
1954                 }
1955             }
1956             final Set<String> staleVolumeNames = new ArraySet<>();
1957             staleVolumeNames.addAll(knownVolumeNames);
1958             staleVolumeNames.removeAll(recentVolumeNames);
1959             for (String staleVolumeName : staleVolumeNames) {
1960                 final int num = db.delete("files", FileColumns.VOLUME_NAME + "=?",
1961                         new String[] { staleVolumeName });
1962                 Log.d(TAG, "Forgot " + num + " stale items from " + staleVolumeName);
1963                 mDatabaseBackupAndRecovery.deleteBackupForVolume(staleVolumeName);
1964             }
1965             return null;
1966         });
1967 
1968         synchronized (mDirectoryCache) {
1969             mDirectoryCache.clear();
1970         }
1971     }
1972 
1973     @VisibleForTesting
setUriResolver(PickerUriResolver resolver)1974     public void setUriResolver(PickerUriResolver resolver) {
1975         Log.w(TAG, "Changing the PickerUriResolver!!! Should only be called during test");
1976         mPickerUriResolver = resolver;
1977     }
1978 
1979     @VisibleForTesting
detectSpecialFormat(@onNull CancellationSignal signal)1980     void detectSpecialFormat(@NonNull CancellationSignal signal) {
1981         // Picker sync and special format update can execute concurrently and run into a deadlock.
1982         // Acquiring a lock before execution of each flow to avoid this.
1983         PickerSyncController.sIdleMaintenanceSyncLock.lock();
1984         try {
1985             mExternalDatabase.runWithTransaction((db) -> {
1986                 updateSpecialFormatColumn(db, signal);
1987                 return null;
1988             });
1989         } finally {
1990             PickerSyncController.sIdleMaintenanceSyncLock.unlock();
1991         }
1992     }
1993 
fixUnsupportedMimeTypesForAndroid15(Context context)1994     private void fixUnsupportedMimeTypesForAndroid15(Context context) {
1995         if (!Flags.enableMimeTypeFixForAndroid15()) {
1996             return;
1997         }
1998 
1999         if (context == null) {
2000             return;
2001         }
2002 
2003         if (Build.VERSION.SDK_INT != Build.VERSION_CODES.VANILLA_ICE_CREAM) {
2004             return;
2005         }
2006 
2007         SharedPreferences prefs = context.getSharedPreferences(MEDIAPROVIDER_PREFS,
2008                 Context.MODE_PRIVATE);
2009         if (prefs.getBoolean(IS_MIME_TYPE_FIXED_IN_ANDROID_15, false)) {
2010             Log.v(TAG, "Mime type already corrected");
2011             return;
2012         }
2013 
2014         mExternalDatabase.runWithTransaction(db -> {
2015             boolean isSuccess = MimeTypeFixHandler.updateUnsupportedMimeTypes(db);
2016             // if success then update the shared pref value
2017             if (isSuccess) {
2018                 SharedPreferences.Editor editor = prefs.edit();
2019                 editor.putBoolean(IS_MIME_TYPE_FIXED_IN_ANDROID_15, true);
2020                 editor.apply();
2021             }
2022             return null;
2023         });
2024     }
2025 
updateSpecialFormatColumn(SQLiteDatabase db, @NonNull CancellationSignal signal)2026     private void updateSpecialFormatColumn(SQLiteDatabase db, @NonNull CancellationSignal signal) {
2027         // This is to ensure we only do a bounded iteration over the rows as updates can fail, and
2028         // we don't want to keep running the query/update indefinitely.
2029         final int totalRowsToUpdate = getPendingSpecialFormatRowsCount(db, signal);
2030         for (int i = 0; i < totalRowsToUpdate; i += IDLE_MAINTENANCE_ROWS_LIMIT) {
2031             try (PickerDbFacade.UpdateMediaOperation operation =
2032                          mPickerDbFacade.beginUpdateMediaOperation(
2033                                  PickerSyncController.LOCAL_PICKER_PROVIDER_AUTHORITY)) {
2034                 updateSpecialFormatForLimitedRows(db, signal, operation);
2035                 operation.setSuccess();
2036             }
2037         }
2038     }
2039 
getPendingSpecialFormatRowsCount(SQLiteDatabase db, @NonNull CancellationSignal signal)2040     private int getPendingSpecialFormatRowsCount(SQLiteDatabase db,
2041             @NonNull CancellationSignal signal) {
2042         try (Cursor c = queryForPendingSpecialFormatColumns(db, /* limit */ null, signal)) {
2043             if (c == null) {
2044                 return 0;
2045             }
2046             return c.getCount();
2047         }
2048     }
2049 
updateSpecialFormatForLimitedRows(SQLiteDatabase externalDb, @NonNull CancellationSignal signal, PickerDbFacade.UpdateMediaOperation operation)2050     private void updateSpecialFormatForLimitedRows(SQLiteDatabase externalDb,
2051             @NonNull CancellationSignal signal, PickerDbFacade.UpdateMediaOperation operation) {
2052         // Accumulate all the new SPECIAL_FORMAT updates with their ids
2053         ArrayMap<Long, Integer> newSpecialFormatValues = new ArrayMap<>();
2054         final String limit = String.valueOf(IDLE_MAINTENANCE_ROWS_LIMIT);
2055         try (Cursor c = queryForPendingSpecialFormatColumns(externalDb, limit, signal)) {
2056             while (c.moveToNext() && !signal.isCanceled()) {
2057                 final long id = c.getLong(0);
2058                 final String path = c.getString(1);
2059                 newSpecialFormatValues.put(id, getSpecialFormatValue(path));
2060             }
2061         }
2062 
2063         // Now, update all the new SPECIAL_FORMAT values in both external db and picker db.
2064         final ContentValues pickerDbValues = new ContentValues();
2065         final ContentValues externalDbValues = new ContentValues();
2066         int count = 0;
2067         for (long id : newSpecialFormatValues.keySet()) {
2068             if (signal.isCanceled()) {
2069                 return;
2070             }
2071 
2072             int specialFormat = newSpecialFormatValues.get(id);
2073 
2074             pickerDbValues.clear();
2075             pickerDbValues.put(PickerDbFacade.KEY_STANDARD_MIME_TYPE_EXTENSION, specialFormat);
2076             boolean pickerDbWriteSuccess = operation.execute(String.valueOf(id), pickerDbValues);
2077 
2078             externalDbValues.clear();
2079             externalDbValues.put(_SPECIAL_FORMAT, specialFormat);
2080             final String externalDbSelection = MediaColumns._ID + "=?";
2081             final String[] externalDbSelectionArgs = new String[]{String.valueOf(id)};
2082             boolean externalDbWriteSuccess =
2083                     externalDb.update("files", externalDbValues, externalDbSelection,
2084                             externalDbSelectionArgs)
2085                             == 1;
2086 
2087             if (pickerDbWriteSuccess && externalDbWriteSuccess) {
2088                 count++;
2089             }
2090         }
2091         Log.d(TAG, "Updated standard_mime_type_extension for " + count + " items");
2092     }
2093 
getSpecialFormatValue(String path)2094     private int getSpecialFormatValue(String path) {
2095         final File file = new File(path);
2096         if (!file.exists()) {
2097             // We always update special format to none if the file is not found or there is an
2098             // error, this is so that we do not repeat over the same column again and again.
2099             return _SPECIAL_FORMAT_NONE;
2100         }
2101 
2102         try {
2103             return SpecialFormatDetector.detect(file);
2104         } catch (Exception e) {
2105             // we tried our best, no need to run special detection again and again if it
2106             // throws exception once, it is likely to do so everytime.
2107             Log.d(TAG, "Failed to detect special format for file: " + file, e);
2108             return _SPECIAL_FORMAT_NONE;
2109         }
2110     }
2111 
queryForPendingSpecialFormatColumns(SQLiteDatabase db, String limit, @NonNull CancellationSignal signal)2112     private Cursor queryForPendingSpecialFormatColumns(SQLiteDatabase db, String limit,
2113             @NonNull CancellationSignal signal) {
2114         // Run special detection for images only
2115         final String selection = _SPECIAL_FORMAT + " IS NULL AND "
2116                 + MEDIA_TYPE + "=" + MEDIA_TYPE_IMAGE;
2117         final String[] projection = new String[] { MediaColumns._ID, MediaColumns.DATA };
2118         return db.query(/* distinct */ true, "files", projection, selection, null, null, null,
2119                 null, limit, signal);
2120     }
2121 
2122     /**
2123      * Delete any expired content on mounted volumes. The expired content on unmounted
2124      * volumes will be deleted when we forget any stale volumes; we're cautious about
2125      * wildly changing clocks, so only delete items within the last week.
2126      * If the items are expired more than one week, extend the expired time of them
2127      * another one week to avoid data loss with incorrect time zone data. We will
2128      * delete it when it is expired next time.
2129      *
2130      * @param signal the cancellation signal
2131      * @return the integer array includes total deleted count and total extended count
2132      */
2133     @NonNull
deleteOrExtendExpiredItems(@onNull CancellationSignal signal)2134     private int[] deleteOrExtendExpiredItems(@NonNull CancellationSignal signal) {
2135         final long expiredOneWeek =
2136                 ((System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS) / 1000);
2137         final long now = (System.currentTimeMillis() / 1000);
2138         final long expiredTime = now + (FileUtils.DEFAULT_DURATION_EXTENDED / 1000);
2139         return mExternalDatabase.runWithTransaction((db) -> {
2140             String selection = FileColumns.DATE_EXPIRES + " < " + now;
2141             selection += " AND (IS_PENDING=1 OR IS_TRASHED=1)";
2142             selection += " AND volume_name in " + bindList(MediaStore.getExternalVolumeNames(
2143                     getContext()).toArray());
2144             String[] projection = new String[]{"volume_name", "_id",
2145                     FileColumns.DATE_EXPIRES, FileColumns.DATA};
2146             final class TrashItem {
2147                 final String mVolumeName;
2148                 final long mId;
2149                 final long mDateExpires;
2150                 final String mOriginalPath;
2151 
2152                 TrashItem(String volumeName, long id, long dateExpires, String oriPath) {
2153                     this.mVolumeName = volumeName;
2154                     this.mId = id;
2155                     this.mDateExpires = dateExpires;
2156                     this.mOriginalPath = oriPath;
2157                 }
2158             }
2159 
2160             final List<TrashItem> items = new ArrayList<>();
2161             try (Cursor c = db.query(true, "files", projection, selection,
2162                     null, null, null, null, null, signal)) {
2163                 while (c.moveToNext()) {
2164                     items.add(new TrashItem(
2165                             c.getString(0), // volumeName
2166                             c.getLong(1),   // id
2167                             c.getLong(2),   // dateExpires
2168                             c.getString(3)  // oriPath
2169                     ));
2170                 }
2171             }
2172 
2173             int totalDeleteCount = 0;
2174             int totalExtendedCount = 0;
2175             int index = 0;
2176 
2177             for (TrashItem item : items) {
2178                 if (item.mDateExpires > expiredOneWeek) {
2179                     totalDeleteCount += delete(Files.getContentUri(item.mVolumeName, item.mId),
2180                             null, null);
2181                 } else {
2182                     boolean success = extendExpiredItem(db, item.mOriginalPath, item.mId,
2183                             expiredTime, expiredTime + index);
2184                     if (success) {
2185                         totalExtendedCount++;
2186                     }
2187                     index++;
2188                 }
2189             }
2190 
2191             return new int[]{totalDeleteCount, totalExtendedCount};
2192         });
2193     }
2194 
2195     /**
2196      * Extend the expired items by renaming the file to new path with new timestamp and updating the
2197      * database for {@link FileColumns#DATA} and {@link FileColumns#DATE_EXPIRES}. If there is
2198      * UNIQUE constraint error for FileColumns.DATA, use adjustedExpiredTime and generate the new
2199      * path by adjustedExpiredTime.
2200      */
extendExpiredItem(@onNull SQLiteDatabase db, @NonNull String originalPath, long id, long newExpiredTime, long adjustedExpiredTime)2201     private boolean extendExpiredItem(@NonNull SQLiteDatabase db, @NonNull String originalPath,
2202             long id, long newExpiredTime, long adjustedExpiredTime) {
2203         String newPath = FileUtils.getAbsoluteExtendedPath(originalPath, newExpiredTime);
2204         if (newPath == null) {
2205             Log.e(TAG, "Couldn't compute path for " + originalPath + " and expired time "
2206                     + newExpiredTime);
2207             return false;
2208         }
2209 
2210         try {
2211             if (updateDatabaseForExpiredItem(db, newPath, id, newExpiredTime)) {
2212                 return renameInLowerFsAndInvalidateFuseDentry(originalPath, newPath);
2213             }
2214             return false;
2215         } catch (SQLiteConstraintException e) {
2216             final String errorMessage =
2217                     "Update database _data from " + originalPath + " to " + newPath + " failed.";
2218             Log.d(TAG, errorMessage, e);
2219         }
2220 
2221         // When we update the database for newPath with newExpiredTime, if the new path already
2222         // exists in the database, it may raise SQLiteConstraintException.
2223         // If there are two expired items that have the same display name in the same directory,
2224         // but they have different expired time. E.g. .trashed-123-A.jpg and .trashed-456-A.jpg.
2225         // After we rename .trashed-123-A.jpg to .trashed-newExpiredTime-A.jpg, then we rename
2226         // .trashed-456-A.jpg to .trashed-newExpiredTime-A.jpg, it raises the exception. For
2227         // this case, we will retry it with the adjustedExpiredTime again.
2228         newPath = FileUtils.getAbsoluteExtendedPath(originalPath, adjustedExpiredTime);
2229         Log.i(TAG, "Retrying to extend expired item with the new path = " + newPath);
2230         try {
2231             if (updateDatabaseForExpiredItem(db, newPath, id, adjustedExpiredTime)) {
2232                 return renameInLowerFsAndInvalidateFuseDentry(originalPath, newPath);
2233             }
2234         } catch (SQLiteConstraintException e) {
2235             // If we want to rename one expired item E.g. .trashed-123-A.jpg., and there is another
2236             // non-expired trashed/pending item has the same name. E.g.
2237             // .trashed-adjustedExpiredTime-A.jpg. When we rename .trashed-123-A.jpg to
2238             // .trashed-adjustedExpiredTime-A.jpg, it raises the SQLiteConstraintException.
2239             // The smallest unit of the expired time we use is second. It is a very rare case.
2240             // When this case is happened, we can handle it in next idle maintenance.
2241             final String errorMessage =
2242                     "Update database _data from " + originalPath + " to " + newPath + " failed.";
2243             Log.d(TAG, errorMessage, e);
2244         }
2245 
2246         return false;
2247     }
2248 
updateDatabaseForExpiredItem(@onNull SQLiteDatabase db, @NonNull String path, long id, long expiredTime)2249     private boolean updateDatabaseForExpiredItem(@NonNull SQLiteDatabase db,
2250             @NonNull String path, long id, long expiredTime) {
2251         final String table = "files";
2252         final String whereClause = MediaColumns._ID + "=?";
2253         final String[] whereArgs = new String[]{String.valueOf(id)};
2254         final ContentValues values = new ContentValues();
2255         values.put(FileColumns.DATA, path);
2256         values.put(FileColumns.DATE_EXPIRES, expiredTime);
2257         final int count = db.update(table, values, whereClause, whereArgs);
2258         return count == 1;
2259     }
2260 
renameInLowerFsAndInvalidateFuseDentry(@onNull String originalPath, @NonNull String newPath)2261     private boolean renameInLowerFsAndInvalidateFuseDentry(@NonNull String originalPath,
2262             @NonNull String newPath) {
2263         try {
2264             Os.rename(originalPath, newPath);
2265             invalidateFuseDentry(originalPath);
2266             invalidateFuseDentry(newPath);
2267             return true;
2268         } catch (ErrnoException e) {
2269             final String errorMessage = "Rename " + originalPath + " to " + newPath
2270                     + " in lower file system for extending item failed.";
2271             Log.e(TAG, errorMessage, e);
2272         }
2273         return false;
2274     }
2275 
onIdleMaintenanceStopped()2276     public void onIdleMaintenanceStopped() {
2277         mMediaScanner.onIdleScanStopped();
2278     }
2279 
2280     /**
2281      * Orphan any content of the given package. This will delete Android/media orphaned files from
2282      * the database.
2283      */
onPackageOrphaned(String packageName, int uid)2284     public void onPackageOrphaned(String packageName, int uid) {
2285         mExternalDatabase.runWithTransaction((db) -> {
2286             final int userId = uid / PER_USER_RANGE;
2287             onPackageOrphaned(db, packageName, userId);
2288 
2289             if (SdkLevel.isAtLeastU()) {
2290                 removeAllMediaGrantsForUid(uid, userId, packageName);
2291             }
2292             return null;
2293         });
2294     }
2295 
2296     /**
2297      * Orphan any content of the given package from the given database. This will delete
2298      * Android/media files from the database if the underlying file no longer exists.
2299      */
onPackageOrphaned(@onNull SQLiteDatabase db, @NonNull String packageName, int userId)2300     public void onPackageOrphaned(@NonNull SQLiteDatabase db,
2301             @NonNull String packageName, int userId) {
2302         // Delete Android/media entries.
2303         deleteAndroidMediaEntriesAndInvalidateDentryCache(db, packageName, userId);
2304         // Orphan rest of entries.
2305         orphanEntries(db, packageName, userId);
2306         mDatabaseBackupAndRecovery.removeOwnerIdToPackageRelation(packageName, userId);
2307 
2308     }
2309 
2310     /**
2311      * Removes all media_grants for all packages with the given UID. (i.e. shared packages.)
2312      *
2313      * @param uid the package uid. (will use this to query all shared packages that use this uid)
2314      * @param userId the user id, since packages can be installed by multiple users.
2315      * @param additionalPackageName An optional additional package name in the event that the
2316      *     package has been removed at won't be returned by the PackageManager APIs.
2317      */
removeAllMediaGrantsForUid( int uid, int userId, @Nullable String additionalPackageName)2318     private void removeAllMediaGrantsForUid(
2319             int uid, int userId, @Nullable String additionalPackageName) {
2320 
2321         String[] packages;
2322         try {
2323             LocalCallingIdentity lci =
2324                     LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid);
2325             packages = lci.getSharedPackageNamesArray();
2326         } catch (IllegalArgumentException notFound) {
2327             // If there are no packages found, this means the specified UID has no packages
2328             // remaining on the system.
2329             packages = new String[]{};
2330         }
2331         if (additionalPackageName != null) {
2332             // Include the passed additional package in the list LocalCallingIdentity returns.
2333             List<String> packageList = new ArrayList<>();
2334             packageList.addAll(Arrays.asList(packages));
2335             packageList.add(additionalPackageName);
2336             packages = packageList.toArray(new String[packageList.size()]);
2337         }
2338 
2339         // TODO(b/260685885): Add e2e tests to ensure these are cleared when a package
2340         // is removed.
2341         mMediaGrants.removeAllMediaGrantsForPackages(
2342                 packages, /* reason */ "Package orphaned", userId);
2343     }
2344 
deleteAndroidMediaEntriesAndInvalidateDentryCache(SQLiteDatabase db, String packageName, int userId)2345     private void deleteAndroidMediaEntriesAndInvalidateDentryCache(SQLiteDatabase db,
2346             String packageName, int userId) {
2347         String relativePath = "Android/media/" + DatabaseUtils.escapeForLike(packageName) + "/%";
2348         try (Cursor cursor = db.query(
2349                 "files",
2350                 new String[] { MediaColumns._ID, MediaColumns.DATA },
2351                 "relative_path LIKE ? ESCAPE '\\' AND owner_package_name=? AND _user_id=?",
2352                 new String[] { relativePath, packageName, "" + userId },
2353                 /* groupBy= */ null,
2354                 /* having= */ null,
2355                 /* orderBy= */null,
2356                 /* limit= */ null)) {
2357             int countDeleted = 0;
2358             if (cursor != null) {
2359                 while (cursor.moveToNext()) {
2360                     File file = new File(cursor.getString(1));
2361                     // We check for existence to be sure we don't delete files that still exist.
2362                     // This can happen even if the pair (package, userid) is unknown,
2363                     // since some framework implementations may rely on special userids.
2364                     if (!file.exists()) {
2365                         countDeleted +=
2366                                 db.delete("files", "_id=?", new String[]{cursor.getString(0)});
2367                     }
2368                 }
2369             }
2370             Log.d(TAG, "Deleted " + countDeleted + " Android/media items belonging to "
2371                     + packageName + " on " + db.getPath());
2372         }
2373 
2374         // Invalidate Dentry cache for Android/media/<package-name> directories
2375         invalidateDentryForExternalStorage(packageName);
2376     }
2377 
orphanEntries( @onNull SQLiteDatabase db, @NonNull String packageName, int userId)2378     private void orphanEntries(
2379             @NonNull SQLiteDatabase db, @NonNull String packageName, int userId) {
2380         final ContentValues values = new ContentValues();
2381         values.putNull(FileColumns.OWNER_PACKAGE_NAME);
2382 
2383         final int countOrphaned = db.update("files", values,
2384                 "owner_package_name=? AND _user_id=?", new String[] { packageName, "" + userId });
2385         if (countOrphaned > 0) {
2386             Log.d(TAG, "Orphaned " + countOrphaned + " items belonging to "
2387                     + packageName + " on " + db.getPath());
2388         }
2389     }
2390 
scanDirectory(@onNull File dir, @ScanReason int reason)2391     public void scanDirectory(@NonNull File dir, @ScanReason int reason) {
2392         mMediaScanner.scanDirectory(dir, reason);
2393     }
2394 
scanFile(@onNull File file, @ScanReason int reason)2395     public Uri scanFile(@NonNull File file, @ScanReason int reason) {
2396         return mMediaScanner.scanFile(file, reason);
2397     }
2398 
scanFileAsMediaProvider(File file)2399     private Uri scanFileAsMediaProvider(File file) {
2400         final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
2401         try {
2402             return scanFile(file, REASON_DEMAND);
2403         } finally {
2404             restoreLocalCallingIdentity(tokenInner);
2405         }
2406     }
2407 
2408     /**
2409      * Called when a new file is created through FUSE
2410      *
2411      * @param path path of the file that was created
2412      *
2413      * Called from JNI in jni/MediaProviderWrapper.cpp
2414      */
2415     @Keep
onFileCreatedForFuse(String path)2416     public void onFileCreatedForFuse(String path) {
2417         // Make sure we update the quota type of the file
2418         BackgroundThread.getExecutor().execute(() -> {
2419             File file = new File(path);
2420             int mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
2421             updateQuotaTypeForFileInternal(file, mediaType);
2422         });
2423     }
2424 
isAppCloneUserPair(int userId1, int userId2)2425     private boolean isAppCloneUserPair(int userId1, int userId2) {
2426         UserHandle user1 = UserHandle.of(userId1);
2427         UserHandle user2 = UserHandle.of(userId2);
2428         if (SdkLevel.isAtLeastS()) {
2429             if (mUserCache.userSharesMediaWithParent(user1)
2430                     || mUserCache.userSharesMediaWithParent(user2)) {
2431                 return true;
2432             }
2433             if (Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.S) {
2434                 // If we're on S or higher, and we shipped with S or higher, only allow the new
2435                 // app cloning functionality
2436                 return false;
2437             }
2438             // else, fall back to deprecated solution below on updating devices
2439         }
2440         try {
2441             Method isAppCloneUserPair = StorageManager.class.getMethod("isAppCloneUserPair",
2442                 int.class, int.class);
2443             return (Boolean) isAppCloneUserPair.invoke(mStorageManager, userId1, userId2);
2444         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
2445             Log.w(TAG, "isAppCloneUserPair failed. Users: " + userId1 + " and " + userId2);
2446             return false;
2447         }
2448     }
2449 
2450     /**
2451      * Determines whether the passed in userId forms an app clone user pair with user 0.
2452      *
2453      * @param userId user ID to check
2454      *
2455      * Called from JNI in jni/MediaProviderWrapper.cpp
2456      */
2457     @Keep
isAppCloneUserForFuse(int userId)2458     public boolean isAppCloneUserForFuse(int userId) {
2459         if (!isCrossUserEnabled()) {
2460             Log.d(TAG, "CrossUser not enabled.");
2461             return false;
2462         }
2463         boolean result = isAppCloneUserPair(0, userId);
2464 
2465         Log.w(TAG, "isAppCloneUserPair for user " + userId + ": " + result);
2466 
2467         return result;
2468     }
2469 
2470     /**
2471      * Determines if to allow FUSE_LOOKUP for uid. Might allow uids that don't belong to the
2472      * MediaProvider user, depending on OEM configuration.
2473      *
2474      * @param uid linux uid to check
2475      *
2476      * Called from JNI in jni/MediaProviderWrapper.cpp
2477      */
2478     @Keep
shouldAllowLookupForFuse(int uid, int pathUserId)2479     public boolean shouldAllowLookupForFuse(int uid, int pathUserId) {
2480         int callingUserId = uidToUserId(uid);
2481         if (!isCrossUserEnabled()) {
2482             Log.d(TAG, "CrossUser not enabled. Users: " + callingUserId + " and " + pathUserId);
2483             return false;
2484         }
2485 
2486         if (callingUserId != pathUserId && callingUserId != 0 && pathUserId != 0) {
2487             Log.w(TAG, "CrossUser at least one user is 0 check failed. Users: " + callingUserId
2488                     + " and " + pathUserId);
2489             return false;
2490         }
2491 
2492         if (mUserCache.isWorkProfile(callingUserId) || mUserCache.isWorkProfile(pathUserId)) {
2493             // Cross-user lookup not allowed if one user in the pair has a profile owner app
2494             Log.w(TAG, "CrossUser work profile check failed. Users: " + callingUserId + " and "
2495                     + pathUserId);
2496             return false;
2497         }
2498 
2499         boolean result = isAppCloneUserPair(pathUserId, callingUserId);
2500         if (result) {
2501             Log.i(TAG, "CrossUser allowed. Users: " + callingUserId + " and " + pathUserId);
2502         } else {
2503             Log.w(TAG, "CrossUser isAppCloneUserPair check failed. Users: " + callingUserId
2504                     + " and " + pathUserId);
2505         }
2506 
2507         return result;
2508     }
2509 
2510     /**
2511      * Called from FUSE to transform a file
2512      *
2513      * A transform can change the file contents for {@code uid} from {@code src} to {@code dst}
2514      * depending on {@code flags}. This allows the FUSE daemon serve different file contents for
2515      * the same file to different apps.
2516      *
2517      * The only supported transform for now is transcoding which re-encodes a file taken in a modern
2518      * format like HEVC to a legacy format like AVC.
2519      *
2520      * @param src file path to transform
2521      * @param dst file path to save transformed file
2522      * @param flags determines the kind of transform
2523      * @param readUid app that called us requesting transform
2524      * @param openUid app that originally made the open call
2525      * @param mediaCapabilitiesUid app for which the transform decision was made,
2526      *                             0 if decision was made with openUid
2527      *
2528      * Called from JNI in jni/MediaProviderWrapper.cpp
2529      */
2530     @Keep
transformForFuse(String src, String dst, int transforms, int transformsReason, int readUid, int openUid, int mediaCapabilitiesUid)2531     public boolean transformForFuse(String src, String dst, int transforms, int transformsReason,
2532             int readUid, int openUid, int mediaCapabilitiesUid) {
2533         if ((transforms & FLAG_TRANSFORM_TRANSCODING) != 0) {
2534             if (mTranscodeHelper.isTranscodeFileCached(src, dst)) {
2535                 Log.d(TAG, "Using transcode cache for " + src);
2536                 return true;
2537             }
2538 
2539             // In general we always mark the opener as causing transcoding.
2540             // However, if the mediaCapabilitiesUid is available then we mark the reader as causing
2541             // transcoding.  This handles the case where a malicious app might want to take
2542             // advantage of mediaCapabilitiesUid by setting it to another app's uid and reading the
2543             // media contents itself; in such cases we'd mark the reader (malicious app) for the
2544             // cost of transcoding.
2545             //
2546             //                     openUid             readUid                mediaCapabilitiesUid
2547             // -------------------------------------------------------------------------------------
2548             // using picker         SAF                 app                           app
2549             // abusive case        bad app             bad app                       victim
2550             // modern to lega-
2551             // -cy sharing         modern              legacy                        legacy
2552             //
2553             // we'd not be here in the below case.
2554             // legacy to mode-
2555             // -rn sharing         legacy              modern                        modern
2556 
2557             int transcodeUid = openUid;
2558             if (mediaCapabilitiesUid > 0) {
2559                 Log.d(TAG, "Fix up transcodeUid to " + readUid + ". openUid " + openUid
2560                         + ", mediaCapabilitiesUid " + mediaCapabilitiesUid);
2561                 transcodeUid = readUid;
2562             }
2563             return mTranscodeHelper.transcode(src, dst, transcodeUid, transformsReason);
2564         }
2565         return true;
2566     }
2567 
2568     /**
2569      * Called from FUSE to get {@link FileLookupResult} for a {@code path} and {@code uid}
2570      *
2571      * {@link FileLookupResult} contains transforms, transforms completion status and ioPath
2572      * for transform lookup query for a file and uid.
2573      *
2574      * @param path file path to get transforms for
2575      * @param uid app requesting IO form kernel
2576      * @param tid FUSE thread id handling IO request from kernel
2577      *
2578      * Called from JNI in jni/MediaProviderWrapper.cpp
2579      */
2580     @Keep
onFileLookupForFuse(String path, int uid, int tid)2581     public FileLookupResult onFileLookupForFuse(String path, int uid, int tid) {
2582         uid = getBinderUidForFuse(uid, tid);
2583         // Use MediaProviders UserId as the caller might be calling cross profile.
2584         final int userId = UserHandle.myUserId();
2585 
2586         if (isSyntheticPath(path, userId)) {
2587             if (isRedactedPath(path, userId)) {
2588                 return handleRedactedFileLookup(uid, path);
2589             } else if (isPickerPath(path, userId)) {
2590                 return handlePickerFileLookup(userId, uid, path);
2591             }
2592 
2593             throw new IllegalStateException("Unexpected synthetic path: " + path);
2594         }
2595 
2596         if (mTranscodeHelper.supportsTranscode(path)) {
2597             return handleTranscodedFileLookup(path, uid, tid);
2598         }
2599 
2600         return new FileLookupResult(/* transforms */ 0, uid, /* ioPath */ "");
2601     }
2602 
handleTranscodedFileLookup(String path, int uid, int tid)2603     private FileLookupResult handleTranscodedFileLookup(String path, int uid, int tid) {
2604         final int transformsReason;
2605         final PendingOpenInfo info;
2606 
2607         synchronized (mPendingOpenInfo) {
2608             info = mPendingOpenInfo.get(tid);
2609         }
2610 
2611         if (info != null && info.uid == uid) {
2612             transformsReason = info.transcodeReason;
2613         } else {
2614             transformsReason = mTranscodeHelper.shouldTranscode(path, uid, null /* bundle */);
2615         }
2616 
2617         if (transformsReason > 0) {
2618             final String ioPath = mTranscodeHelper.prepareIoPath(path, uid);
2619             final boolean transformsComplete = mTranscodeHelper.isTranscodeFileCached(path, ioPath);
2620 
2621             return new FileLookupResult(FLAG_TRANSFORM_TRANSCODING, transformsReason, uid,
2622                     transformsComplete, /* transformsSupported */ true, ioPath);
2623         }
2624 
2625         return new FileLookupResult(/* transforms */ 0, transformsReason, uid,
2626                 /* transformsComplete */ true, /* transformsSupported */ true, "");
2627     }
2628 
handleRedactedFileLookup(int uid, @NonNull String path)2629     private FileLookupResult handleRedactedFileLookup(int uid, @NonNull String path) {
2630         final LocalCallingIdentity token = clearLocalCallingIdentity();
2631         final String fileName = extractFileName(path);
2632 
2633         final DatabaseHelper helper;
2634         try {
2635             helper = getDatabaseForUri(FileUtils.getContentUriForPath(path));
2636         } catch (VolumeNotFoundException e) {
2637             throw new IllegalStateException("Volume not found for file: " + path);
2638         }
2639 
2640         try (final Cursor c = helper.runWithoutTransaction(
2641                 (db) -> db.query("files", new String[]{MediaColumns.DATA},
2642                         FileColumns.REDACTED_URI_ID + "=?", new String[]{fileName}, null, null,
2643                         null))) {
2644             if (c.moveToFirst()) {
2645                 return new FileLookupResult(FLAG_TRANSFORM_REDACTION, uid, c.getString(0));
2646             }
2647 
2648             throw new IllegalStateException("Failed to fetch synthetic redacted path: " + path);
2649         } finally {
2650             restoreLocalCallingIdentity(token);
2651         }
2652     }
2653 
2654     /** TODO(b/242153950) :Add negative tests for permission check of file lookup of synthetic
2655      * paths. */
handlePickerFileLookup(int userId, int uid, @NonNull String path)2656     private FileLookupResult handlePickerFileLookup(int userId, int uid, @NonNull String path) {
2657         final File file = new File(path);
2658         final List<String> syntheticRelativePathSegments =
2659                 extractSyntheticRelativePathSegements(path, userId);
2660         final int segmentCount = syntheticRelativePathSegments.size();
2661 
2662         if (segmentCount < 1 || segmentCount > 5) {
2663             throw new IllegalStateException("Unexpected synthetic picker path: " + file);
2664         }
2665 
2666         final String lastSegment = syntheticRelativePathSegments.get(segmentCount - 1);
2667 
2668         boolean result = false;
2669         switch (segmentCount) {
2670             case 1:
2671                 // .../picker or .../picker_get_content or .../picker_transcoded
2672                 if (lastSegment.equals(PICKER_SEGMENT)
2673                         || lastSegment.equals(PICKER_GET_CONTENT_SEGMENT)
2674                         || lastSegment.equals(PICKER_TRANSCODED_SEGMENT)) {
2675                     result = file.exists() || file.mkdir();
2676                 }
2677                 break;
2678             case 2:
2679                 // .../picker/<user-id> or .../picker_get_content/<user-id> or
2680                 // .../picker_transcoded/<user-id>
2681                 try {
2682                     Integer.parseInt(lastSegment);
2683                     result = file.exists() || file.mkdir();
2684                 } catch (NumberFormatException e) {
2685                     Log.w(TAG, "Invalid user id for picker file lookup: " + lastSegment
2686                             + ". File: " + file);
2687                 }
2688                 break;
2689             case 3:
2690                 // .../picker/<user-id>/<authority> or
2691                 // .../picker_get_content/<user-id>/<authority> or
2692                 // .../picker_transcoded/<user-id>/<authority>
2693                 result = preparePickerAuthorityPathSegment(file, lastSegment, uid);
2694                 break;
2695             case 4:
2696                 // .../picker/<user-id>/<authority>/media or
2697                 // .../picker_get_content/<user-id>/<authority>/media or
2698                 // .../picker_transcoded/<user-id>/<authority>/media
2699                 if (lastSegment.equals("media")) {
2700                     result = file.exists() || file.mkdir();
2701                 }
2702                 break;
2703             case 5:
2704                 // .../picker/<user-id>/<authority>/media/<media-id.extension> or
2705                 // .../picker_get_content/<user-id>/<authority>/media/<media-id.extension> or
2706                 // .../picker_transcoded/<user-id>/<authority>/media/<media-id.extension>
2707                 final String pickerSegmentType = syntheticRelativePathSegments.get(0);
2708                 final String fileUserId = syntheticRelativePathSegments.get(1);
2709                 final String authority = syntheticRelativePathSegments.get(2);
2710                 result = preparePickerMediaIdPathSegment(file, pickerSegmentType, authority,
2711                         lastSegment, fileUserId, uid);
2712                 break;
2713         }
2714 
2715         if (result) {
2716             return new FileLookupResult(FLAG_TRANSFORM_PICKER, uid, path);
2717         }
2718         throw new IllegalStateException("Failed to prepare synthetic picker path: " + file);
2719     }
2720 
handlePickerFileOpen(String path, int uid)2721     private FileOpenResult handlePickerFileOpen(String path, int uid) {
2722         final String[] segments = path.split("/");
2723         if (segments.length != 11) {
2724             Log.e(TAG, "Picker file open failed. Unexpected segments: " + path);
2725             return new FileOpenResult(OsConstants.ENOENT /* status */, uid, /* transformsUid */ 0,
2726                     new long[0]);
2727         }
2728 
2729         // ['', 'storage', 'emulated', '0', 'transforms', 'synthetic',
2730         // 'picker' or 'picker_get_content' or 'picker_transcoded',
2731         // '<user-id>', '<host>', 'media', '<fileName>']
2732         final String pickerSegmentType = segments[6];
2733         final String userId = segments[7];
2734         final String fileName = segments[10];
2735         final String host = segments[8];
2736         final String authority = userId + "@" + host;
2737         final int lastDotIndex = fileName.lastIndexOf('.');
2738 
2739         if (lastDotIndex == -1) {
2740             Log.e(TAG, "Picker file open failed. No file extension: " + path);
2741             return FileOpenResult.createError(OsConstants.ENOENT, uid);
2742         }
2743 
2744         final String mediaId = fileName.substring(0, lastDotIndex);
2745         final ParcelFileDescriptor pfd;
2746         if (pickerSegmentType.equalsIgnoreCase(PICKER_TRANSCODED_SEGMENT)) {
2747             try {
2748                 pfd = mPhotoPickerTranscodeHelper.openTranscodedFile(host, mediaId);
2749             } catch (FileNotFoundException e) {
2750                 Log.e(TAG, "Picker transcoded file open failed. No cached transcoded file found.");
2751                 return FileOpenResult.createError(OsConstants.ENOENT, uid);
2752             }
2753         } else {
2754             final Uri uri = getMediaUri(authority).buildUpon().appendPath(mediaId).build();
2755             IBinder binder = getContext().getContentResolver()
2756                     .call(uri, METHOD_GET_ASYNC_CONTENT_PROVIDER, null, null)
2757                     .getBinder(EXTRA_ASYNC_CONTENT_PROVIDER);
2758             if (binder == null) {
2759                 Log.e(TAG, "Picker file open failed. No cloud media provider found.");
2760                 return FileOpenResult.createError(OsConstants.ENOENT, uid);
2761             }
2762             IAsyncContentProvider iAsyncProvider = IAsyncContentProvider.Stub.asInterface(binder);
2763             AsyncContentProvider asyncContentProvider = new AsyncContentProvider(iAsyncProvider);
2764             try {
2765                 pfd = asyncContentProvider.openMedia(uri, "r");
2766             } catch (FileNotFoundException | ExecutionException | InterruptedException
2767                      | TimeoutException | RemoteException e) {
2768                 Log.e(TAG, "Picker file open failed. Failed to open URI: " + uri, e);
2769                 return FileOpenResult.createError(OsConstants.ENOENT, uid);
2770             }
2771         }
2772 
2773         try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {
2774             final String mimeType = MimeUtils.resolveMimeType(new File(path));
2775             // Picker segment indicates we need to force redact location metadata.
2776             // Picker_get_content indicates that we need to check A_M_L permission to decide if the
2777             // metadata needs to be redacted
2778             LocalCallingIdentity callingIdentityForOriginalUid = getCachedCallingIdentityForFuse(
2779                     uid);
2780             final boolean isRedactionNeeded = pickerSegmentType.equalsIgnoreCase(PICKER_SEGMENT)
2781                     || pickerSegmentType.equalsIgnoreCase(PICKER_TRANSCODED_SEGMENT)
2782                     || callingIdentityForOriginalUid == null
2783                     || isRedactionNeededForPickerUri(callingIdentityForOriginalUid);
2784             Log.v(TAG, "Redaction needed for file open: " + isRedactionNeeded);
2785             long[] redactionRanges = new long[0];
2786             if (isRedactionNeeded) {
2787                 redactionRanges = RedactionUtils.getRedactionRanges(fis, mimeType);
2788                 Log.v(TAG, "Redaction ranges: " + Arrays.toString(redactionRanges));
2789             }
2790             return new FileOpenResult(0 /* status */, uid, /* transformsUid */ 0,
2791                     /* nativeFd */ pfd.detachFd(), redactionRanges);
2792         } catch (IOException e) {
2793             Log.e(TAG, "Picker file open failed. No file extension: " + path, e);
2794             return FileOpenResult.createError(OsConstants.ENOENT, uid);
2795         }
2796     }
2797 
preparePickerAuthorityPathSegment(File file, String authority, int uid)2798     private boolean preparePickerAuthorityPathSegment(File file, String authority, int uid) {
2799         if (mPickerSyncController.isProviderEnabled(authority)) {
2800             return file.exists() || file.mkdir();
2801         }
2802 
2803         return false;
2804     }
2805 
preparePickerMediaIdPathSegment(File file, String pickerSegmentType, String authority, String fileName, String userId, int uid)2806     private boolean preparePickerMediaIdPathSegment(File file, String pickerSegmentType,
2807             String authority, String fileName, String userId, int uid) {
2808         final String mediaId = extractFileName(fileName);
2809         final String[] projection = new String[]{MediaStore.PickerMediaColumns.SIZE};
2810 
2811         final Uri uri = Uri.parse(
2812                 "content://media/" + pickerSegmentType + "/" + userId + "/" + authority + "/media/"
2813                         + mediaId);
2814         try (Cursor cursor = mPickerUriResolver.query(uri, projection, /* callingPid */0, uid,
2815                 mCallingIdentity.get().getPackageName())) {
2816             if (cursor != null && cursor.moveToFirst()) {
2817                 // For picker transcoded files, get their actual size, as ths value may differ from
2818                 // the source file. The code is put after the query operation to make sure that
2819                 // the app accessing the file have required permissions.
2820                 if (pickerSegmentType.equalsIgnoreCase(PICKER_TRANSCODED_SEGMENT)) {
2821                     long size = mPhotoPickerTranscodeHelper.getTranscodedFileSize(authority,
2822                             mediaId);
2823                     if (size > 0) {
2824                         return createSparseFile(file, size);
2825                     }
2826                     return false;
2827                 }
2828 
2829                 final int sizeBytesIdx = cursor.getColumnIndex(MediaStore.PickerMediaColumns.SIZE);
2830                 if (sizeBytesIdx != -1) {
2831 
2832                     return createSparseFile(file, cursor.getLong(sizeBytesIdx));
2833                 }
2834             }
2835         }
2836 
2837         return false;
2838     }
2839 
getBinderUidForFuse(int uid, int tid)2840     public int getBinderUidForFuse(int uid, int tid) {
2841         if (uid != MY_UID) {
2842             return uid;
2843         }
2844 
2845         synchronized (mPendingOpenInfo) {
2846             PendingOpenInfo info = mPendingOpenInfo.get(tid);
2847             if (info == null) {
2848                 return uid;
2849             }
2850             return info.uid;
2851         }
2852     }
2853 
uidToUserId(int uid)2854     private static int uidToUserId(int uid) {
2855         return uid / PER_USER_RANGE;
2856     }
2857 
2858     /**
2859      * Returns true if the app denoted by the given {@code uid} and {@code packageName} is allowed
2860      * to clear other apps' cache directories.
2861      */
hasPermissionToClearCaches(Context context, ApplicationInfo ai)2862     static boolean hasPermissionToClearCaches(Context context, ApplicationInfo ai) {
2863         PermissionUtils.setOpDescription("clear app cache");
2864         try {
2865             return PermissionUtils.checkPermissionManager(context, /* pid */ -1, ai.uid,
2866                     ai.packageName, /* attributionTag */ null);
2867         } finally {
2868             PermissionUtils.clearOpDescription();
2869         }
2870     }
2871 
2872     @VisibleForTesting
computeAudioLocalizedValues(ContentValues values)2873     void computeAudioLocalizedValues(ContentValues values) {
2874         try {
2875             final String title = values.getAsString(AudioColumns.TITLE);
2876             final String titleRes = values.getAsString(AudioColumns.TITLE_RESOURCE_URI);
2877 
2878             if (!TextUtils.isEmpty(titleRes)) {
2879                 final String localized = getLocalizedTitle(titleRes);
2880                 if (!TextUtils.isEmpty(localized)) {
2881                     values.put(AudioColumns.TITLE, localized);
2882                 }
2883             } else {
2884                 final String localized = getLocalizedTitle(title);
2885                 if (!TextUtils.isEmpty(localized)) {
2886                     values.put(AudioColumns.TITLE, localized);
2887                     values.put(AudioColumns.TITLE_RESOURCE_URI, title);
2888                 }
2889             }
2890         } catch (Exception e) {
2891             Log.w(TAG, "Failed to localize title", e);
2892         }
2893     }
2894 
2895     @VisibleForTesting
computeAudioKeyValues(ContentValues values)2896     static void computeAudioKeyValues(ContentValues values) {
2897         computeAudioKeyValue(values, AudioColumns.TITLE, AudioColumns.TITLE_KEY, /* focusId */
2898                 null, /* hashValue */ 0);
2899         computeAudioKeyValue(values, AudioColumns.ARTIST, AudioColumns.ARTIST_KEY,
2900                 AudioColumns.ARTIST_ID, /* hashValue */ 0);
2901         computeAudioKeyValue(values, AudioColumns.GENRE, AudioColumns.GENRE_KEY,
2902                 AudioColumns.GENRE_ID, /* hashValue */ 0);
2903         computeAudioAlbumKeyValue(values);
2904     }
2905 
2906     /**
2907      * To distinguish same-named albums, we append a hash. The hash is
2908      * based on the "album artist" tag if present, otherwise on the path of
2909      * the parent directory of the audio file.
2910      */
computeAudioAlbumKeyValue(ContentValues values)2911     private static void computeAudioAlbumKeyValue(ContentValues values) {
2912         int hashCode = 0;
2913 
2914         final String albumArtist = values.getAsString(MediaColumns.ALBUM_ARTIST);
2915         if (!TextUtils.isEmpty(albumArtist)) {
2916             hashCode = albumArtist.hashCode();
2917         } else {
2918             final String path = values.getAsString(MediaColumns.DATA);
2919             if (!TextUtils.isEmpty(path)) {
2920                 hashCode = path.substring(0, path.lastIndexOf('/')).hashCode();
2921             }
2922         }
2923 
2924         computeAudioKeyValue(values, AudioColumns.ALBUM, AudioColumns.ALBUM_KEY,
2925                 AudioColumns.ALBUM_ID, hashCode);
2926     }
2927 
computeAudioKeyValue(@onNull ContentValues values, @NonNull String focus, @Nullable String focusKey, @Nullable String focusId, int hashValue)2928     private static void computeAudioKeyValue(@NonNull ContentValues values, @NonNull String focus,
2929             @Nullable String focusKey, @Nullable String focusId, int hashValue) {
2930         if (focusKey != null) values.remove(focusKey);
2931         if (focusId != null) values.remove(focusId);
2932 
2933         final String value = values.getAsString(focus);
2934         if (TextUtils.isEmpty(value)) return;
2935 
2936         final String key = Audio.keyFor(value);
2937         if (key == null) return;
2938 
2939         if (focusKey != null) {
2940             values.put(focusKey, key);
2941         }
2942         if (focusId != null) {
2943             // Many apps break if we generate negative IDs, so trim off the
2944             // highest bit to ensure we're always unsigned
2945             final long id = Hashing.farmHashFingerprint64().hashString(key + hashValue,
2946                     StandardCharsets.UTF_8).asLong() & ~(1L << 63);
2947             values.put(focusId, id);
2948         }
2949     }
2950 
2951     @Override
canonicalize(@onNull Uri uri)2952     public Uri canonicalize(@NonNull Uri uri) {
2953         // Skip when we have nothing to canonicalize
2954         if ("1".equals(uri.getQueryParameter(CANONICAL))) {
2955             return uri;
2956         }
2957 
2958         final boolean allowHidden = mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF);
2959         final int match = matchUri(uri, allowHidden);
2960 
2961         try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
2962             switch (match) {
2963                 case AUDIO_MEDIA_ID: {
2964                     final String title = getDefaultTitleFromCursor(c);
2965                     if (!TextUtils.isEmpty(title)) {
2966                         final Uri.Builder builder = uri.buildUpon();
2967                         builder.appendQueryParameter(AudioColumns.TITLE, title);
2968                         builder.appendQueryParameter(CANONICAL, "1");
2969                         return builder.build();
2970                     }
2971                     break;
2972                 }
2973                 case VIDEO_MEDIA_ID:
2974                 case IMAGES_MEDIA_ID: {
2975                     final String documentId = c
2976                             .getString(c.getColumnIndexOrThrow(MediaColumns.DOCUMENT_ID));
2977                     if (!TextUtils.isEmpty(documentId)) {
2978                         final Uri.Builder builder = uri.buildUpon();
2979                         builder.appendQueryParameter(MediaColumns.DOCUMENT_ID, documentId);
2980                         builder.appendQueryParameter(CANONICAL, "1");
2981                         return builder.build();
2982                     }
2983                     break;
2984                 }
2985             }
2986         } catch (FileNotFoundException e) {
2987             Log.w(TAG, e.getMessage());
2988         }
2989         return null;
2990     }
2991 
2992     @Override
uncanonicalize(@onNull Uri uri)2993     public Uri uncanonicalize(@NonNull Uri uri) {
2994         // Skip when we have nothing to uncanonicalize
2995         if (!"1".equals(uri.getQueryParameter(CANONICAL))) {
2996             return uri;
2997         }
2998         final boolean allowHidden = mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF);
2999         final int match = matchUri(uri, allowHidden);
3000 
3001         // Extract values and then clear to avoid recursive lookups
3002         final String title = uri.getQueryParameter(AudioColumns.TITLE);
3003         final String documentId = uri.getQueryParameter(MediaColumns.DOCUMENT_ID);
3004         uri = uri.buildUpon().clearQuery().build();
3005 
3006         switch (match) {
3007             case AUDIO_MEDIA_ID: {
3008                 // First check for an exact match
3009                 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
3010                     if (Objects.equals(title, getDefaultTitleFromCursor(c))) {
3011                         return uri;
3012                     }
3013                 } catch (FileNotFoundException e) {
3014                     Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e);
3015                 }
3016 
3017                 // Otherwise fallback to searching
3018                 final Uri baseUri = ContentUris.removeId(uri);
3019                 try (Cursor c = queryForSingleItem(baseUri,
3020                         new String[] { BaseColumns._ID },
3021                         AudioColumns.TITLE + "=?", new String[] { title }, null)) {
3022                     return ContentUris.withAppendedId(baseUri, c.getLong(0));
3023                 } catch (FileNotFoundException e) {
3024                     Log.w(TAG, "Failed to resolve " + uri + ": " + e);
3025                     return null;
3026                 }
3027             }
3028             case VIDEO_MEDIA_ID:
3029             case IMAGES_MEDIA_ID: {
3030                 // First check for an exact match
3031                 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
3032                     if (Objects.equals(title, getDefaultTitleFromCursor(c))) {
3033                         return uri;
3034                     }
3035                 } catch (FileNotFoundException e) {
3036                     Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e);
3037                 }
3038 
3039                 // Otherwise fallback to searching
3040                 final Uri baseUri = ContentUris.removeId(uri);
3041                 try (Cursor c = queryForSingleItem(baseUri,
3042                         new String[] { BaseColumns._ID },
3043                         MediaColumns.DOCUMENT_ID + "=?", new String[] { documentId }, null)) {
3044                     return ContentUris.withAppendedId(baseUri, c.getLong(0));
3045                 } catch (FileNotFoundException e) {
3046                     Log.w(TAG, "Failed to resolve " + uri + ": " + e);
3047                     return null;
3048                 }
3049             }
3050         }
3051 
3052         return uri;
3053     }
3054 
safeUncanonicalize(Uri uri)3055     private Uri safeUncanonicalize(Uri uri) {
3056         Uri newUri = uncanonicalize(uri);
3057         if (newUri != null) {
3058             return newUri;
3059         }
3060         return uri;
3061     }
3062 
safeTraceSectionNameWithUri(String operation, Uri uri)3063     private static String safeTraceSectionNameWithUri(String operation, Uri uri) {
3064         String sectionName = "MP." + operation + " [" + uri + "]";
3065         if (sectionName.length() > MAX_SECTION_NAME_LEN) {
3066             return sectionName.substring(0, MAX_SECTION_NAME_LEN);
3067         }
3068         return sectionName;
3069     }
3070 
3071     /**
3072      * @return where clause to exclude database rows where
3073      * <ul>
3074      * <li> {@code column} is set or
3075      * <li> {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE and not owned by
3076      * calling package.
3077      * <li> {@code column} is {@link MediaColumns#IS_PENDING}, is unset and is waiting for
3078      * metadata update from a deferred scan.
3079      * </ul>
3080      */
getWhereClauseForMatchExclude(@onNull String column)3081     private String getWhereClauseForMatchExclude(@NonNull String column) {
3082         if (column.equalsIgnoreCase(MediaColumns.IS_PENDING)) {
3083             // Don't include rows that are pending for metadata
3084             final String pendingForMetadata = FileColumns._MODIFIER + "="
3085                     + FileColumns._MODIFIER_CR_PENDING_METADATA;
3086             final String notPending = String.format("(%s=0 AND NOT %s)", column,
3087                     pendingForMetadata);
3088 
3089             // Include owned pending files from Fuse
3090             final String pendingFromFuse = String.format("(%s=1 AND %s AND %s)", column,
3091                     MATCH_PENDING_FROM_FUSE, getWhereForOwnerPackageMatch(mCallingIdentity.get()));
3092 
3093             return "(" + notPending + " OR " + pendingFromFuse + ")";
3094         }
3095         return column + "=0";
3096     }
3097 
3098     /**
3099      * @return where clause to include database rows where
3100      * <ul>
3101      * <li> {@code column} is not set or
3102      * <li> {@code column} is set and calling package has write permission to corresponding db row
3103      *      or {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE.
3104      * </ul>
3105      * The method is used to match db rows corresponding to writable pending and trashed files.
3106      */
3107     @Nullable
getWhereClauseForMatchableVisibleFromFilePath(@onNull Uri uri, @NonNull String column)3108     private String getWhereClauseForMatchableVisibleFromFilePath(@NonNull Uri uri,
3109             @NonNull String column) {
3110         if (checkCallingPermissionGlobal(uri, /*forWrite*/ true)) {
3111             // No special filtering needed
3112             return null;
3113         }
3114 
3115         int uriType = matchUri(uri, isCallingPackageAllowedHidden());
3116         if (hasAccessToCollection(mCallingIdentity.get(), uriType, /* forWrite */ true)) {
3117             // has direct write access to whole collection, no special filtering needed.
3118             return null;
3119         }
3120 
3121         final String writeAccessCheckSql = getWhereForConstrainedAccess(mCallingIdentity.get(),
3122                 uriType, /* forWrite */ true, Bundle.EMPTY);
3123 
3124         final String matchWritableRowsClause = String.format("%s=0 OR (%s=1 AND (%s OR %s))",
3125                 column, column, MATCH_PENDING_FROM_FUSE, writeAccessCheckSql);
3126 
3127         return matchWritableRowsClause;
3128     }
3129 
3130     /**
3131      * Gets list of files in {@code path} from media provider database.
3132      *
3133      * @param path path of the directory.
3134      * @param uid UID of the calling process.
3135      * @return a list of file names in the given directory path.
3136      * An empty list is returned if no files are visible to the calling app or the given directory
3137      * does not have any files.
3138      * A list with ["/"] is returned if the path is not indexed by MediaProvider database or
3139      * calling package is a legacy app and has appropriate storage permissions for the given path.
3140      * In both scenarios file names should be obtained from lower file system.
3141      * A list with empty string[""] is returned if the calling package doesn't have access to the
3142      * given path.
3143      *
3144      * <p>Directory names are always obtained from lower file system.
3145      *
3146      * Called from JNI in jni/MediaProviderWrapper.cpp
3147      */
3148     @Keep
getFilesInDirectoryForFuse(String path, int uid)3149     public String[] getFilesInDirectoryForFuse(String path, int uid) {
3150         final LocalCallingIdentity token =
3151                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
3152         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
3153 
3154         try {
3155             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
3156                 return new String[] {""};
3157             }
3158 
3159             if (shouldBypassFuseRestrictions(/*forWrite*/ false, path)) {
3160                 return new String[] {"/"};
3161             }
3162 
3163             // Do not allow apps to list Android/data or Android/obb dirs.
3164             // On primary volumes, apps that get special access to these directories get it via
3165             // mount views of lowerfs. On secondary volumes, such apps would return early from
3166             // shouldBypassFuseRestrictions above.
3167             if (isDataOrObbPath(path)) {
3168                 return new String[] {""};
3169             }
3170 
3171             // Legacy apps that made is this far don't have the right storage permission and hence
3172             // are not allowed to access anything other than their external app directory
3173             if (isCallingPackageRequestingLegacy()) {
3174                 return new String[] {""};
3175             }
3176 
3177             // Get relative path for the contents of given directory.
3178             String relativePath = extractRelativePathWithDisplayName(path);
3179             if (relativePath == null) {
3180                 // Path is /storage/emulated/, if relativePath is null, MediaProvider doesn't
3181                 // have any details about the given directory. Use lower file system to obtain
3182                 // files and directories in the given directory.
3183                 return new String[] {"/"};
3184             }
3185             // Getting UserId from the directory path, as clone user shares the MediaProvider
3186             // of user 0.
3187             int userIdFromPath = FileUtils.extractUserId(path);
3188             // In some cases, like querying public volumes, userId is not available in path. We
3189             // take userId from the user running MediaProvider process (sUserId).
3190             if (userIdFromPath == -1) {
3191                 userIdFromPath = sUserId;
3192             }
3193             // For all other paths, get file names from media provider database.
3194             // Return media and non-media files visible to the calling package.
3195             ArrayList<String> fileNamesList = new ArrayList<>();
3196 
3197             // Only FileColumns.DATA contains actual name of the file.
3198             String[] projection = {MediaColumns.DATA};
3199 
3200             Bundle queryArgs = new Bundle();
3201             queryArgs.putString(QUERY_ARG_SQL_SELECTION, MediaColumns.RELATIVE_PATH +
3202                     " =? and " + FileColumns._USER_ID + " =? and mime_type not like 'null'");
3203             queryArgs.putStringArray(QUERY_ARG_SQL_SELECTION_ARGS, new String[] {relativePath,
3204                     String.valueOf(userIdFromPath)});
3205             // Get database entries for files from MediaProvider database with
3206             // MediaColumns.RELATIVE_PATH as the given path.
3207             try (final Cursor cursor = query(FileUtils.getContentUriForPath(path), projection,
3208                     queryArgs, null)) {
3209                 while(cursor.moveToNext()) {
3210                     fileNamesList.add(extractDisplayName(cursor.getString(0)));
3211                 }
3212             }
3213             return fileNamesList.toArray(new String[fileNamesList.size()]);
3214         } finally {
3215             restoreLocalCallingIdentity(token);
3216         }
3217     }
3218 
3219     /**
3220      * Scan files during directory renames for the following reasons:
3221      * <ul>
3222      * <li>Because we don't update db rows for directories, we scan the oldPath to discard stale
3223      * directory db rows. This prevents conflicts during subsequent db operations with oldPath.
3224      * <li>We need to scan newPath as well, because the new directory may have become hidden
3225      * or unhidden, in which case we need to update the media types of the contained files
3226      * </ul>
3227      */
scanRenamedDirectoryForFuse(@onNull String oldPath, @NonNull String newPath)3228     private void scanRenamedDirectoryForFuse(@NonNull String oldPath, @NonNull String newPath) {
3229         scanFileAsMediaProvider(new File(oldPath));
3230         scanFileAsMediaProvider(new File(newPath));
3231     }
3232 
3233     /**
3234      * Checks if given {@code mimeType} is supported in {@code path}.
3235      */
isMimeTypeSupportedInPath(String path, String mimeType)3236     private boolean isMimeTypeSupportedInPath(String path, String mimeType) {
3237         final String supportedPrimaryMimeType;
3238         final int match = matchUri(getContentUriForFile(path, mimeType), true);
3239         switch (match) {
3240             case AUDIO_MEDIA:
3241                 supportedPrimaryMimeType = "audio";
3242                 break;
3243             case VIDEO_MEDIA:
3244                 supportedPrimaryMimeType = "video";
3245                 break;
3246             case IMAGES_MEDIA:
3247                 supportedPrimaryMimeType = "image";
3248                 break;
3249             default:
3250                 supportedPrimaryMimeType = ClipDescription.MIMETYPE_UNKNOWN;
3251         }
3252         return (supportedPrimaryMimeType.equalsIgnoreCase(ClipDescription.MIMETYPE_UNKNOWN) ||
3253                 StringUtils.startsWithIgnoreCase(mimeType, supportedPrimaryMimeType));
3254     }
3255 
3256     /**
3257      * Removes owner package for the renamed path if the calling package doesn't own the db row
3258      *
3259      * When oldPath is renamed to newPath, if newPath exists in the database, and caller is not the
3260      * owner of the file, owner package is set to 'null'. This prevents previous owner of newPath
3261      * from accessing renamed file.
3262      * @return {@code true} if
3263      * <ul>
3264      * <li> there is no corresponding database row for given {@code path}
3265      * <li> shared calling package is the owner of the database row
3266      * <li> owner package name is already set to 'null'
3267      * <li> updating owner package name to 'null' was successful.
3268      * </ul>
3269      * Returns {@code false} otherwise.
3270      */
maybeRemoveOwnerPackageForFuseRename(@onNull DatabaseHelper helper, @NonNull String path)3271     private boolean maybeRemoveOwnerPackageForFuseRename(@NonNull DatabaseHelper helper,
3272             @NonNull String path) {
3273         final Uri uri = FileUtils.getContentUriForPath(path);
3274         final int match = matchUri(uri, isCallingPackageAllowedHidden());
3275         final String ownerPackageName;
3276         final String selection = MediaColumns.DATA + " =? AND "
3277                 + MediaColumns.OWNER_PACKAGE_NAME + " != 'null'";
3278         final String[] selectionArgs = new String[] {path};
3279 
3280         final SQLiteQueryBuilder qbForQuery =
3281                 getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, null);
3282         try (Cursor c = qbForQuery.query(helper, new String[] {FileColumns.OWNER_PACKAGE_NAME},
3283                 selection, selectionArgs, null, null, null, null, null)) {
3284             if (!c.moveToFirst()) {
3285                 // We don't need to remove owner_package from db row if path doesn't exist in
3286                 // database or owner_package is already set to 'null'
3287                 return true;
3288             }
3289             ownerPackageName = c.getString(0);
3290             if (isCallingIdentitySharedPackageName(ownerPackageName)) {
3291                 // We don't need to remove owner_package from db row if calling package is the owner
3292                 // of the database row
3293                 return true;
3294             }
3295         }
3296 
3297         final SQLiteQueryBuilder qbForUpdate =
3298                 getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null);
3299         ContentValues values = new ContentValues();
3300         values.put(FileColumns.OWNER_PACKAGE_NAME, "null");
3301         return qbForUpdate.update(helper, values, selection, selectionArgs) == 1;
3302     }
3303 
updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values)3304     private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
3305             @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) {
3306         return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY);
3307     }
3308 
updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, @NonNull Bundle qbExtras)3309     private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
3310             @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values,
3311             @NonNull Bundle qbExtras) {
3312         return updateDatabaseForFuseRename(helper, oldPath, newPath, values, qbExtras,
3313                 FileUtils.getContentUriForPath(oldPath));
3314     }
3315 
3316     /**
3317      * Updates database entry for given {@code path} with {@code values}
3318      */
updateDatabaseForFuseRename(@onNull DatabaseHelper helper, @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, @NonNull Bundle qbExtras, Uri uriOldPath)3319     private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
3320             @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values,
3321             @NonNull Bundle qbExtras, Uri uriOldPath) {
3322         boolean allowHidden = isCallingPackageAllowedHidden();
3323         final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE,
3324                 matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null);
3325 
3326         // uriOldPath may use Files uri which doesn't allow modifying AudioColumns. Include
3327         // AudioColumns projection map if we are modifying any audio columns while renaming
3328         // database rows.
3329         if (values.containsKey(AudioColumns.IS_RINGTONE)) {
3330             qbForUpdate.setProjectionMap(getProjectionMap(AudioColumns.class, FileColumns.class));
3331         }
3332 
3333         if (values.containsKey(FileColumns._MODIFIER)) {
3334             qbForUpdate.allowColumn(FileColumns._MODIFIER);
3335         }
3336 
3337         final String selection = MediaColumns.DATA + " =? ";
3338         int count = 0;
3339         boolean retryUpdateWithReplace = false;
3340 
3341         try {
3342             Long parent = values.getAsLong(FileColumns.PARENT);
3343             // Opening a transaction here and ensuring the qbForUpdate happens within
3344             // doesn't open two transactions, but just joins the existing one
3345             count = helper.runWithTransaction((db) -> {
3346                 if (parent == null && newPath != null) {
3347                     final long parentId = getParent(db, newPath);
3348                     values.put(FileColumns.PARENT, parentId);
3349                 }
3350                 // TODO(b/146777893): System gallery apps can rename a media directory
3351                 // containing non-media files. This update doesn't support updating
3352                 // non-media files that are not owned by system gallery app.
3353                 return qbForUpdate.update(helper, values, selection, new String[]{oldPath});
3354             });
3355         } catch (SQLiteConstraintException e) {
3356             Log.w(TAG, "Database update failed while renaming " + oldPath, e);
3357             retryUpdateWithReplace = true;
3358         }
3359 
3360         if (retryUpdateWithReplace) {
3361             if (deleteForFuseRename(helper, oldPath, newPath, qbExtras, selection, allowHidden)) {
3362                 Log.i(TAG, "Retrying database update after deleting conflicting entry");
3363                 count = qbForUpdate.update(helper, values, selection, new String[]{oldPath});
3364             } else {
3365                 return false;
3366             }
3367         }
3368         return count == 1;
3369     }
3370 
deleteForFuseRename(DatabaseHelper helper, String oldPath, String newPath, Bundle qbExtras, String selection, boolean allowHidden)3371     private boolean deleteForFuseRename(DatabaseHelper helper, String oldPath,
3372             String newPath, Bundle qbExtras, String selection, boolean allowHidden) {
3373         // We are replacing file in newPath with file in oldPath. If calling package has
3374         // write permission for newPath, delete existing database entry and retry update.
3375         final Uri uriNewPath = FileUtils.getContentUriForPath(oldPath);
3376         final SQLiteQueryBuilder qbForDelete = getQueryBuilder(TYPE_DELETE,
3377                 matchUri(uriNewPath, allowHidden), uriNewPath, qbExtras, null);
3378         if (qbForDelete.delete(helper, selection, new String[] {newPath}) == 1) {
3379             return true;
3380         }
3381         // Check if delete can be done using other URI grants
3382         final String[] projection = new String[] {
3383                 FileColumns.MEDIA_TYPE,
3384                 FileColumns.DATA,
3385                 FileColumns._ID,
3386                 FileColumns.IS_DOWNLOAD,
3387                 FileColumns.MIME_TYPE,
3388         };
3389         return
3390             deleteWithOtherUriGrants(
3391                     FileUtils.getContentUriForPath(newPath),
3392                     helper, projection, selection, new String[] {newPath}, qbExtras) == 1;
3393     }
3394 
3395     /**
3396      * Gets {@link ContentValues} for updating database entry to {@code path}.
3397      */
getContentValuesForFuseRename(String path, String newMimeType, boolean wasHidden, boolean isHidden, boolean isSameMimeType)3398     private ContentValues getContentValuesForFuseRename(String path, String newMimeType,
3399             boolean wasHidden, boolean isHidden, boolean isSameMimeType) {
3400         ContentValues values = new ContentValues();
3401         values.put(MediaColumns.MIME_TYPE, newMimeType);
3402         values.put(MediaColumns.DATA, path);
3403 
3404         if (isHidden) {
3405             values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
3406         } else {
3407             int mediaType = MimeUtils.resolveMediaType(newMimeType);
3408             values.put(FileColumns.MEDIA_TYPE, mediaType);
3409         }
3410 
3411         if ((!isHidden && wasHidden) || !isSameMimeType) {
3412             // Set the modifier as MODIFIER_FUSE so that apps can scan the file to update the
3413             // metadata. Otherwise, scan will skip scanning this file because rename() doesn't
3414             // change lastModifiedTime and scan assumes there is no change in the file.
3415             values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE);
3416         }
3417 
3418         if (MimeUtils.isAudioMimeType(newMimeType) && !values.containsKey(FileColumns._MODIFIER)) {
3419             computeAudioLocalizedValues(values);
3420             computeAudioKeyValues(values);
3421             FileUtils.computeAudioTypeValuesFromData(path, values::put);
3422         }
3423 
3424         FileUtils.computeValuesFromData(values, isFuseThread());
3425         return values;
3426     }
3427 
getIncludedDefaultDirectories()3428     private ArrayList<String> getIncludedDefaultDirectories() {
3429         final ArrayList<String> includedDefaultDirs = new ArrayList<>();
3430         if (mCallingIdentity.get().checkCallingPermissionVideo(/* forWrite */
3431                 true, /* forDataDelivery */ true)) {
3432             includedDefaultDirs.add(Environment.DIRECTORY_DCIM);
3433             includedDefaultDirs.add(Environment.DIRECTORY_PICTURES);
3434             includedDefaultDirs.add(Environment.DIRECTORY_MOVIES);
3435         } else if (mCallingIdentity.get().checkCallingPermissionImages(/* forWrite */
3436                 true, /* forDataDelivery */ true)) {
3437             includedDefaultDirs.add(Environment.DIRECTORY_DCIM);
3438             includedDefaultDirs.add(Environment.DIRECTORY_PICTURES);
3439         }
3440         return includedDefaultDirs;
3441     }
3442 
3443     /**
3444      * Gets all files in the given {@code path} and subdirectories of the given {@code path}.
3445      */
getAllFilesForRenameDirectory(String oldPath)3446     private ArrayList<String> getAllFilesForRenameDirectory(String oldPath) {
3447         final String selection = FileColumns.DATA + " LIKE ? ESCAPE '\\'"
3448                 + " and mime_type not like 'null'";
3449         final String[] selectionArgs = new String[] {DatabaseUtils.escapeForLike(oldPath) + "/%"};
3450         ArrayList<String> fileList = new ArrayList<>();
3451 
3452         final LocalCallingIdentity token = clearLocalCallingIdentity();
3453         try (final Cursor c = query(FileUtils.getContentUriForPath(oldPath),
3454                 new String[] {MediaColumns.DATA}, selection, selectionArgs, null)) {
3455             while (c.moveToNext()) {
3456                 String filePath = c.getString(0);
3457                 filePath = filePath.replaceFirst(Pattern.quote(oldPath + "/"), "");
3458                 fileList.add(filePath);
3459             }
3460         } finally {
3461             restoreLocalCallingIdentity(token);
3462         }
3463         return fileList;
3464     }
3465 
3466     /**
3467      * Gets files in the given {@code path} and subdirectories of the given {@code path} for which
3468      * calling package has write permissions.
3469      *
3470      * This method throws {@code IllegalArgumentException} if the directory has one or more
3471      * files for which calling package doesn't have write permission or if file type is not
3472      * supported in {@code newPath}
3473      */
getWritableFilesForRenameDirectory(String oldPath, String newPath)3474     private ArrayList<String> getWritableFilesForRenameDirectory(String oldPath, String newPath)
3475             throws IllegalArgumentException {
3476         // Try a simple check to see if the caller has full access to the given collections first
3477         // before falling back to performing a query to probe for access.
3478         final String oldRelativePath = extractRelativePathWithDisplayName(oldPath);
3479         final String newRelativePath = extractRelativePathWithDisplayName(newPath);
3480         boolean hasFullAccessToOldPath = false;
3481         boolean hasFullAccessToNewPath = false;
3482         for (String defaultDir : getIncludedDefaultDirectories()) {
3483             if (oldRelativePath.startsWith(defaultDir)) hasFullAccessToOldPath = true;
3484             if (newRelativePath.startsWith(defaultDir)) hasFullAccessToNewPath = true;
3485         }
3486         if (hasFullAccessToNewPath && hasFullAccessToOldPath) {
3487             return getAllFilesForRenameDirectory(oldPath);
3488         }
3489 
3490         final int countAllFilesInDirectory;
3491         final String selection = FileColumns.DATA + " LIKE ? ESCAPE '\\'"
3492                 + " and mime_type not like 'null'";
3493         final String[] selectionArgs = new String[] {DatabaseUtils.escapeForLike(oldPath) + "/%"};
3494 
3495         final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath);
3496 
3497         final LocalCallingIdentity token = clearLocalCallingIdentity();
3498         try (final Cursor c = query(uriOldPath, new String[] {MediaColumns._ID}, selection,
3499                 selectionArgs, null)) {
3500             // get actual number of files in the given directory.
3501             countAllFilesInDirectory = c.getCount();
3502         } finally {
3503             restoreLocalCallingIdentity(token);
3504         }
3505 
3506         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE,
3507                 matchUri(uriOldPath, isCallingPackageAllowedHidden()), uriOldPath, Bundle.EMPTY,
3508                 null);
3509         final DatabaseHelper helper;
3510         try {
3511             helper = getDatabaseForUri(uriOldPath);
3512         } catch (VolumeNotFoundException e) {
3513             throw new IllegalStateException("Volume not found while querying files for renaming "
3514                     + oldPath);
3515         }
3516 
3517         ArrayList<String> fileList = new ArrayList<>();
3518         final String[] projection = {MediaColumns.DATA, MediaColumns.MIME_TYPE};
3519         try (Cursor c = qb.query(helper, projection, selection, selectionArgs, null, null, null,
3520                 null, null)) {
3521             // Check if the calling package has write permission to all files in the given
3522             // directory. If calling package has write permission to all files in the directory, the
3523             // query with update uri should return same number of files as previous query.
3524             if (c.getCount() != countAllFilesInDirectory) {
3525                 throw new IllegalArgumentException("Calling package doesn't have write permission "
3526                         + " to rename one or more files in " + oldPath);
3527             }
3528             while(c.moveToNext()) {
3529                 String filePath = c.getString(0);
3530                 filePath = filePath.replaceFirst(Pattern.quote(oldPath + "/"), "");
3531 
3532                 final String mimeType = c.getString(1);
3533                 if (!isMimeTypeSupportedInPath(newPath + "/" + filePath, mimeType)) {
3534                     throw new IllegalArgumentException("Can't rename " + oldPath + "/" + filePath
3535                             + ". Mime type " + mimeType + " not supported in " + newPath);
3536                 }
3537                 fileList.add(filePath);
3538             }
3539         }
3540         return fileList;
3541     }
3542 
renameInLowerFs(String oldPath, String newPath)3543     private int renameInLowerFs(String oldPath, String newPath) {
3544         try {
3545             Os.rename(oldPath, newPath);
3546             return 0;
3547         } catch (ErrnoException e) {
3548             final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed.";
3549             Log.e(TAG, errorMessage, e);
3550             return e.errno;
3551         }
3552     }
3553 
3554     /**
3555      * Rename directory from {@code oldPath} to {@code newPath}.
3556      *
3557      * Renaming a directory is only allowed if calling package has write permission to all files in
3558      * the given directory tree and all file types in the given directory tree are supported by the
3559      * top level directory of new path. Renaming a directory is split into three steps:
3560      * 1. Check calling package's permissions for all files in the given directory tree. Also check
3561      *    file type support for all files in the {@code newPath}.
3562      * 2. Try updating database for all files in the directory.
3563      * 3. Rename the directory in lower file system. If rename in the lower file system is
3564      *    successful, commit database update.
3565      *
3566      * @param oldPath path of the directory to be renamed.
3567      * @param newPath new path of directory to be renamed.
3568      * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed.
3569      * <ul>
3570      * <li>{@link OsConstants#EPERM} Renaming a directory with file types not supported by
3571      * {@code newPath} or renaming a directory with files for which calling package doesn't have
3572      * write permission.
3573      * This method can also return errno returned from {@code Os.rename} function.
3574      */
renameDirectoryCheckedForFuse(String oldPath, String newPath)3575     private int renameDirectoryCheckedForFuse(String oldPath, String newPath) {
3576         final ArrayList<String> fileList;
3577         try {
3578             fileList = getWritableFilesForRenameDirectory(oldPath, newPath);
3579         } catch (IllegalArgumentException e) {
3580             final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. ";
3581             Log.e(TAG, errorMessage, e);
3582             return OsConstants.EPERM;
3583         }
3584 
3585         return renameDirectoryUncheckedForFuse(oldPath, newPath, fileList);
3586     }
3587 
renameDirectoryUncheckedForFuse(String oldPath, String newPath, ArrayList<String> fileList)3588     private int renameDirectoryUncheckedForFuse(String oldPath, String newPath,
3589             ArrayList<String> fileList) {
3590         final DatabaseHelper helper;
3591         try {
3592             helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath));
3593         } catch (VolumeNotFoundException e) {
3594             throw new IllegalStateException("Volume not found while trying to update database for "
3595                     + oldPath, e);
3596         }
3597 
3598         helper.beginTransaction();
3599         try {
3600             final Bundle qbExtras = new Bundle();
3601             qbExtras.putStringArrayList(INCLUDED_DEFAULT_DIRECTORIES,
3602                     getIncludedDefaultDirectories());
3603             final boolean wasHidden = FileUtils.shouldDirBeHidden(new File(oldPath));
3604             final boolean isHidden = FileUtils.shouldDirBeHidden(new File(newPath));
3605             for (String filePath : fileList) {
3606                 final String newFilePath = newPath + "/" + filePath;
3607                 final String mimeType = MimeUtils.resolveMimeType(new File(newFilePath));
3608                 if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath,
3609                         getContentValuesForFuseRename(newFilePath, mimeType, wasHidden, isHidden,
3610                                 /* isSameMimeType */ true),
3611                         qbExtras)) {
3612                     Log.e(TAG, "Calling package doesn't have write permission to rename file.");
3613                     return OsConstants.EPERM;
3614                 }
3615             }
3616 
3617             // Rename the directory in lower file system.
3618             int errno = renameInLowerFs(oldPath, newPath);
3619             if (errno == 0) {
3620                 helper.setTransactionSuccessful();
3621             } else {
3622                 return errno;
3623             }
3624         } finally {
3625             helper.endTransaction();
3626         }
3627         // Directory movement might have made new/old path hidden.
3628         scanRenamedDirectoryForFuse(oldPath, newPath);
3629         return 0;
3630     }
3631 
3632     /**
3633      * Rename a file from {@code oldPath} to {@code newPath}.
3634      *
3635      * Renaming a file is split into three parts:
3636      * 1. Check if {@code newPath} supports new file type.
3637      * 2. Try updating database entry from {@code oldPath} to {@code newPath}. This update may fail
3638      *    if calling package doesn't have write permission for {@code oldPath} and {@code newPath}.
3639      * 3. Rename the file in lower file system. If Rename in lower file system succeeds, commit
3640      *    database update.
3641      * @param oldPath path of the file to be renamed.
3642      * @param newPath new path of the file to be renamed.
3643      * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed.
3644      * <ul>
3645      * <li>{@link OsConstants#EPERM} Calling package doesn't have write permission for
3646      * {@code oldPath} or {@code newPath}, or file type is not supported by {@code newPath}.
3647      * This method can also return errno returned from {@code Os.rename} function.
3648      */
renameFileCheckedForFuse(String oldPath, String newPath)3649     private int renameFileCheckedForFuse(String oldPath, String newPath) {
3650         // Check if new mime type is supported in new path.
3651         final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
3652         if (!isMimeTypeSupportedInPath(newPath, newMimeType)) {
3653             return OsConstants.EPERM;
3654         }
3655         return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ false) ;
3656     }
3657 
renameFileUncheckedForFuse(String oldPath, String newPath)3658     private int renameFileUncheckedForFuse(String oldPath, String newPath) {
3659         return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ true) ;
3660     }
3661 
renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions)3662     private int renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions) {
3663         final DatabaseHelper helper;
3664         try {
3665             helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath));
3666         } catch (VolumeNotFoundException e) {
3667             throw new IllegalStateException("Failed to update database row with " + oldPath, e);
3668         }
3669 
3670         final boolean wasHidden = FileUtils.shouldFileBeHidden(new File(oldPath));
3671         final boolean isHidden = FileUtils.shouldFileBeHidden(new File(newPath));
3672         helper.beginTransaction();
3673         try {
3674             final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
3675             final String oldMimeType = MimeUtils.resolveMimeType(new File(oldPath));
3676             final boolean isSameMimeType = newMimeType.equalsIgnoreCase(oldMimeType);
3677             ContentValues contentValues = getContentValuesForFuseRename(newPath, newMimeType,
3678                     wasHidden, isHidden, isSameMimeType);
3679             if (!updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues)) {
3680                 if (!bypassRestrictions) {
3681                     // Check for other URI format grants for oldPath only. Check right before
3682                     // returning EPERM, to leave positive case performance unaffected.
3683                     if (!renameWithOtherUriGrants(helper, oldPath, newPath, contentValues)) {
3684                         Log.e(TAG, "Calling package doesn't have write permission to rename file.");
3685                         return OsConstants.EPERM;
3686                     }
3687                 } else if (!maybeRemoveOwnerPackageForFuseRename(helper, newPath)) {
3688                     Log.wtf(TAG, "Couldn't clear owner package name for " + newPath);
3689                     return OsConstants.EPERM;
3690                 }
3691             }
3692 
3693             // Try renaming oldPath to newPath in lower file system.
3694             int errno = renameInLowerFs(oldPath, newPath);
3695             if (errno == 0) {
3696                 helper.setTransactionSuccessful();
3697             } else {
3698                 return errno;
3699             }
3700         } finally {
3701             helper.endTransaction();
3702         }
3703         // The above code should have taken are of the mime/media type of the new file,
3704         // even if it was moved to/from a hidden directory.
3705         // This leaves cases where the source/dest of the move is a .nomedia file itself. Eg:
3706         // 1) /sdcard/foo/.nomedia => /sdcard/foo/bar.mp3
3707         //    in this case, the code above has given bar.mp3 the correct mime type, but we should
3708         //    still can /sdcard/foo, because it's now no longer hidden
3709         // 2) /sdcard/foo/.nomedia => /sdcard/bar/.nomedia
3710         //    in this case, we need to scan both /sdcard/foo and /sdcard/bar/
3711         // 3) /sdcard/foo/bar.mp3 => /sdcard/foo/.nomedia
3712         //    in this case, we need to scan all of /sdcard/foo
3713         if (extractDisplayName(oldPath).equals(".nomedia")) {
3714             scanFileAsMediaProvider(new File(oldPath).getParentFile());
3715         }
3716         if (extractDisplayName(newPath).equals(".nomedia")) {
3717             scanFileAsMediaProvider(new File(newPath).getParentFile());
3718         }
3719 
3720         return 0;
3721     }
3722 
3723     /**
3724      * Rename file by checking for other URI grants on oldPath
3725      *
3726      * We don't support replace scenario by checking for other URI grants on newPath (if it exists).
3727      */
renameWithOtherUriGrants(DatabaseHelper helper, String oldPath, String newPath, ContentValues contentValues)3728     private boolean renameWithOtherUriGrants(DatabaseHelper helper, String oldPath, String newPath,
3729             ContentValues contentValues) {
3730         final Uri oldPathGrantedUri = getOtherUriGrantsForPath(oldPath, /* forWrite */ true);
3731         if (oldPathGrantedUri == null) {
3732             return false;
3733         }
3734         return updateDatabaseForFuseRename(helper, oldPath, newPath, contentValues, Bundle.EMPTY,
3735                 oldPathGrantedUri);
3736     }
3737 
3738     /**
3739      * Rename file/directory without imposing any restrictions.
3740      *
3741      * We don't impose any rename restrictions for apps that bypass scoped storage restrictions.
3742      * However, we update database entries for renamed files to keep the database consistent.
3743      */
renameUncheckedForFuse(String oldPath, String newPath)3744     private int renameUncheckedForFuse(String oldPath, String newPath) {
3745         if (new File(oldPath).isFile()) {
3746             return renameFileUncheckedForFuse(oldPath, newPath);
3747         } else {
3748             return renameDirectoryUncheckedForFuse(oldPath, newPath,
3749                     getAllFilesForRenameDirectory(oldPath));
3750         }
3751     }
3752 
3753     /**
3754      * Rename file or directory from {@code oldPath} to {@code newPath}.
3755      *
3756      * @param oldPath path of the file or directory to be renamed.
3757      * @param newPath new path of the file or directory to be renamed.
3758      * @param uid UID of the calling package.
3759      * @return 0 on successful rename, appropriate errno value if the rename is not allowed.
3760      * <ul>
3761      * <li>{@link OsConstants#ENOENT} Renaming a non-existing file or renaming a file from path that
3762      * is not indexed by MediaProvider database.
3763      * <li>{@link OsConstants#EPERM} Renaming a default directory or renaming a file to a file type
3764      * not supported by new path.
3765      * This method can also return errno returned from {@code Os.rename} function.
3766      *
3767      * Called from JNI in jni/MediaProviderWrapper.cpp
3768      */
3769     @Keep
renameForFuse(String oldPath, String newPath, int uid)3770     public int renameForFuse(String oldPath, String newPath, int uid) {
3771         final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. ";
3772         final LocalCallingIdentity token =
3773                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
3774         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), oldPath);
3775 
3776         try {
3777             if (isPrivatePackagePathNotAccessibleByCaller(oldPath)
3778                     || isPrivatePackagePathNotAccessibleByCaller(newPath)) {
3779                 return OsConstants.EACCES;
3780             }
3781 
3782             if (!newPath.equals(getAbsoluteSanitizedPath(newPath))) {
3783                 Log.e(TAG, "New path name contains invalid characters.");
3784                 return OsConstants.EPERM;
3785             }
3786 
3787             if (shouldBypassDatabaseAndSetDirtyForFuse(uid, oldPath)
3788                     && shouldBypassDatabaseAndSetDirtyForFuse(uid, newPath)) {
3789                 return renameInLowerFs(oldPath, newPath);
3790             }
3791 
3792             if (shouldBypassFuseRestrictions(/*forWrite*/ true, oldPath)
3793                     && shouldBypassFuseRestrictions(/*forWrite*/ true, newPath)) {
3794                 return renameUncheckedForFuse(oldPath, newPath);
3795             }
3796             // Legacy apps that made is this far don't have the right storage permission and hence
3797             // are not allowed to access anything other than their external app directory
3798             if (isCallingPackageRequestingLegacy()) {
3799                 return OsConstants.EACCES;
3800             }
3801 
3802             final String[] oldRelativePath = sanitizePath(extractRelativePath(oldPath));
3803             final String[] newRelativePath = sanitizePath(extractRelativePath(newPath));
3804             if (oldRelativePath.length == 0 || newRelativePath.length == 0) {
3805                 // Rename not allowed on paths that can't be translated to RELATIVE_PATH.
3806                 Log.e(TAG, errorMessage +  "Invalid path.");
3807                 return OsConstants.EPERM;
3808             }
3809             if (oldRelativePath.length == 1 && TextUtils.isEmpty(oldRelativePath[0])) {
3810                 // Allow rename of files/folders other than default directories.
3811                 final String displayName = extractDisplayName(oldPath);
3812                 for (String defaultFolder : DEFAULT_FOLDER_NAMES) {
3813                     if (displayName.equals(defaultFolder)) {
3814                         Log.e(TAG, errorMessage + oldPath + " is a default folder."
3815                                 + " Renaming a default folder is not allowed.");
3816                         return OsConstants.EPERM;
3817                     }
3818                 }
3819             }
3820             if (newRelativePath.length == 1 && TextUtils.isEmpty(newRelativePath[0])) {
3821                 Log.e(TAG, errorMessage +  newPath + " is in root folder."
3822                         + " Renaming a file/directory to root folder is not allowed");
3823                 return OsConstants.EPERM;
3824             }
3825 
3826             final File directoryAndroid = new File(
3827                     extractVolumePath(oldPath).toLowerCase(Locale.ROOT),
3828                     DIRECTORY_ANDROID_LOWER_CASE
3829             );
3830             final File directoryAndroidMedia = new File(directoryAndroid, DIRECTORY_MEDIA);
3831             String newPathLowerCase = newPath.toLowerCase(Locale.ROOT);
3832             if (directoryAndroidMedia.getAbsolutePath().equalsIgnoreCase(oldPath)) {
3833                 // Don't allow renaming 'Android/media' directory.
3834                 // Android/[data|obb] are bind mounted and these paths don't go through FUSE.
3835                 Log.e(TAG, errorMessage +  oldPath + " is a default folder in app external "
3836                         + "directory. Renaming a default folder is not allowed.");
3837                 return OsConstants.EPERM;
3838             } else if (FileUtils.contains(directoryAndroid, new File(newPathLowerCase))) {
3839                 if (newRelativePath.length <= 2) {
3840                     // Path is directly under Android, Android/media, Android/data, Android/obb or
3841                     // some other directory under Android. Don't allow moving files and directories
3842                     // in these paths. Files and directories are only allowed to move to path
3843                     // Android/media/<app_specific_directory>/*
3844                     Log.e(TAG, errorMessage +  newPath + " is in app external directory. "
3845                             + "Renaming a file/directory to app external directory is not "
3846                             + "allowed.");
3847                     return OsConstants.EPERM;
3848                 } else if (!FileUtils.contains(directoryAndroidMedia, new File(newPathLowerCase))) {
3849                     // New path is not in Android/media/*. Don't allow moving of files or
3850                     // directories to app external directory other than media directory.
3851                     Log.e(TAG, errorMessage +  newPath + " is not in external media directory."
3852                             + "File/directory can only be renamed to a path in external media "
3853                             + "directory. Renaming file/directory to path in other external "
3854                             + "directories is not allowed");
3855                     return OsConstants.EPERM;
3856                 }
3857             }
3858 
3859             // Continue renaming files/directories if rename of oldPath to newPath is allowed.
3860             if (new File(oldPath).isFile()) {
3861                 return renameFileCheckedForFuse(oldPath, newPath);
3862             } else {
3863                 return renameDirectoryCheckedForFuse(oldPath, newPath);
3864             }
3865         } finally {
3866             restoreLocalCallingIdentity(token);
3867         }
3868     }
3869 
3870     /**
3871      * Check if enable_unicode_check flag is enabled
3872      * Called from JNI in jni/MediaProviderWrapper.cpp
3873      */
3874     @Keep
isUnicodeCheckEnabledForFuse()3875     public boolean isUnicodeCheckEnabledForFuse() {
3876         return Flags.enableUnicodeCheck();
3877     }
3878 
3879     @Override
checkUriPermission(@onNull Uri uri, int uid, int modeFlags)3880     public int checkUriPermission(@NonNull Uri uri, int uid,
3881             /* @Intent.AccessUriMode */ int modeFlags) {
3882         final LocalCallingIdentity token = clearLocalCallingIdentity(
3883                 LocalCallingIdentity.fromExternal(getContext(), mUserCache, uid));
3884 
3885         if (isRedactedUri(uri)) {
3886             if ((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
3887                 // we don't allow write grants on redacted uris.
3888                 return PackageManager.PERMISSION_DENIED;
3889             }
3890 
3891             uri = getUriForRedactedUri(uri);
3892         }
3893 
3894         if (isPickerUri(uri)) {
3895             if (isCallerPhotoPicker()) {
3896                 // Allow PhotoPicker app access to Picker media.
3897                 return PERMISSION_GRANTED;
3898             }
3899             // Do not allow implicit access (by the virtue of ownership/permission) to picker uris.
3900             // Picker uris should have explicit permission grants.
3901             // If the calling app A has an explicit grant on picker uri, UriGrantsManagerService
3902             // will check the grant status and allow app A to grant the uri to app B (without
3903             // calling into MediaProvider)
3904             return PackageManager.PERMISSION_DENIED;
3905         }
3906 
3907         try {
3908             final boolean allowHidden = isCallingPackageAllowedHidden();
3909             final int table = matchUri(uri, allowHidden);
3910 
3911             final DatabaseHelper helper;
3912             try {
3913                 helper = getDatabaseForUri(uri);
3914             } catch (VolumeNotFoundException e) {
3915                 return PackageManager.PERMISSION_DENIED;
3916             }
3917 
3918             final int type;
3919             if ((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
3920                 type = TYPE_UPDATE;
3921             } else {
3922                 type = TYPE_QUERY;
3923             }
3924 
3925             final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, null);
3926             try (Cursor c = qb.query(helper,
3927                     new String[] { BaseColumns._ID }, null, null, null, null, null, null, null)) {
3928                 if (c.getCount() == 1) {
3929                     c.moveToFirst();
3930                     final long cursorId = c.getLong(0);
3931 
3932                     long uriId = -1;
3933                     try {
3934                         uriId = ContentUris.parseId(uri);
3935                     } catch (NumberFormatException ignored) {
3936                         // if the id is not a number, the uri doesn't have a valid ID at the end of
3937                         // the uri, (i.e., uri is uri of the table not of the item/row)
3938                     }
3939 
3940                     if (uriId != -1 && cursorId == uriId) {
3941                         return PackageManager.PERMISSION_GRANTED;
3942                     }
3943                 }
3944             }
3945 
3946             // For the uri with id cases, if it isn't returned in above query section, the result
3947             // isn't as expected. Don't grant the permission.
3948             switch (table) {
3949                 case AUDIO_MEDIA_ID:
3950                 case IMAGES_MEDIA_ID:
3951                 case VIDEO_MEDIA_ID:
3952                 case DOWNLOADS_ID:
3953                 case FILES_ID:
3954                 case AUDIO_MEDIA_ID_GENRES_ID:
3955                 case AUDIO_GENRES_ID:
3956                 case AUDIO_PLAYLISTS_ID:
3957                 case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
3958                 case AUDIO_ARTISTS_ID:
3959                 case AUDIO_ALBUMS_ID:
3960                     return PackageManager.PERMISSION_DENIED;
3961                 default:
3962                     // continue below
3963             }
3964 
3965             // If the uri is a valid content uri and doesn't have a valid ID at the end of the uri,
3966             // (i.e., uri is uri of the table not of the item/row), and app doesn't request prefix
3967             // grant, we are willing to grant this uri permission since this doesn't grant them any
3968             // extra access. This grant will only grant permissions on given uri, it will not grant
3969             // access to db rows of the corresponding table.
3970             if ((modeFlags & Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) == 0) {
3971                 return PackageManager.PERMISSION_GRANTED;
3972             }
3973         } finally {
3974             restoreLocalCallingIdentity(token);
3975         }
3976         return PackageManager.PERMISSION_DENIED;
3977     }
3978 
3979     @Override
query(@onNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)3980     public Cursor query(@NonNull Uri uri, String[] projection, String selection,
3981                         String[] selectionArgs, String sortOrder) {
3982         return query(uri, projection,
3983                 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, sortOrder), null);
3984     }
3985 
3986     @Override
query(@onNull Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)3987     public Cursor query(@NonNull Uri uri, String[] projection, Bundle queryArgs,
3988                         CancellationSignal signal) {
3989         return query(uri, projection, queryArgs, signal, /* forSelf */ false);
3990     }
3991 
query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal, boolean forSelf)3992     private Cursor query(Uri uri, String[] projection, Bundle queryArgs,
3993             CancellationSignal signal, boolean forSelf) {
3994         Trace.beginSection(safeTraceSectionNameWithUri("query", uri));
3995         try {
3996             return queryInternal(uri, projection, queryArgs, signal, forSelf);
3997         } catch (FallbackException e) {
3998             return e.translateForQuery(getCallingPackageTargetSdkVersion());
3999         } finally {
4000             Trace.endSection();
4001         }
4002     }
4003 
queryInternal(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal, boolean forSelf)4004     private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs,
4005             CancellationSignal signal, boolean forSelf) throws FallbackException {
4006         if (isPickerUri(uri)) {
4007             return mPickerUriResolver.query(uri, projection, mCallingIdentity.get().pid,
4008                     mCallingIdentity.get().uid, mCallingIdentity.get().getPackageName());
4009         }
4010 
4011         final String volumeName = getVolumeName(uri);
4012         PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
4013         queryArgs = (queryArgs != null) ? queryArgs : new Bundle();
4014 
4015         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
4016         queryArgs.remove(INCLUDED_DEFAULT_DIRECTORIES);
4017 
4018         final ArraySet<String> honoredArgs = new ArraySet<>();
4019         DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator);
4020 
4021         // In case of QUERY_ARG_MEDIA_STANDARD_SORT_ORDER
4022         // disregard existing sort order and sort by INFERRED_DATE
4023         if (Flags.inferredMediaDate() &&
4024                 queryArgs.containsKey(QUERY_ARG_MEDIA_STANDARD_SORT_ORDER)) {
4025             queryArgs.putString(QUERY_ARG_SQL_SORT_ORDER,
4026                     MediaColumns.INFERRED_DATE + " DESC");
4027         }
4028 
4029         Uri redactedUri = null;
4030         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
4031         queryArgs.remove(QUERY_ARG_REDACTED_URI);
4032         if (isRedactedUri(uri)) {
4033             redactedUri = uri;
4034             uri = getUriForRedactedUri(uri);
4035             queryArgs.putParcelable(QUERY_ARG_REDACTED_URI, redactedUri);
4036         }
4037 
4038         uri = safeUncanonicalize(uri);
4039 
4040         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
4041         final boolean allowHidden = isCallingPackageAllowedHidden();
4042         final int table = mUriMatcher.matchUri(uri, allowHidden, isCallerPhotoPicker());
4043 
4044         if (table == MEDIA_GRANTS) {
4045             return getReadGrantedMediaForPackage(queryArgs);
4046         }
4047 
4048         // handle MEDIA_SCANNER before calling getDatabaseForUri()
4049         if (table == MEDIA_SCANNER) {
4050             // create a cursor to return volume currently being scanned by the media scanner
4051             MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME});
4052             c.addRow(new String[] {mMediaScannerVolume});
4053             return c;
4054         }
4055 
4056         // Used temporarily (until we have unique media IDs) to get an identifier
4057         // for the current sd card, so that the music app doesn't have to use the
4058         // non-public getFatVolumeId method
4059         if (table == FS_ID) {
4060             MatrixCursor c = new MatrixCursor(new String[] {"fsid"});
4061             // current FAT volume ID
4062             int volumeId = -1;
4063             c.addRow(new Integer[] {volumeId});
4064             return c;
4065         }
4066 
4067         if (table == VERSION) {
4068             MatrixCursor c = new MatrixCursor(new String[] {"version"});
4069             c.addRow(new Integer[] {DatabaseHelper.getDatabaseVersion(getContext())});
4070             return c;
4071         }
4072 
4073         if (PickerUriResolver.PICKER_INTERNAL_TABLES.contains(table)) {
4074             return mPickerUriResolver.query(table, queryArgs, mPickerDbFacade.getLocalProvider(),
4075                     mPickerSyncController.getCloudProvider(), mPickerDataLayer);
4076         }
4077         if (table == PICKER_INTERNAL_V2) {
4078             return PickerUriResolverV2.query(
4079                     getContext().getApplicationContext(), uri, queryArgs, signal);
4080         }
4081 
4082         final DatabaseHelper helper = getDatabaseForUri(uri);
4083         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, queryArgs,
4084                 honoredArgs::add);
4085         // Allowing hidden column _user_id for this query to support Cloned Profile use case.
4086         if (table == FILES) {
4087             qb.allowColumn(FileColumns._USER_ID);
4088         }
4089 
4090         if (targetSdkVersion < Build.VERSION_CODES.R) {
4091             // Some apps are abusing "ORDER BY" clauses to inject "LIMIT"
4092             // clauses; gracefully lift them out.
4093             DatabaseUtils.recoverAbusiveSortOrder(queryArgs);
4094 
4095             // Some apps are abusing the Uri query parameters to inject LIMIT
4096             // clauses; gracefully lift them out.
4097             DatabaseUtils.recoverAbusiveLimit(uri, queryArgs);
4098         }
4099 
4100         if (targetSdkVersion < Build.VERSION_CODES.Q) {
4101             // Some apps are abusing the "WHERE" clause by injecting "GROUP BY"
4102             // clauses; gracefully lift them out.
4103             DatabaseUtils.recoverAbusiveSelection(queryArgs);
4104 
4105             // Some apps are abusing the first column to inject "DISTINCT";
4106             // gracefully lift them out.
4107             if ((projection != null) && (projection.length > 0)
4108                     && projection[0].startsWith("DISTINCT ")) {
4109                 projection[0] = projection[0].substring("DISTINCT ".length());
4110                 qb.setDistinct(true);
4111             }
4112 
4113             // Some apps are generating thumbnails with getThumbnail(), but then
4114             // ignoring the returned Bitmap and querying the raw table; give
4115             // them a row with enough information to find the original image.
4116             final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION);
4117             if ((table == IMAGES_THUMBNAILS || table == VIDEO_THUMBNAILS)
4118                     && !TextUtils.isEmpty(selection)) {
4119                 final Matcher matcher = PATTERN_SELECTION_ID.matcher(selection);
4120                 if (matcher.matches()) {
4121                     final long id = Long.parseLong(matcher.group(1));
4122 
4123                     final Uri fullUri;
4124                     if (table == IMAGES_THUMBNAILS) {
4125                         fullUri = ContentUris.withAppendedId(
4126                                 Images.Media.getContentUri(volumeName), id);
4127                     } else if (table == VIDEO_THUMBNAILS) {
4128                         fullUri = ContentUris.withAppendedId(
4129                                 Video.Media.getContentUri(volumeName), id);
4130                     } else {
4131                         throw new IllegalArgumentException();
4132                     }
4133 
4134                     final MatrixCursor cursor = new MatrixCursor(projection);
4135                     final File file = ContentResolver.encodeToFile(
4136                             fullUri.buildUpon().appendPath("thumbnail").build());
4137                     final String data = file.getAbsolutePath();
4138                     cursor.newRow().add(MediaColumns._ID, null)
4139                             .add(Images.Thumbnails.IMAGE_ID, id)
4140                             .add(Video.Thumbnails.VIDEO_ID, id)
4141                             .add(MediaColumns.DATA, data);
4142                     return cursor;
4143                 }
4144             }
4145         }
4146 
4147         // Update locale if necessary.
4148         if (helper.isInternal() && !Locale.getDefault().equals(mLastLocale)) {
4149             Log.i(TAG, "Updating locale within queryInternal");
4150             onLocaleChanged(false);
4151         }
4152 
4153         Cursor c;
4154 
4155         if (Flags.enableOemMetadata()
4156                 && hasColumnsToFilterInProjection(qb, projection, List.of(OEM_METADATA))
4157                 && !mCallingIdentity.get().checkCallingPermissionOemMetadata()) {
4158             // Filter oem_data column to return as NULL
4159             projection = updateProjectionToFilterColumns(qb, projection, List.of(OEM_METADATA));
4160         }
4161 
4162         // The prev deprecated latitude and longitude columns are being populated again for
4163         // picker search. We prevent any read access to them if they are present in the
4164         // query projection.
4165         if (indexMediaLatitudeLongitude() && hasColumnsToFilterInProjection(
4166                         qb, projection, List.of(LATITUDE, LONGITUDE)) && !isCallingPackageSelf()) {
4167             // Filter latitude and longitude to return as NULL
4168             projection = updateProjectionToFilterColumns(
4169                     qb, projection,  List.of(LATITUDE, LONGITUDE));
4170         }
4171 
4172         if (shouldFilterOwnerPackageNameFlag()
4173                 && shouldFilterOwnerPackageNameInProjection(qb, projection)) {
4174             Log.i(TAG, String.format("Filtering owner package name for %s, projection: %s",
4175                     mCallingIdentity.get().getPackageName(), Arrays.toString(projection)));
4176 
4177             // Get a list of all owner_package_names in the result
4178             final String[] ownerPackageNamesArr = getAllOwnerPackageNames(qb, helper,
4179                     queryArgs, signal);
4180 
4181             // Get a list of queryable owner_package_names out of all
4182             final Set<String> queryablePackages = getQueryablePackages(ownerPackageNamesArr);
4183 
4184             // Substitute owner_package_name column with following:
4185             // CASE WHEN owner_package_name IN ('queryablePackageA','queryablePackageB')
4186             // THEN owner_package_name ELSE NULL END AS owner_package_name
4187             final String[] newProjection = prepareSubstitution(qb, projection, queryablePackages);
4188             c = qb.query(helper, newProjection, queryArgs, signal);
4189         } else {
4190             c = qb.query(helper, projection, queryArgs, signal);
4191         }
4192 
4193         if (c != null && !forSelf) {
4194             // As a performance optimization, only configure notifications when
4195             // resulting cursor will leave our process
4196             final boolean callerIsRemote = mCallingIdentity.get().pid != android.os.Process.myPid();
4197             if (callerIsRemote && !isFuseThread()) {
4198                 c.setNotificationUri(getContext().getContentResolver(), uri);
4199             }
4200 
4201             final Bundle extras = new Bundle();
4202             extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS,
4203                     honoredArgs.toArray(new String[honoredArgs.size()]));
4204             c.setExtras(extras);
4205         }
4206 
4207         // Query was on a redacted URI, update the sensitive information such as the _ID, DATA etc.
4208         if (redactedUri != null && c != null) {
4209             try {
4210                 return getRedactedUriCursor(redactedUri, c);
4211             } finally {
4212                 c.close();
4213             }
4214         }
4215 
4216         return c;
4217     }
4218 
hasColumnsToFilterInProjection( SQLiteQueryBuilder qb, String[] projection, List<String> columnsToFilter)4219     private boolean hasColumnsToFilterInProjection(
4220             SQLiteQueryBuilder qb, String[] projection, List<String> columnsToFilter) {
4221         boolean columnsFound = false;
4222         List<String> projectionInLowerCase = new ArrayList<>();
4223         if (projection != null) {
4224             projectionInLowerCase = Arrays.asList(projection);
4225             projectionInLowerCase.replaceAll(String::toLowerCase);
4226         }
4227         for (String column: columnsToFilter) {
4228             columnsFound =
4229                     (!projectionInLowerCase.isEmpty() && projectionInLowerCase.contains(column))
4230                     || (projection == null && qb.getProjectionMap() != null
4231                     && qb.getProjectionMap().containsKey(column));
4232             if (columnsFound) {
4233                 return columnsFound;
4234             }
4235         }
4236         return columnsFound;
4237     }
4238 
updateProjectionToFilterColumns( SQLiteQueryBuilder qb, String[] projection, List<String> columnsToFilter)4239     private String[] updateProjectionToFilterColumns(
4240             SQLiteQueryBuilder qb, String[] projection, List<String> columnsToFilter) {
4241         projection = maybeReplaceNullProjection(projection, qb);
4242         List<String> projectionList = Arrays.asList(projection);
4243         projectionList.replaceAll(String::toLowerCase);
4244 
4245         for (String columnToFilter: columnsToFilter) {
4246             if (projectionList.contains(columnToFilter)) {
4247                 int indexOfColumnToBeFiltered = projectionList.indexOf(columnToFilter);
4248                 projectionList.set(
4249                         indexOfColumnToBeFiltered,
4250                         constructNullProjectionForColumn(columnToFilter)
4251                 );
4252             }
4253         }
4254         String[] updatedProjection = new String[projectionList.size()];
4255         return projectionList.toArray(updatedProjection);
4256     }
4257 
constructNullProjectionForColumn(String columnName)4258     private String constructNullProjectionForColumn(String columnName) {
4259         return "NULL AS " + columnName;
4260     }
4261 
4262     /**
4263      * Constructs the following projection string:
4264      *     CASE WHEN owner_package_name IN ("queryablePackageA","queryablePackageB")
4265      *     THEN owner_package_name ELSE NULL END AS owner_package_name
4266      */
constructOwnerPackageNameProjection(Set<String> queryablePackages)4267     private String constructOwnerPackageNameProjection(Set<String> queryablePackages) {
4268         final String packageNames = String.join(",", queryablePackages
4269                 .stream()
4270                 .map(name -> ("'" + name + "'"))
4271                 .collect(Collectors.toList()));
4272 
4273         final StringBuilder newProjection = new StringBuilder()
4274                 .append("CASE WHEN ")
4275                 .append(OWNER_PACKAGE_NAME)
4276                 .append(" IN (")
4277                 .append(packageNames)
4278                 .append(") THEN ")
4279                 .append(OWNER_PACKAGE_NAME)
4280                 .append(" ELSE NULL END AS ")
4281                 .append(OWNER_PACKAGE_NAME);
4282 
4283         Log.d(TAG, "Constructed owner_package_name substitution: " + newProjection);
4284         return newProjection.toString();
4285     }
4286 
getAllOwnerPackageNames(SQLiteQueryBuilder qb, DatabaseHelper helper, Bundle queryArgs, CancellationSignal signal)4287     private String[] getAllOwnerPackageNames(SQLiteQueryBuilder qb, DatabaseHelper helper,
4288             Bundle queryArgs, CancellationSignal signal) {
4289         final SQLiteQueryBuilder qbCopy = new SQLiteQueryBuilder(qb);
4290         qbCopy.setDistinct(true);
4291         qbCopy.appendWhereStandalone(OWNER_PACKAGE_NAME + " <> '' AND "
4292                 + OWNER_PACKAGE_NAME + " <> 'null' AND " + OWNER_PACKAGE_NAME + " IS NOT NULL");
4293         final Cursor ownerPackageNames = qbCopy.query(helper, new String[]{OWNER_PACKAGE_NAME},
4294                 queryArgs, signal);
4295 
4296         final String[] ownerPackageNamesArr = new String[ownerPackageNames.getCount()];
4297         int i = 0;
4298         while (ownerPackageNames.moveToNext()) {
4299             ownerPackageNamesArr[i++] = ownerPackageNames.getString(0);
4300         }
4301         return ownerPackageNamesArr;
4302     }
4303 
prepareSubstitution(SQLiteQueryBuilder qb, String[] projection, Set<String> queryablePackages)4304     private String[] prepareSubstitution(SQLiteQueryBuilder qb,
4305             String[] projection, Set<String> queryablePackages) {
4306         projection = maybeReplaceNullProjection(projection, qb);
4307         if (qb.getProjectionAllowlist() == null) {
4308             qb.setProjectionAllowlist(new ArrayList<>());
4309         }
4310         final String[] newProjection = new String[projection.length];
4311         for (int i = 0; i < projection.length; i++) {
4312             if (!OWNER_PACKAGE_NAME.equalsIgnoreCase(projection[i])) {
4313                 newProjection[i] = projection[i];
4314             } else {
4315                 newProjection[i] = constructOwnerPackageNameProjection(queryablePackages);
4316                 // Allow constructed owner_package_name column in projection
4317                 final String escapedColumnCase = Pattern.quote(newProjection[i]);
4318                 qb.getProjectionAllowlist().add(Pattern.compile(escapedColumnCase));
4319             }
4320         }
4321         return newProjection;
4322     }
4323 
maybeReplaceNullProjection(String[] projection, SQLiteQueryBuilder qb)4324     private String[] maybeReplaceNullProjection(String[] projection, SQLiteQueryBuilder qb) {
4325         // List all columns instead of placing "*" in the SQL query
4326         // to be able to substitute some columns
4327         if (projection == null) {
4328             projection = qb.getAllColumnsFromProjectionMap();
4329             // Allow all columns from the projection map
4330             qb.setStrictColumns(false);
4331         }
4332         return projection;
4333     }
4334 
4335     @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
getQueryablePackages(String[] packageNames)4336     private Set<String> getQueryablePackages(String[] packageNames) {
4337         final boolean[] canBeQueriedInfo;
4338         try {
4339             canBeQueriedInfo = mPackageManager.canPackageQuery(
4340                     mCallingIdentity.get().getPackageName(), packageNames);
4341         } catch (NameNotFoundException e) {
4342             Log.e(TAG, "Invalid package name", e);
4343             // If package manager throws an error, only assume calling package as queryable package
4344             return new HashSet<>(Arrays.asList(mCallingIdentity.get().getPackageName()));
4345         }
4346 
4347         final Set<String> queryablePackages = new HashSet<>();
4348         for (int i = 0; i < packageNames.length; i++) {
4349             if (canBeQueriedInfo[i]) {
4350                 queryablePackages.add(packageNames[i]);
4351             }
4352         }
4353         return queryablePackages;
4354     }
4355 
4356     @NotNull
getReadGrantedMediaForPackage(Bundle extras)4357     private Cursor getReadGrantedMediaForPackage(Bundle extras) {
4358         final int caller = Binder.getCallingUid();
4359         int userId;
4360         String[] packageNames;
4361         if (!checkPermissionSelf(caller)) {
4362             // All other callers are unauthorized.
4363             throw new SecurityException(
4364                     getSecurityExceptionMessage("read media grants"));
4365         }
4366         final PackageManager pm = getContext().getPackageManager();
4367         final int packageUid = extras.getInt(Intent.EXTRA_UID);
4368         packageNames = pm.getPackagesForUid(packageUid);
4369         // Get the userId from packageUid as the initiator could be a cloned app, which
4370         // accesses Media via MP of its parent user and Binder's callingUid reflects
4371         // the latter.
4372         userId = uidToUserId(packageUid);
4373         String[] mimeTypes = extras.getStringArray(EXTRA_MIME_TYPE_SELECTION);
4374         // Available volumes, to filter out any external storage that may be removed but the grants
4375         // persisted.
4376         String[] availableVolumes = mVolumeCache.getExternalVolumeNames().toArray(new String[0]);
4377         return mMediaGrants.getMediaGrantsForPackages(packageNames, userId, mimeTypes,
4378                 availableVolumes);
4379     }
4380 
4381     @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
shouldFilterOwnerPackageNameInProjection(SQLiteQueryBuilder qb, String[] projection)4382     private boolean shouldFilterOwnerPackageNameInProjection(SQLiteQueryBuilder qb,
4383             String[] projection) {
4384         return projectionNeedsOwnerPackageFiltering(projection, qb)
4385             && isApplicableForOwnerPackageNameFiltering();
4386     }
4387 
isApplicableForOwnerPackageNameFiltering()4388     private boolean isApplicableForOwnerPackageNameFiltering() {
4389         return SdkLevel.isAtLeastU()
4390                 && getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
4391                 && !mCallingIdentity.get().checkCallingPermissionsOwnerPackageName();
4392     }
4393 
projectionNeedsOwnerPackageFiltering(String[] proj, SQLiteQueryBuilder qb)4394     private boolean projectionNeedsOwnerPackageFiltering(String[] proj, SQLiteQueryBuilder qb) {
4395         return (proj != null && Arrays.asList(proj).contains(MediaColumns.OWNER_PACKAGE_NAME))
4396                 || (proj == null && qb.getProjectionMap() != null
4397                     && qb.getProjectionMap().containsKey(OWNER_PACKAGE_NAME));
4398     }
4399 
shouldFilterOwnerPackageNameFlag()4400     private boolean shouldFilterOwnerPackageNameFlag() {
4401         return true;
4402     }
4403 
isUriSupportedForRedaction(Uri uri)4404     private boolean isUriSupportedForRedaction(Uri uri) {
4405         final int match = matchUri(uri, true);
4406         return REDACTED_URI_SUPPORTED_TYPES.contains(match);
4407     }
4408 
getRedactedUriCursor(Uri redactedUri, @NonNull Cursor c)4409     private Cursor getRedactedUriCursor(Uri redactedUri, @NonNull Cursor c) {
4410         final HashSet<String> columnNames = new HashSet<>(Arrays.asList(c.getColumnNames()));
4411         final MatrixCursor redactedUriCursor = new MatrixCursor(c.getColumnNames());
4412         final String redactedUriId = redactedUri.getLastPathSegment();
4413 
4414         if (!c.moveToFirst()) {
4415             return redactedUriCursor;
4416         }
4417 
4418         // NOTE: It is safe to assume that there will only be one entry corresponding to a
4419         // redacted URI as it corresponds to a unique DB entry.
4420         if (c.getCount() != 1) {
4421             throw new AssertionError("Two rows corresponding to " + redactedUri.toString()
4422                     + " found, when only one expected");
4423         }
4424 
4425         final MatrixCursor.RowBuilder row = redactedUriCursor.newRow();
4426         for (String columnName : c.getColumnNames()) {
4427             final int colIndex = c.getColumnIndex(columnName);
4428             if (c.getType(colIndex) == FIELD_TYPE_BLOB) {
4429                 row.add(c.getBlob(colIndex));
4430             } else {
4431                 row.add(c.getString(colIndex));
4432             }
4433         }
4434 
4435         String ext = getFileExtensionFromCursor(c, columnNames);
4436         ext = ext == null ? "" : "." + ext;
4437         final String displayName = redactedUriId + ext;
4438         final String data = buildPrimaryVolumeFile(uidToUserId(Binder.getCallingUid()),
4439                 getRedactedRelativePath(), displayName).getAbsolutePath();
4440 
4441         updateRow(columnNames, MediaColumns._ID, row, redactedUriId);
4442         updateRow(columnNames, MediaColumns.DISPLAY_NAME, row, displayName);
4443         updateRow(columnNames, MediaColumns.RELATIVE_PATH, row, getRedactedRelativePath());
4444         updateRow(columnNames, MediaColumns.BUCKET_DISPLAY_NAME, row, getRedactedRelativePath());
4445         updateRow(columnNames, MediaColumns.DATA, row, data);
4446         updateRow(columnNames, MediaColumns.DOCUMENT_ID, row, null);
4447         updateRow(columnNames, MediaColumns.INSTANCE_ID, row, null);
4448         updateRow(columnNames, MediaColumns.BUCKET_ID, row, null);
4449 
4450         return redactedUriCursor;
4451     }
4452 
4453     @Nullable
getFileExtensionFromCursor(@onNull Cursor c, @NonNull HashSet<String> columnNames)4454     private static String getFileExtensionFromCursor(@NonNull Cursor c,
4455             @NonNull HashSet<String> columnNames) {
4456         if (columnNames.contains(MediaColumns.DATA)) {
4457             return extractFileExtension(c.getString(c.getColumnIndex(MediaColumns.DATA)));
4458         }
4459         if (columnNames.contains(MediaColumns.DISPLAY_NAME)) {
4460             return extractFileExtension(c.getString(c.getColumnIndex(MediaColumns.DISPLAY_NAME)));
4461         }
4462         return null;
4463     }
4464 
updateRow(HashSet<String> columnNames, String columnName, MatrixCursor.RowBuilder row, Object val)4465     private void updateRow(HashSet<String> columnNames, String columnName,
4466             MatrixCursor.RowBuilder row, Object val) {
4467         if (columnNames.contains(columnName)) {
4468             row.add(columnName, val);
4469         }
4470     }
4471 
getUriForRedactedUri(Uri redactedUri)4472     private Uri getUriForRedactedUri(Uri redactedUri) {
4473         final Uri.Builder builder = redactedUri.buildUpon();
4474         builder.path(null);
4475         final List<String> segments = redactedUri.getPathSegments();
4476         for (int i = 0; i < segments.size() - 1; i++) {
4477             builder.appendPath(segments.get(i));
4478         }
4479 
4480         DatabaseHelper helper;
4481         try {
4482             helper = getDatabaseForUri(redactedUri);
4483         } catch (VolumeNotFoundException e) {
4484             throw e.rethrowAsIllegalArgumentException();
4485         }
4486 
4487         try (final Cursor c = helper.runWithoutTransaction(
4488                 (db) -> db.query("files", new String[]{MediaColumns._ID},
4489                         FileColumns.REDACTED_URI_ID + "=?",
4490                         new String[]{redactedUri.getLastPathSegment()}, null, null, null))) {
4491             if (!c.moveToFirst()) {
4492                 throw new IllegalArgumentException(
4493                         "Uri: " + redactedUri.toString() + " not found.");
4494             }
4495 
4496             builder.appendPath(c.getString(0));
4497             return builder.build();
4498         }
4499     }
4500 
isRedactedUri(Uri uri)4501     private boolean isRedactedUri(Uri uri) {
4502         String id = uri.getLastPathSegment();
4503         return id != null && id.startsWith(REDACTED_URI_ID_PREFIX)
4504                 && id.length() == REDACTED_URI_ID_SIZE;
4505     }
4506 
4507     @Override
getType(Uri url)4508     public String getType(Uri url) {
4509         if (isRedactedUri(url)) {
4510             return queryForTypeAsCaller(url);
4511         }
4512 
4513         final int match = matchUri(url, true);
4514         switch (match) {
4515             case IMAGES_MEDIA_ID:
4516             case AUDIO_MEDIA_ID:
4517             case AUDIO_PLAYLISTS_ID:
4518             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
4519             case VIDEO_MEDIA_ID:
4520             case DOWNLOADS_ID:
4521             case FILES_ID:
4522                 if (SdkLevel.isAtLeastU()) {
4523                     // Starting Android 14, there is permission check for
4524                     // getting types requiring internal query.
4525                     return queryForTypeAsCaller(url);
4526                 } else {
4527                     return queryForTypeAsSelf(url);
4528                 }
4529 
4530             case IMAGES_MEDIA:
4531             case IMAGES_THUMBNAILS:
4532                 return Images.Media.CONTENT_TYPE;
4533 
4534             case AUDIO_ALBUMART_ID:
4535             case AUDIO_ALBUMART_FILE_ID:
4536             case IMAGES_THUMBNAILS_ID:
4537             case VIDEO_THUMBNAILS_ID:
4538                 return "image/jpeg";
4539 
4540             case AUDIO_MEDIA:
4541             case AUDIO_GENRES_ID_MEMBERS:
4542             case AUDIO_PLAYLISTS_ID_MEMBERS:
4543                 return Audio.Media.CONTENT_TYPE;
4544 
4545             case AUDIO_GENRES:
4546             case AUDIO_MEDIA_ID_GENRES:
4547                 return Audio.Genres.CONTENT_TYPE;
4548             case AUDIO_GENRES_ID:
4549             case AUDIO_MEDIA_ID_GENRES_ID:
4550                 return Audio.Genres.ENTRY_CONTENT_TYPE;
4551             case AUDIO_PLAYLISTS:
4552                 return Audio.Playlists.CONTENT_TYPE;
4553 
4554             case VIDEO_MEDIA:
4555                 return Video.Media.CONTENT_TYPE;
4556             case DOWNLOADS:
4557                 return Downloads.CONTENT_TYPE;
4558 
4559             case PICKER_ID:
4560             case PICKER_GET_CONTENT_ID:
4561                 return mPickerUriResolver.getType(url, Binder.getCallingPid(),
4562                         Binder.getCallingUid());
4563         }
4564         throw new IllegalStateException("Unknown URL : " + url);
4565     }
4566 
queryForTypeAsSelf(Uri url)4567     private String queryForTypeAsSelf(Uri url) {
4568         final LocalCallingIdentity token = clearLocalCallingIdentity();
4569         try {
4570             return queryForTypeAsCaller(url);
4571         } finally {
4572             restoreLocalCallingIdentity(token);
4573         }
4574     }
4575 
queryForTypeAsCaller(Uri url)4576     private String queryForTypeAsCaller(Uri url) {
4577         try (Cursor cursor = queryForSingleItem(url,
4578                 new String[] { MediaColumns.MIME_TYPE }, null, null, null)) {
4579             return cursor.getString(0);
4580         } catch (FileNotFoundException e) {
4581             throw new IllegalArgumentException(e.getMessage());
4582         }
4583     }
4584 
4585     @VisibleForTesting
ensureFileColumns(@onNull Uri uri, @NonNull ContentValues values)4586     void ensureFileColumns(@NonNull Uri uri, @NonNull ContentValues values)
4587             throws VolumeArgumentException, VolumeNotFoundException {
4588         final LocalUriMatcher matcher = new LocalUriMatcher(MediaStore.AUTHORITY);
4589         final int match = matcher.matchUri(uri, true);
4590         ensureNonUniqueFileColumns(match, uri, Bundle.EMPTY, values, null /* currentPath */);
4591     }
4592 
ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)4593     private void ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
4594             @NonNull ContentValues values, @Nullable String currentPath)
4595             throws VolumeArgumentException, VolumeNotFoundException {
4596         ensureFileColumns(match, uri, extras, values, true, currentPath);
4597     }
4598 
ensureNonUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)4599     private void ensureNonUniqueFileColumns(int match, @NonNull Uri uri,
4600             @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)
4601             throws VolumeArgumentException, VolumeNotFoundException {
4602         ensureFileColumns(match, uri, extras, values, false, currentPath);
4603     }
4604 
4605     /**
4606      * Get the various file-related {@link MediaColumns} in the given
4607      * {@link ContentValues} into a consistent condition. Also validates that defined
4608      * columns are valid for the given {@link Uri}, such as ensuring that only
4609      * {@code image/*} can be inserted into
4610      * {@link android.provider.MediaStore.Images}.
4611      */
ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)4612     private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
4613             @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)
4614             throws VolumeArgumentException, VolumeNotFoundException {
4615         Trace.beginSection("MP.ensureFileColumns");
4616 
4617         Objects.requireNonNull(uri);
4618         Objects.requireNonNull(extras);
4619         Objects.requireNonNull(values);
4620 
4621         // Figure out defaults based on Uri being modified
4622         String defaultMimeType = ClipDescription.MIMETYPE_UNKNOWN;
4623         int defaultMediaType = FileColumns.MEDIA_TYPE_NONE;
4624         String defaultPrimary = Environment.DIRECTORY_DOWNLOADS;
4625         String defaultSecondary = null;
4626         List<String> allowedPrimary = Arrays.asList(
4627                 Environment.DIRECTORY_DOWNLOADS,
4628                 Environment.DIRECTORY_DOCUMENTS);
4629         switch (match) {
4630             case AUDIO_MEDIA:
4631             case AUDIO_MEDIA_ID:
4632                 defaultMimeType = "audio/mpeg";
4633                 defaultMediaType = FileColumns.MEDIA_TYPE_AUDIO;
4634                 defaultPrimary = Environment.DIRECTORY_MUSIC;
4635                 if (SdkLevel.isAtLeastS()) {
4636                     allowedPrimary = Arrays.asList(
4637                             Environment.DIRECTORY_ALARMS,
4638                             Environment.DIRECTORY_AUDIOBOOKS,
4639                             Environment.DIRECTORY_MUSIC,
4640                             Environment.DIRECTORY_NOTIFICATIONS,
4641                             Environment.DIRECTORY_PODCASTS,
4642                             Environment.DIRECTORY_RECORDINGS,
4643                             Environment.DIRECTORY_RINGTONES);
4644                 } else {
4645                     allowedPrimary = Arrays.asList(
4646                             Environment.DIRECTORY_ALARMS,
4647                             Environment.DIRECTORY_AUDIOBOOKS,
4648                             Environment.DIRECTORY_MUSIC,
4649                             Environment.DIRECTORY_NOTIFICATIONS,
4650                             Environment.DIRECTORY_PODCASTS,
4651                             FileUtils.DIRECTORY_RECORDINGS,
4652                             Environment.DIRECTORY_RINGTONES);
4653                 }
4654                 break;
4655             case VIDEO_MEDIA:
4656             case VIDEO_MEDIA_ID:
4657                 defaultMimeType = "video/mp4";
4658                 defaultMediaType = FileColumns.MEDIA_TYPE_VIDEO;
4659                 defaultPrimary = Environment.DIRECTORY_MOVIES;
4660                 allowedPrimary = Arrays.asList(
4661                         Environment.DIRECTORY_DCIM,
4662                         Environment.DIRECTORY_MOVIES,
4663                         Environment.DIRECTORY_PICTURES);
4664                 break;
4665             case IMAGES_MEDIA:
4666             case IMAGES_MEDIA_ID:
4667                 defaultMimeType = "image/jpeg";
4668                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
4669                 defaultPrimary = Environment.DIRECTORY_PICTURES;
4670                 allowedPrimary = Arrays.asList(
4671                         Environment.DIRECTORY_DCIM,
4672                         Environment.DIRECTORY_PICTURES);
4673                 break;
4674             case AUDIO_ALBUMART:
4675             case AUDIO_ALBUMART_ID:
4676                 defaultMimeType = "image/jpeg";
4677                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
4678                 defaultPrimary = Environment.DIRECTORY_MUSIC;
4679                 allowedPrimary = Collections.singletonList(defaultPrimary);
4680                 defaultSecondary = DIRECTORY_THUMBNAILS;
4681                 break;
4682             case VIDEO_THUMBNAILS:
4683             case VIDEO_THUMBNAILS_ID:
4684                 defaultMimeType = "image/jpeg";
4685                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
4686                 defaultPrimary = Environment.DIRECTORY_MOVIES;
4687                 allowedPrimary = Collections.singletonList(defaultPrimary);
4688                 defaultSecondary = DIRECTORY_THUMBNAILS;
4689                 break;
4690             case IMAGES_THUMBNAILS:
4691             case IMAGES_THUMBNAILS_ID:
4692                 defaultMimeType = "image/jpeg";
4693                 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
4694                 defaultPrimary = Environment.DIRECTORY_PICTURES;
4695                 allowedPrimary = Collections.singletonList(defaultPrimary);
4696                 defaultSecondary = DIRECTORY_THUMBNAILS;
4697                 break;
4698             case AUDIO_PLAYLISTS:
4699             case AUDIO_PLAYLISTS_ID:
4700                 defaultMimeType = "audio/mpegurl";
4701                 defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
4702                 defaultPrimary = Environment.DIRECTORY_MUSIC;
4703                 allowedPrimary = Arrays.asList(
4704                         Environment.DIRECTORY_MUSIC,
4705                         Environment.DIRECTORY_MOVIES);
4706                 break;
4707             case DOWNLOADS:
4708             case DOWNLOADS_ID:
4709                 defaultPrimary = Environment.DIRECTORY_DOWNLOADS;
4710                 allowedPrimary = Collections.singletonList(defaultPrimary);
4711                 break;
4712             case FILES:
4713             case FILES_ID:
4714                 // Use defaults above
4715                 break;
4716             default:
4717                 Log.w(TAG, "Unhandled location " + uri + "; assuming generic files");
4718                 break;
4719         }
4720 
4721         final String resolvedVolumeName = resolveVolumeName(uri);
4722 
4723         if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))
4724                 && MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)) {
4725             // TODO: promote this to top-level check
4726             throw new UnsupportedOperationException(
4727                     "Writing to internal storage is not supported.");
4728         }
4729 
4730         // Force values when raw path provided
4731         if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
4732             FileUtils.computeValuesFromData(values, isFuseThread());
4733         }
4734 
4735         final boolean isTargetSdkROrHigher =
4736                 getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R;
4737         final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
4738         final String mimeTypeFromExt = TextUtils.isEmpty(displayName) ? null :
4739                 MimeUtils.resolveMimeType(new File(displayName));
4740 
4741         if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) {
4742             if (isTargetSdkROrHigher) {
4743                 // Extract the MIME type from the display name if we couldn't resolve it from the
4744                 // raw path
4745                 if (mimeTypeFromExt != null) {
4746                     values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
4747                 } else {
4748                     // We couldn't resolve mimeType, it means that both display name and MIME type
4749                     // were missing in values, so we use defaultMimeType.
4750                     values.put(MediaColumns.MIME_TYPE, defaultMimeType);
4751                 }
4752             } else if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
4753                 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
4754             } else {
4755                 // We don't use mimeTypeFromExt to preserve legacy behavior.
4756                 values.put(MediaColumns.MIME_TYPE, defaultMimeType);
4757             }
4758         }
4759 
4760         String mimeType = values.getAsString(MediaColumns.MIME_TYPE);
4761         if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
4762             // We allow any mimeType for generic uri with default media type as MEDIA_TYPE_NONE.
4763         } else if (mimeType != null &&
4764                 MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) == null) {
4765             if (mimeTypeFromExt != null &&
4766                     defaultMediaType == MimeUtils.resolveMediaType(mimeTypeFromExt)) {
4767                 // If mimeType from extension matches the defaultMediaType of uri, we use mimeType
4768                 // from file extension as mimeType. This is an effort to guess the mimeType when we
4769                 // get unsupported mimeType.
4770                 // Note: We can't force defaultMimeType because when we force defaultMimeType, we
4771                 // will force the file extension as well. For example, if DISPLAY_NAME=Foo.png and
4772                 // mimeType="image/*". If we force mimeType to be "image/jpeg", we append the file
4773                 // name with the new file extension i.e., "Foo.png.jpg" where as the expected file
4774                 // name was "Foo.png"
4775                 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
4776             } else if (isTargetSdkROrHigher) {
4777                 // We are here because given mimeType is unsupported also we couldn't guess valid
4778                 // mimeType from file extension.
4779                 throw new IllegalArgumentException("Unsupported MIME type " + mimeType);
4780             } else {
4781                 // We can't throw error for legacy apps, so we try to use defaultMimeType.
4782                 values.put(MediaColumns.MIME_TYPE, defaultMimeType);
4783             }
4784         }
4785 
4786         // Give ourselves reasonable defaults when missing
4787         if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) {
4788             values.put(MediaColumns.DISPLAY_NAME,
4789                     String.valueOf(System.currentTimeMillis()));
4790         }
4791         final Integer formatObject = values.getAsInteger(FileColumns.FORMAT);
4792         final int format = formatObject == null ? 0 : formatObject;
4793         if (format == MtpConstants.FORMAT_ASSOCIATION) {
4794             values.putNull(MediaColumns.MIME_TYPE);
4795         }
4796 
4797         mimeType = values.getAsString(MediaColumns.MIME_TYPE);
4798         // Quick check MIME type against table
4799         if (mimeType != null) {
4800             PulledMetrics.logMimeTypeAccess(getCallingUidOrSelf(), mimeType);
4801             final int actualMediaType = MimeUtils.resolveMediaType(mimeType);
4802             if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
4803                 // Give callers an opportunity to work with playlists and
4804                 // subtitles using the generic files table
4805                 switch (actualMediaType) {
4806                     case FileColumns.MEDIA_TYPE_PLAYLIST:
4807                         defaultMimeType = "audio/mpegurl";
4808                         defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
4809                         defaultPrimary = Environment.DIRECTORY_MUSIC;
4810                         allowedPrimary = new ArrayList<>(allowedPrimary);
4811                         allowedPrimary.add(Environment.DIRECTORY_MUSIC);
4812                         allowedPrimary.add(Environment.DIRECTORY_MOVIES);
4813                         break;
4814                     case FileColumns.MEDIA_TYPE_SUBTITLE:
4815                         defaultMimeType = "application/x-subrip";
4816                         defaultMediaType = FileColumns.MEDIA_TYPE_SUBTITLE;
4817                         defaultPrimary = Environment.DIRECTORY_MOVIES;
4818                         allowedPrimary = new ArrayList<>(allowedPrimary);
4819                         allowedPrimary.add(Environment.DIRECTORY_MUSIC);
4820                         allowedPrimary.add(Environment.DIRECTORY_MOVIES);
4821                         break;
4822                 }
4823             } else if (defaultMediaType != actualMediaType) {
4824                 final String[] split = defaultMimeType.split("/");
4825                 throw new IllegalArgumentException(
4826                         "MIME type " + mimeType + " cannot be inserted into " + uri
4827                                 + "; expected MIME type under " + split[0] + "/*");
4828             }
4829         }
4830 
4831         // Use default directories when missing
4832         if (TextUtils.isEmpty(values.getAsString(MediaColumns.RELATIVE_PATH))) {
4833             if (defaultSecondary != null) {
4834                 values.put(MediaColumns.RELATIVE_PATH,
4835                         defaultPrimary + '/' + defaultSecondary + '/');
4836             } else {
4837                 values.put(MediaColumns.RELATIVE_PATH,
4838                         defaultPrimary + '/');
4839             }
4840         }
4841 
4842         // Generate path when undefined
4843         if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
4844             // Note that just the volume name isn't enough to determine the path,
4845             // since we can manage different volumes with the same name for
4846             // different users. Instead, if we have a current path (which implies
4847             // an already existing file to be renamed), use that to derive the
4848             // user-id of the file, and in turn use that to derive the correct
4849             // volume. Cross-user renames are not supported without a specified
4850             // DATA column.
4851             File volumePath;
4852             UserHandle userHandle = mCallingIdentity.get().getUser();
4853             Integer userIdFromPathObject = values.getAsInteger(FileColumns._USER_ID);
4854             int userIdFromPath = (userIdFromPathObject == null ? userHandle.getIdentifier() :
4855                     userIdFromPathObject);
4856             // In case if the _user_id column is set, and is different from the userHandle
4857             // determined from mCallingIdentity, we prefer the former, as it comes from the original
4858             // path provided to MP process.
4859             // Normally this does not create any issues, but when cloned profile is active, an app
4860             // in root user can try to create an image file in lower file system, by specifying
4861             // the file directory as /storage/emulated/<cloneUserId>/DCIM. For such cases, we
4862             // would want <cloneUserId> to be used to determine path in MP entry.
4863             if (userHandle.getIdentifier() != userIdFromPath
4864                     && isAppCloneUserPair(userHandle.getIdentifier(), userIdFromPath)) {
4865                 userHandle = UserHandle.of(userIdFromPath);
4866             }
4867             if (currentPath != null) {
4868                 int userId = FileUtils.extractUserId(currentPath);
4869                 if (userId != -1) {
4870                     userHandle = UserHandle.of(userId);
4871                 }
4872             }
4873             try {
4874                 volumePath = mVolumeCache.getVolumePath(resolvedVolumeName, userHandle);
4875             } catch (FileNotFoundException e) {
4876                 throw new IllegalArgumentException(e);
4877             }
4878 
4879             FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ !isFuseThread());
4880             FileUtils.computeDataFromValues(values, volumePath, isFuseThread());
4881             assertFileColumnsConsistent(match, uri, values);
4882 
4883             // Create result file
4884             File res = new File(values.getAsString(MediaColumns.DATA));
4885             try {
4886                 if (makeUnique) {
4887                     res = FileUtils.buildUniqueFile(res.getParentFile(),
4888                             mimeType, res.getName());
4889                 } else {
4890                     res = FileUtils.buildNonUniqueFile(res.getParentFile(),
4891                             mimeType, res.getName());
4892                 }
4893             } catch (FileNotFoundException e) {
4894                 throw new IllegalStateException(
4895                         "Failed to build unique file: " + res + " " + values);
4896             }
4897 
4898             // Require that content lives under well-defined directories to help
4899             // keep the user's content organized
4900 
4901             // Start by saying unchanged directories are valid
4902             final String currentDir = (currentPath != null)
4903                     ? new File(currentPath).getParent() : null;
4904             boolean validPath = res.getParent().equals(currentDir);
4905 
4906             // Next, consider allowing based on allowed primary directory
4907             final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/");
4908             final String primary = extractTopLevelDir(relativePath);
4909             if (!validPath) {
4910                 validPath = containsIgnoreCase(allowedPrimary, primary);
4911             }
4912 
4913             // Next, consider allowing paths when referencing a related item
4914             final Uri relatedUri = extras.getParcelable(QUERY_ARG_RELATED_URI);
4915             if (!validPath && relatedUri != null) {
4916                 try (Cursor c = queryForSingleItem(relatedUri, new String[] {
4917                         MediaColumns.MIME_TYPE,
4918                         MediaColumns.RELATIVE_PATH,
4919                 }, null, null, null)) {
4920                     // If top-level MIME type matches, and relative path
4921                     // matches, then allow caller to place things here
4922 
4923                     final String expectedType = MimeUtils.extractPrimaryType(
4924                             c.getString(0));
4925                     final String actualType = MimeUtils.extractPrimaryType(
4926                             values.getAsString(MediaColumns.MIME_TYPE));
4927                     if (!Objects.equals(expectedType, actualType)) {
4928                         throw new IllegalArgumentException("Placement of " + actualType
4929                                 + " item not allowed in relation to " + expectedType + " item");
4930                     }
4931 
4932                     final String expectedPath = c.getString(1);
4933                     final String actualPath = values.getAsString(MediaColumns.RELATIVE_PATH);
4934                     if (!Objects.equals(expectedPath, actualPath)) {
4935                         throw new IllegalArgumentException("Placement of " + actualPath
4936                                 + " item not allowed in relation to " + expectedPath + " item");
4937                     }
4938 
4939                     // If we didn't see any trouble above, then we'll allow it
4940                     validPath = true;
4941                 } catch (FileNotFoundException e) {
4942                     Log.w(TAG, "Failed to find related item " + relatedUri + ": " + e);
4943                 }
4944             }
4945 
4946             // Consider allowing external media directory of calling package
4947             if (!validPath) {
4948                 final String pathOwnerPackage = extractPathOwnerPackageName(res.getAbsolutePath());
4949                 if (pathOwnerPackage != null) {
4950                     validPath = isExternalMediaDirectory(res.getAbsolutePath()) &&
4951                             isCallingIdentitySharedPackageName(pathOwnerPackage);
4952                 }
4953             }
4954 
4955             // Allow apps with MANAGE_EXTERNAL_STORAGE to create files anywhere
4956             if (!validPath) {
4957                 validPath = isCallingPackageManager();
4958             }
4959 
4960             // Allow system gallery to create image/video files.
4961             if (!validPath) {
4962                 // System gallery can create image/video files in any existing directory, it can
4963                 // also create subdirectories in any existing top-level directory. However, system
4964                 // gallery is not allowed to create non-default top level directory.
4965                 final boolean createNonDefaultTopLevelDir = primary != null &&
4966                         !FileUtils.buildPath(volumePath, primary).exists();
4967                 validPath = !createNonDefaultTopLevelDir && canSystemGalleryAccessTheFile(
4968                         res.getAbsolutePath());
4969             }
4970 
4971             // Nothing left to check; caller can't use this path
4972             if (!validPath) {
4973                 throw new IllegalArgumentException(
4974                         "Primary directory " + primary + " not allowed for " + uri
4975                                 + "; allowed directories are " + allowedPrimary);
4976             }
4977 
4978             boolean isFuseThread = isFuseThread();
4979             // Check if the following are true:
4980             // 1. Not a FUSE thread
4981             // 2. |res| is a child of a default dir and the default dir is missing
4982             // If true, we want to update the mTime of the volume root, after creating the dir
4983             // on the lower filesystem. This fixes some FileManagers relying on the mTime change
4984             // for UI updates
4985             File defaultDirVolumePath =
4986                     isFuseThread ? null : checkDefaultDirMissing(resolvedVolumeName, res);
4987             // Ensure all parent folders of result file exist
4988             res.getParentFile().mkdirs();
4989             if (!res.getParentFile().exists()) {
4990                 throw new IllegalStateException("Failed to create directory: " + res);
4991             }
4992             touchFusePath(defaultDirVolumePath);
4993 
4994             values.put(MediaColumns.DATA, res.getAbsolutePath());
4995             // buildFile may have changed the file name, compute values to extract new DISPLAY_NAME.
4996             // Note: We can't extract displayName from res.getPath() because for pending & trashed
4997             // files DISPLAY_NAME will not be same as file name.
4998             FileUtils.computeValuesFromData(values, isFuseThread);
4999         } else {
5000             assertFileColumnsConsistent(match, uri, values);
5001         }
5002 
5003         assertPrivatePathNotInValues(values);
5004 
5005         // Drop columns that aren't relevant for special tables
5006         switch (match) {
5007             case AUDIO_ALBUMART:
5008             case VIDEO_THUMBNAILS:
5009             case IMAGES_THUMBNAILS:
5010                 final Set<String> valid = getProjectionMap(MediaStore.Images.Thumbnails.class)
5011                         .keySet();
5012                 for (String key : new ArraySet<>(values.keySet())) {
5013                     if (!valid.contains(key)) {
5014                         values.remove(key);
5015                     }
5016                 }
5017                 break;
5018         }
5019 
5020         Trace.endSection();
5021     }
5022 
5023     /**
5024      * For apps targetSdk >= S: Check that values does not contain any external private path.
5025      * For all apps: Check that values does not contain any other app's external private paths.
5026      */
assertPrivatePathNotInValues(ContentValues values)5027     private void assertPrivatePathNotInValues(ContentValues values)
5028             throws IllegalArgumentException {
5029         ArrayList<String> relativePaths = new ArrayList<String>();
5030         relativePaths.add(extractRelativePath(values.getAsString(MediaColumns.DATA)));
5031         relativePaths.add(values.getAsString(MediaColumns.RELATIVE_PATH));
5032 
5033         for (final String relativePath : relativePaths) {
5034             if (!isDataOrObbRelativePath(relativePath)) {
5035                 continue;
5036             }
5037 
5038             /**
5039              * Don't allow apps to insert/update database row to files in Android/data or
5040              * Android/obb dirs. These are app private directories and files in these private
5041              * directories can't be added to public media collection.
5042              *
5043              * Note: For backwards compatibility we allow apps with targetSdk < S to insert private
5044              * files to MediaProvider
5045              */
5046             if (CompatChanges.isChangeEnabled(ENABLE_CHECKS_FOR_PRIVATE_FILES,
5047                     Binder.getCallingUid())) {
5048                 throw new IllegalArgumentException(
5049                         "Inserting private file: " + relativePath + " is not allowed.");
5050             }
5051 
5052             /**
5053              * Restrict all (legacy and non-legacy) apps from inserting paths in other
5054              * app's private directories.
5055              * Allow legacy apps to insert/update files in app private directories for backward
5056              * compatibility but don't allow them to do so in other app's private directories.
5057              */
5058             if (!isCallingIdentityAllowedAccessToDataOrObbPath(relativePath)) {
5059                 throw new IllegalArgumentException(
5060                         "Inserting private file: " + relativePath + " is not allowed.");
5061             }
5062         }
5063     }
5064 
5065     /**
5066      * @return the default dir if {@code file} is a child of default dir and it's missing,
5067      * {@code null} otherwise.
5068      */
checkDefaultDirMissing(String volumeName, File file)5069     private File checkDefaultDirMissing(String volumeName, File file) {
5070         String topLevelDir = FileUtils.extractTopLevelDir(file.getPath());
5071         if (topLevelDir != null && FileUtils.isDefaultDirectoryName(topLevelDir)) {
5072             try {
5073                 File volumePath = getVolumePath(volumeName);
5074                 if (!new File(volumePath, topLevelDir).exists()) {
5075                     return volumePath;
5076                 }
5077             } catch (FileNotFoundException e) {
5078                 Log.w(TAG, "Failed to checkDefaultDirMissing for " + file, e);
5079             }
5080         }
5081         return null;
5082     }
5083 
5084     /** Updates mTime of {@code path} on the FUSE filesystem */
touchFusePath(@ullable File path)5085     private void touchFusePath(@Nullable File path) {
5086         if (path != null) {
5087             // Touch root of volume to update mTime on FUSE filesystem
5088             // This allows FileManagers that may be relying on mTime changes to update their UI
5089             File fusePath = toFuseFile(path);
5090             Log.i(TAG, "Touching FUSE path " + fusePath);
5091             fusePath.setLastModified(System.currentTimeMillis());
5092         }
5093     }
5094 
5095     /**
5096      * Check that any requested {@link MediaColumns#DATA} paths actually
5097      * live on the storage volume being targeted.
5098      */
assertFileColumnsConsistent(int match, Uri uri, ContentValues values)5099     private void assertFileColumnsConsistent(int match, Uri uri, ContentValues values)
5100             throws VolumeArgumentException, VolumeNotFoundException {
5101         if (!values.containsKey(MediaColumns.DATA)) return;
5102 
5103         final String volumeName = resolveVolumeName(uri);
5104         try {
5105             // Quick check that the requested path actually lives on volume
5106             final Collection<File> allowed = getAllowedVolumePaths(volumeName);
5107             final File actual = new File(values.getAsString(MediaColumns.DATA))
5108                     .getCanonicalFile();
5109             if (!FileUtils.contains(allowed, actual)) {
5110                 throw new VolumeArgumentException(actual, allowed);
5111             }
5112         } catch (IOException e) {
5113             throw new VolumeNotFoundException(volumeName);
5114         }
5115     }
5116 
5117     @Override
bulkInsert(Uri uri, ContentValues[] values)5118     public int bulkInsert(Uri uri, ContentValues[] values) {
5119         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
5120         final boolean allowHidden = isCallingPackageAllowedHidden();
5121         final int match = matchUri(uri, allowHidden);
5122 
5123         if (match == VOLUMES) {
5124             return super.bulkInsert(uri, values);
5125         }
5126 
5127         if (match == AUDIO_PLAYLISTS_ID || match == AUDIO_PLAYLISTS_ID_MEMBERS) {
5128             final String resolvedVolumeName = resolveVolumeName(uri);
5129 
5130             final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
5131             final Uri playlistUri = ContentUris.withAppendedId(
5132                     MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId);
5133 
5134             final String audioVolumeName =
5135                     MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)
5136                             ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL;
5137 
5138             // Require that caller has write access to underlying media
5139             enforceCallingPermission(playlistUri, Bundle.EMPTY, true);
5140             for (ContentValues each : values) {
5141                 final long audioId = each.getAsLong(Audio.Playlists.Members.AUDIO_ID);
5142                 final Uri audioUri = Audio.Media.getContentUri(audioVolumeName, audioId);
5143                 enforceCallingPermission(audioUri, Bundle.EMPTY, false);
5144             }
5145 
5146             return bulkInsertPlaylist(playlistUri, values);
5147         }
5148 
5149         final DatabaseHelper helper;
5150         try {
5151             helper = getDatabaseForUri(uri);
5152         } catch (VolumeNotFoundException e) {
5153             return e.translateForUpdateDelete(targetSdkVersion);
5154         }
5155 
5156         helper.beginTransaction();
5157         try {
5158             final int result = super.bulkInsert(uri, values);
5159             helper.setTransactionSuccessful();
5160             return result;
5161         } finally {
5162             helper.endTransaction();
5163         }
5164     }
5165 
bulkInsertPlaylist(@onNull Uri uri, @NonNull ContentValues[] values)5166     private int bulkInsertPlaylist(@NonNull Uri uri, @NonNull ContentValues[] values) {
5167         Trace.beginSection("MP.bulkInsertPlaylist");
5168         try {
5169             try {
5170                 return addPlaylistMembers(uri, values);
5171             } catch (SQLiteConstraintException e) {
5172                 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
5173                     throw e;
5174                 } else {
5175                     return 0;
5176                 }
5177             }
5178         } catch (FallbackException e) {
5179             return e.translateForBulkInsert(getCallingPackageTargetSdkVersion());
5180         } finally {
5181             Trace.endSection();
5182         }
5183     }
5184 
insertDirectory(@onNull SQLiteDatabase db, @NonNull String path)5185     private long insertDirectory(@NonNull SQLiteDatabase db, @NonNull String path) {
5186         if (LOGV) Log.v(TAG, "inserting directory " + path);
5187         ContentValues values = new ContentValues();
5188         values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
5189         values.put(FileColumns.DATA, path);
5190         values.put(FileColumns.PARENT, getParent(db, path));
5191         values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path));
5192         values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
5193         values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
5194         values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
5195         values.put(FileColumns.IS_DOWNLOAD, isDownload(path) ? 1 : 0);
5196 
5197         // Getting UserId from the directory path, as clone user shares the MediaProvider
5198         // of user 0.
5199         int userIdFromPath = FileUtils.extractUserId(path);
5200         // In some cases, like querying public volumes, userId is not available in path. We
5201         // take userId from the user running MediaProvider process (sUserId).
5202         if (userIdFromPath != -1) {
5203             if (isAppCloneUserForFuse(userIdFromPath)) {
5204                 values.put(FileColumns._USER_ID, userIdFromPath);
5205             } else {
5206                 values.put(FileColumns._USER_ID, sUserId);
5207             }
5208         }
5209 
5210         File file = new File(path);
5211         if (file.exists()) {
5212             values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
5213         }
5214         return db.insert("files", FileColumns.DATE_MODIFIED, values);
5215     }
5216 
getParent(@onNull SQLiteDatabase db, @NonNull String path)5217     private long getParent(@NonNull SQLiteDatabase db, @NonNull String path) {
5218         final String parentPath = new File(path).getParent();
5219         if (Objects.equals("/", parentPath)) {
5220             return -1;
5221         } else {
5222             synchronized (mDirectoryCache) {
5223                 Long id = mDirectoryCache.get(parentPath);
5224                 if (id != null) {
5225                     return id;
5226                 }
5227             }
5228 
5229             final long id;
5230             try (Cursor c = db.query("files", new String[] { FileColumns._ID },
5231                     FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) {
5232                 if (c.moveToFirst()) {
5233                     id = c.getLong(0);
5234                 } else {
5235                     id = insertDirectory(db, parentPath);
5236                 }
5237             }
5238 
5239             synchronized (mDirectoryCache) {
5240                 mDirectoryCache.put(parentPath, id);
5241             }
5242             return id;
5243         }
5244     }
5245 
5246     /**
5247      * @param c the Cursor whose title to retrieve
5248      * @return the result of {@link #getDefaultTitle(String)} if the result is valid; otherwise
5249      * the value of the {@code MediaStore.Audio.Media.TITLE} column
5250      */
getDefaultTitleFromCursor(Cursor c)5251     private String getDefaultTitleFromCursor(Cursor c) {
5252         String title = null;
5253         final int columnIndex = c.getColumnIndex("title_resource_uri");
5254         // Necessary to check for existence because we may be reading from an old DB version
5255         if (columnIndex > -1) {
5256             final String titleResourceUri = c.getString(columnIndex);
5257             if (titleResourceUri != null) {
5258                 try {
5259                     title = getDefaultTitle(titleResourceUri);
5260                 } catch (Exception e) {
5261                     // Best attempt only
5262                 }
5263             }
5264         }
5265         if (title == null) {
5266             title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE));
5267         }
5268         return title;
5269     }
5270 
5271     /**
5272      * @param title_resource_uri The title resource for which to retrieve the default localization
5273      * @return The title localized to {@code Locale.US}, or {@code null} if unlocalizable
5274      * @throws Exception Thrown if the title appears to be localizable, but the localization failed
5275      * for any reason. For example, the application from which the localized title is fetched is not
5276      * installed, or it does not have the resource which needs to be localized
5277      */
getDefaultTitle(String title_resource_uri)5278     private String getDefaultTitle(String title_resource_uri) throws Exception{
5279         try {
5280             return getTitleFromResourceUri(title_resource_uri, false);
5281         } catch (Exception e) {
5282             Log.e(TAG, "Error getting default title for " + title_resource_uri, e);
5283             throw e;
5284         }
5285     }
5286 
5287     /**
5288      * @param title_resource_uri The title resource to localize
5289      * @return The localized title, or {@code null} if unlocalizable
5290      * @throws Exception Thrown if the title appears to be localizable, but the localization failed
5291      * for any reason. For example, the application from which the localized title is fetched is not
5292      * installed, or it does not have the resource which needs to be localized
5293      */
getLocalizedTitle(String title_resource_uri)5294     private String getLocalizedTitle(String title_resource_uri) throws Exception {
5295         try {
5296             return getTitleFromResourceUri(title_resource_uri, true);
5297         } catch (Exception e) {
5298             Log.e(TAG, "Error getting localized title for " + title_resource_uri, e);
5299             throw e;
5300         }
5301     }
5302 
5303     /**
5304      * Localizable titles conform to this URI pattern:
5305      *   Scheme: {@link ContentResolver.SCHEME_ANDROID_RESOURCE}
5306      *   Authority: Package Name of ringtone title provider
5307      *   First Path Segment: Type of resource (must be "string")
5308      *   Second Path Segment: Resource name of title
5309      *
5310      * @param title_resource_uri The title resource to retrieve
5311      * @param localize Whether or not to localize the title
5312      * @return The title, or {@code null} if unlocalizable
5313      * @throws Exception Thrown if the title appears to be localizable, but the localization failed
5314      * for any reason. For example, the application from which the localized title is fetched is not
5315      * installed, or it does not have the resource which needs to be localized
5316      */
getTitleFromResourceUri(String title_resource_uri, boolean localize)5317     private String getTitleFromResourceUri(String title_resource_uri, boolean localize)
5318         throws Exception {
5319         if (TextUtils.isEmpty(title_resource_uri)) {
5320             return null;
5321         }
5322         final Uri titleUri = Uri.parse(title_resource_uri);
5323         final String scheme = titleUri.getScheme();
5324         if (!ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
5325             return null;
5326         }
5327         final List<String> pathSegments = titleUri.getPathSegments();
5328         if (pathSegments.size() != 2) {
5329             Log.e(TAG, "Error getting localized title for " + title_resource_uri
5330                 + ", must have 2 path segments");
5331             return null;
5332         }
5333         final String type = pathSegments.get(0);
5334         if (!"string".equals(type)) {
5335             Log.e(TAG, "Error getting localized title for " + title_resource_uri
5336                 + ", first path segment must be \"string\"");
5337             return null;
5338         }
5339         final String packageName = titleUri.getAuthority();
5340         final Resources resources;
5341         if (localize) {
5342             resources = mPackageManager.getResourcesForApplication(packageName);
5343         } else {
5344             final Context packageContext = getContext().createPackageContext(packageName, 0);
5345             final Configuration configuration = packageContext.getResources().getConfiguration();
5346             configuration.setLocale(Locale.US);
5347             resources = packageContext.createConfigurationContext(configuration).getResources();
5348         }
5349         final String resourceIdentifier = pathSegments.get(1);
5350         final int id = resources.getIdentifier(resourceIdentifier, type, packageName);
5351         return resources.getString(id);
5352     }
5353 
onLocaleChanged()5354     public void onLocaleChanged() {
5355         onLocaleChanged(true);
5356     }
5357 
onLocaleChanged(boolean forceUpdate)5358     private void onLocaleChanged(boolean forceUpdate) {
5359         mInternalDatabase.runWithTransaction((db) -> {
5360             if (forceUpdate || !mLastLocale.equals(Locale.getDefault())) {
5361                 localizeTitles(db);
5362                 mLastLocale = Locale.getDefault();
5363             }
5364             return null;
5365         });
5366     }
5367 
localizeTitles(@onNull SQLiteDatabase db)5368     private void localizeTitles(@NonNull SQLiteDatabase db) {
5369         try (Cursor c = db.query("files", new String[]{"_id", "title_resource_uri"},
5370             "title_resource_uri IS NOT NULL", null, null, null, null)) {
5371             while (c.moveToNext()) {
5372                 final String id = c.getString(0);
5373                 final String titleResourceUri = c.getString(1);
5374                 final ContentValues values = new ContentValues();
5375                 try {
5376                     values.put(AudioColumns.TITLE_RESOURCE_URI, titleResourceUri);
5377                     computeAudioLocalizedValues(values);
5378                     computeAudioKeyValues(values);
5379                     db.update("files", values, "_id=?", new String[]{id});
5380                 } catch (Exception e) {
5381                     Log.e(TAG, "Error updating localized title for " + titleResourceUri
5382                         + ", keeping old localization");
5383                 }
5384             }
5385         }
5386     }
5387 
insertFile(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, int mediaType)5388     private Uri insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper,
5389             int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values,
5390             int mediaType) throws VolumeArgumentException, VolumeNotFoundException {
5391         boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA)
5392                 || TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA));
5393 
5394         // Make sure all file-related columns are defined
5395         ensureUniqueFileColumns(match, uri, extras, values, null);
5396 
5397         switch (mediaType) {
5398             case FileColumns.MEDIA_TYPE_AUDIO: {
5399                 computeAudioLocalizedValues(values);
5400                 computeAudioKeyValues(values);
5401                 break;
5402             }
5403         }
5404 
5405         // compute bucket_id and bucket_display_name for all files
5406         String path = values.getAsString(MediaStore.MediaColumns.DATA);
5407         FileUtils.computeValuesFromData(values, isFuseThread());
5408         values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
5409 
5410         String title = values.getAsString(MediaStore.MediaColumns.TITLE);
5411         if (title == null && path != null) {
5412             title = extractFileName(path);
5413         }
5414         values.put(FileColumns.TITLE, title);
5415 
5416         String mimeType = null;
5417         int format = MtpConstants.FORMAT_ASSOCIATION;
5418         if (path != null && new File(path).isDirectory()) {
5419             values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
5420             values.putNull(MediaStore.MediaColumns.MIME_TYPE);
5421         } else {
5422             mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE);
5423             final Integer formatObject = values.getAsInteger(FileColumns.FORMAT);
5424             format = (formatObject == null ? 0 : formatObject);
5425         }
5426 
5427         if (format == 0) {
5428             format = MimeUtils.resolveFormatCode(mimeType);
5429         }
5430         if (path != null && path.endsWith("/")) {
5431             // TODO: convert to using FallbackException once VERSION_CODES.S is defined
5432             Log.e(TAG, "directory has trailing slash: " + path);
5433             return null;
5434         }
5435         if (format != 0) {
5436             values.put(FileColumns.FORMAT, format);
5437         }
5438 
5439         if (mimeType == null && path != null && format != MtpConstants.FORMAT_ASSOCIATION) {
5440             mimeType = MimeUtils.resolveMimeType(new File(path));
5441         }
5442 
5443         if (mimeType != null) {
5444             values.put(FileColumns.MIME_TYPE, mimeType);
5445             if (isCallingPackageSelf() && values.containsKey(FileColumns.MEDIA_TYPE)) {
5446                 // Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and
5447                 // FileColumns.MEDIA_TYPE is already populated.
5448             } else if (isFuseThread() && path != null
5449                     && FileUtils.shouldFileBeHidden(new File(path))) {
5450                 // We should only mark MEDIA_TYPE as MEDIA_TYPE_NONE for Fuse Thread.
5451                 // MediaProvider#insert() returns the uri by appending the "rowId" to the given
5452                 // uri, hence to ensure the correct working of the returned uri, we shouldn't
5453                 // change the MEDIA_TYPE in insert operation and let scan change it for us.
5454                 values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
5455             } else {
5456                 values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType));
5457             }
5458         } else {
5459             values.put(FileColumns.MEDIA_TYPE, mediaType);
5460         }
5461 
5462         qb.allowColumn(FileColumns._MODIFIER);
5463         if (isCallingPackageSelf() && values.containsKey(FileColumns._MODIFIER)) {
5464             // We can't identify if the call is coming from media scan, hence
5465             // we let ModernMediaScanner send FileColumns._MODIFIER value.
5466         } else if (isFuseThread()) {
5467             values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE);
5468         } else {
5469             values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR);
5470         }
5471 
5472         // There is no meaning of an owner in the internal storage. It is shared by all users.
5473         // So we only set the user_id field in the database for external storage.
5474         qb.allowColumn(FileColumns._USER_ID);
5475         int ownerUserId = FileUtils.extractUserId(path);
5476         if (helper.isExternal()) {
5477             if (isAppCloneUserForFuse(ownerUserId)) {
5478                 values.put(FileColumns._USER_ID, ownerUserId);
5479             } else {
5480                 values.put(FileColumns._USER_ID, sUserId);
5481             }
5482         }
5483 
5484         final long rowId;
5485         Uri newUri = uri;
5486         {
5487             if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
5488                 String name = values.getAsString(Audio.Playlists.NAME);
5489                 if (name == null && path == null) {
5490                     // MediaScanner will compute the name from the path if we have one
5491                     throw new IllegalArgumentException(
5492                             "no name was provided when inserting abstract playlist");
5493                 }
5494             } else {
5495                 if (path == null) {
5496                     // path might be null for playlists created on the device
5497                     // or transfered via MTP
5498                     throw new IllegalArgumentException(
5499                             "no path was provided when inserting new file");
5500                 }
5501             }
5502 
5503             // make sure modification date and size are set
5504             if (path != null) {
5505                 File file = new File(path);
5506                 if (file.exists()) {
5507                     values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
5508                     if (!values.containsKey(FileColumns.SIZE)) {
5509                         values.put(FileColumns.SIZE, file.length());
5510                     }
5511                 }
5512                 // Checking if the file/directory is hidden can be expensive based on the depth of
5513                 // the directory tree. Call shouldFileBeHidden() only when the caller of insert()
5514                 // cares about returned uri.
5515                 if (!isCallingPackageSelf() && !isFuseThread()
5516                         && FileUtils.shouldFileBeHidden(file)) {
5517                     newUri = MediaStore.Files.getContentUri(MediaStore.getVolumeName(uri));
5518                 }
5519             }
5520 
5521             rowId = insertAllowingUpsert(qb, helper, values, path);
5522         }
5523         if (format == MtpConstants.FORMAT_ASSOCIATION) {
5524             synchronized (mDirectoryCache) {
5525                 mDirectoryCache.put(path, rowId);
5526             }
5527         }
5528 
5529         return ContentUris.withAppendedId(newUri, rowId);
5530     }
5531 
5532     /**
5533      * Inserts a new row in MediaProvider database with {@code values}. Treats insert as upsert for
5534      * double inserts from same package.
5535      */
insertAllowingUpsert(@onNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)5536     private long insertAllowingUpsert(@NonNull SQLiteQueryBuilder qb,
5537             @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)
5538             throws SQLiteConstraintException {
5539         return helper.runWithTransaction((db) -> {
5540             Long parent = values.getAsLong(FileColumns.PARENT);
5541             if (parent == null) {
5542                 if (path != null) {
5543                     final long parentId = getParent(db, path);
5544                     values.put(FileColumns.PARENT, parentId);
5545                 }
5546             }
5547 
5548             try {
5549                 return qb.insert(helper, values);
5550             } catch (SQLiteConstraintException e) {
5551                 final String packages = getAllowedPackagesForUpsert(
5552                         values.getAsString(MediaColumns.OWNER_PACKAGE_NAME));
5553                 SQLiteQueryBuilder qbForUpsert = getQueryBuilderForUpsert(path);
5554                 final long rowId = getIdIfPathOwnedByPackages(qbForUpsert, helper, path, packages);
5555                 // Apps sometimes create a file via direct path and then insert it into
5556                 // MediaStore via ContentResolver. The former should create a database entry,
5557                 // so we have to treat the latter as an upsert.
5558                 // TODO(b/149917493) Perform all INSERT operations as UPSERT.
5559                 if (rowId != -1 && qbForUpsert.update(helper, values, "_id=?",
5560                         new String[]{Long.toString(rowId)}) == 1) {
5561                     return rowId;
5562                 }
5563                 // Rethrow SQLiteConstraintException on failed upsert.
5564                 throw e;
5565             }
5566         });
5567     }
5568 
5569     /**
5570      * @return row id of the entry with path {@code path} if the owner is one of {@code packages}.
5571      */
5572     private long getIdIfPathOwnedByPackages(@NonNull SQLiteQueryBuilder qb,
5573             @NonNull DatabaseHelper helper, String path, String packages) {
5574         final String[] projection = new String[] {FileColumns._ID};
5575         final  String ownerPackageMatchClause = DatabaseUtils.bindSelection(
5576                 MediaColumns.OWNER_PACKAGE_NAME + " IN " + packages);
5577         final String selection = FileColumns.DATA + " =? AND " + ownerPackageMatchClause;
5578 
5579         try (Cursor c = qb.query(helper, projection, selection, new String[] {path}, null, null,
5580                 null, null, null)) {
5581             if (c.moveToFirst()) {
5582                 return c.getLong(0);
5583             }
5584         }
5585         return -1;
5586     }
5587 
5588     /**
5589      * Gets packages that should match to upsert a db row.
5590      *
5591      * A database row can be upserted if
5592      * <ul>
5593      * <li> Calling package or one of the shared packages owns the db row.
5594      * <li> {@code givenOwnerPackage} owns the db row. This is useful when DownloadProvider
5595      * requests upsert on behalf of another app
5596      * </ul>
5597      */
5598     private String getAllowedPackagesForUpsert(@Nullable String givenOwnerPackage) {
5599         ArrayList<String> packages = new ArrayList<>(
5600                 Arrays.asList(mCallingIdentity.get().getSharedPackageNamesArray()));
5601 
5602         // If givenOwnerPackage is CallingIdentity, packages list would already have shared package
5603         // names of givenOwnerPackage. If givenOwnerPackage is not CallingIdentity, since
5604         // DownloadProvider can upsert a row on behalf of app, we should include all shared packages
5605         // of givenOwnerPackage.
5606         if (givenOwnerPackage != null && isCallingPackageDelegator() &&
5607                 !isCallingIdentitySharedPackageName(givenOwnerPackage)) {
5608             // Allow DownloadProvider to Upsert if givenOwnerPackage is owner of the db row.
5609             packages.addAll(Arrays.asList(getSharedPackagesForPackage(givenOwnerPackage)));
5610         }
5611         return bindList((Object[]) packages.toArray());
5612     }
5613 
5614     /**
5615      * @return {@link SQLiteQueryBuilder} for upsert with Files uri. This disables strict columns
5616      * check to allow upsert to update any column with Files uri.
5617      */
5618     private SQLiteQueryBuilder getQueryBuilderForUpsert(@NonNull String path) {
5619         final boolean allowHidden = isCallingPackageAllowedHidden();
5620         Bundle extras = new Bundle();
5621         extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE);
5622         extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE);
5623 
5624         // When Fuse inserts a file to database it doesn't set is_download column. When app tries
5625         // insert with Downloads uri, upsert fails because getIdIfPathExistsForCallingPackage can't
5626         // find a row ID with is_download=1. Use Files uri to get queryBuilder & update any existing
5627         // row irrespective of is_download=1.
5628         final Uri uri = FileUtils.getContentUriForPath(path);
5629         SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri,
5630                 extras, null);
5631 
5632         // We won't be able to update columns that are not part of projection map of Files table. We
5633         // have already checked strict columns in previous insert operation which failed with
5634         // exception. Any malicious column usage would have got caught in insert operation, hence we
5635         // can safely disable strict column check for upsert.
5636         qb.setStrictColumns(false);
5637         return qb;
5638     }
5639 
5640     private void maybePut(@NonNull ContentValues values, @NonNull String key,
5641             @Nullable String value) {
5642         if (value != null) {
5643             values.put(key, value);
5644         }
5645     }
5646 
5647     private boolean maybeMarkAsDownload(@NonNull ContentValues values) {
5648         final String path = values.getAsString(MediaColumns.DATA);
5649         if (path != null && isDownload(path)) {
5650             values.put(FileColumns.IS_DOWNLOAD, 1);
5651             return true;
5652         }
5653         return false;
5654     }
5655 
5656     @NonNull
5657     private static String resolveVolumeName(@NonNull Uri uri) {
5658         final String volumeName = getVolumeName(uri);
5659         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
5660             return MediaStore.VOLUME_EXTERNAL_PRIMARY;
5661         } else {
5662             return volumeName;
5663         }
5664     }
5665 
5666     /**
5667      * @deprecated all operations should be routed through the overload that
5668      *             accepts a {@link Bundle} of extras.
5669      */
5670     @Override
5671     @Deprecated
5672     public Uri insert(Uri uri, ContentValues values) {
5673         return insert(uri, values, null);
5674     }
5675 
5676     @Override
5677     @Nullable
5678     public Uri insert(@NonNull Uri uri, @Nullable ContentValues values,
5679             @Nullable Bundle extras) {
5680         Trace.beginSection(safeTraceSectionNameWithUri("insert", uri));
5681         try {
5682             try {
5683                 return insertInternal(uri, values, extras);
5684             } catch (SQLiteConstraintException e) {
5685                 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
5686                     throw e;
5687                 } else {
5688                     return null;
5689                 }
5690             }
5691         } catch (FallbackException e) {
5692             return e.translateForInsert(getCallingPackageTargetSdkVersion());
5693         } finally {
5694             Trace.endSection();
5695         }
5696     }
5697 
5698     @Nullable
5699     private Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
5700             @Nullable Bundle extras) throws FallbackException {
5701         if (shouldCheckForMaliciousActivity() && !mMaliciousAppDetector.isAppAllowedToCreateFiles(
5702                 mCallingIdentity.get().uid)) {
5703             Log.w(TAG, "Cannot be created, app has created files more than threshold limit of "
5704                     + mMaliciousAppDetector.getFileCreationThresholdLimit());
5705             throw new UnsupportedOperationException(
5706                     "Cannot be created, app has created files more than threshold limit");
5707         }
5708         final String originalVolumeName = getVolumeName(uri);
5709         PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), originalVolumeName);
5710 
5711         extras = (extras != null) ? extras : new Bundle();
5712         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
5713         extras.remove(QUERY_ARG_REDACTED_URI);
5714 
5715         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
5716         extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
5717 
5718         final boolean allowHidden = isCallingPackageAllowedHidden();
5719         final int match = matchUri(uri, allowHidden);
5720 
5721         final String resolvedVolumeName = resolveVolumeName(uri);
5722 
5723         // handle MEDIA_SCANNER before calling getDatabaseForUri()
5724         if (match == MEDIA_SCANNER) {
5725             mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);
5726 
5727             final DatabaseHelper helper = getDatabaseForUri(
5728                     MediaStore.Files.getContentUri(mMediaScannerVolume));
5729 
5730             helper.mScanStartTime = SystemClock.elapsedRealtime();
5731             return MediaStore.getMediaScannerUri();
5732         }
5733 
5734         if (match == VOLUMES) {
5735             String name = initialValues.getAsString("name");
5736             MediaVolume volume = null;
5737             try {
5738                 volume = getVolume(name);
5739                 Uri attachedVolume = attachVolume(volume, /* validate */ true, /* volumeState */
5740                         null);
5741                 if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) {
5742                     final DatabaseHelper helper = getDatabaseForUri(
5743                             MediaStore.Files.getContentUri(mMediaScannerVolume));
5744                     helper.mScanStartTime = SystemClock.elapsedRealtime();
5745                 }
5746                 return attachedVolume;
5747             } catch (FileNotFoundException e) {
5748                 Log.w(TAG, "Couldn't find volume with name " + volume.getName());
5749                 return null;
5750             }
5751         }
5752 
5753         final DatabaseHelper helper = getDatabaseForUri(uri);
5754         switch (match) {
5755             case AUDIO_PLAYLISTS_ID:
5756             case AUDIO_PLAYLISTS_ID_MEMBERS: {
5757                 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
5758                 final Uri playlistUri = ContentUris.withAppendedId(
5759                         MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId);
5760 
5761                 final long audioId = initialValues
5762                         .getAsLong(MediaStore.Audio.Playlists.Members.AUDIO_ID);
5763                 final String audioVolumeName =
5764                         MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)
5765                                 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL;
5766                 final Uri audioUri = ContentUris.withAppendedId(
5767                         MediaStore.Audio.Media.getContentUri(audioVolumeName), audioId);
5768 
5769                 // Require that caller has write access to underlying media
5770                 enforceCallingPermission(playlistUri, Bundle.EMPTY, true);
5771                 enforceCallingPermission(audioUri, Bundle.EMPTY, false);
5772 
5773                 // Playlist contents are always persisted directly into playlist
5774                 // files on disk to ensure that we can reliably migrate between
5775                 // devices and recover from database corruption
5776                 final long id = addPlaylistMembers(playlistUri, initialValues);
5777                 acceptWithExpansion(helper::notifyInsert, resolvedVolumeName, playlistId,
5778                         FileColumns.MEDIA_TYPE_PLAYLIST, false);
5779                 return ContentUris.withAppendedId(MediaStore.Audio.Playlists.Members
5780                         .getContentUri(originalVolumeName, playlistId), id);
5781             }
5782         }
5783 
5784         String path = null;
5785         String ownerPackageName = null;
5786         if (initialValues != null) {
5787             // IDs are forever; nobody should be editing them
5788             initialValues.remove(MediaColumns._ID);
5789 
5790             // Expiration times are hard-coded; let's derive them
5791             FileUtils.computeDateExpires(initialValues);
5792 
5793             // Ignore or augment incoming raw filesystem paths
5794             for (String column : sDataColumns.keySet()) {
5795                 if (!initialValues.containsKey(column)) continue;
5796 
5797                 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
5798                     // Mutation allowed
5799                 } else if (isCallingPackageManager()) {
5800                     // Apps with MANAGE_EXTERNAL_STORAGE have all files access, hence they are
5801                     // allowed to insert files anywhere.
5802                 } else if (getCallingPackageTargetSdkVersion() >=
5803                         Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
5804                     // Throwing an exception so that it doesn't result in some unexpected
5805                     // behavior for apps and make them aware of what is happening.
5806                     throw new IllegalArgumentException("Mutation of " + column
5807                         + " is not allowed.");
5808                 } else {
5809                     Log.w(TAG, "Ignoring mutation of  " + column + " from "
5810                         + getCallingPackageOrSelf());
5811                     initialValues.remove(column);
5812                 }
5813             }
5814 
5815             path = initialValues.getAsString(MediaStore.MediaColumns.DATA);
5816 
5817             if (!isCallingPackageSelf()) {
5818                 initialValues.remove(FileColumns.IS_DOWNLOAD);
5819 
5820                 // We no longer track location metadata
5821                 if (initialValues.containsKey(LATITUDE)) {
5822                     initialValues.putNull(LATITUDE);
5823                 }
5824                 if (initialValues.containsKey(LONGITUDE)) {
5825                     initialValues.putNull(LONGITUDE);
5826                 }
5827             }
5828 
5829             if (getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) {
5830                 // These columns are removed in R.
5831                 if (initialValues.containsKey("primary_directory")) {
5832                     initialValues.remove("primary_directory");
5833                 }
5834                 if (initialValues.containsKey("secondary_directory")) {
5835                     initialValues.remove("secondary_directory");
5836                 }
5837             }
5838 
5839             if (isCallingPackageSelf() || isCallingPackageShell()) {
5840                 // When media inserted by ourselves during a scan, or by the
5841                 // shell, the best we can do is guess ownership based on path
5842                 // when it's not explicitly provided
5843                 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
5844                 if (TextUtils.isEmpty(ownerPackageName)) {
5845                     ownerPackageName = extractPathOwnerPackageName(path);
5846                 }
5847             } else if (isCallingPackageDelegator()) {
5848                 // When caller is a delegator, we handle ownership as a hybrid
5849                 // of the two other cases: we're willing to accept any ownership
5850                 // transfer attempted during insert, but we fall back to using
5851                 // the Binder identity if they don't request a specific owner
5852                 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
5853                 if (TextUtils.isEmpty(ownerPackageName)) {
5854                     ownerPackageName = getCallingPackageOrSelf();
5855                 }
5856             } else {
5857                 // Remote callers have no direct control over owner column; we force
5858                 // it be whoever is creating the content.
5859                 initialValues.remove(FileColumns.OWNER_PACKAGE_NAME);
5860                 ownerPackageName = getCallingPackageOrSelf();
5861             }
5862         }
5863 
5864 
5865         // Enforce oem_metadata permission if caller is not MediaProvider
5866         if (Flags.enableOemMetadataUpdate() && initialValues.containsKey(OEM_METADATA)) {
5867             enforcePermissionCheckForOemMetadataUpdate();
5868         }
5869 
5870         long rowId = -1;
5871         Uri newUri = null;
5872 
5873         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null);
5874 
5875         switch (match) {
5876             case IMAGES_MEDIA: {
5877                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
5878                 final boolean isDownload = maybeMarkAsDownload(initialValues);
5879                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
5880                         FileColumns.MEDIA_TYPE_IMAGE);
5881                 break;
5882             }
5883 
5884             case IMAGES_THUMBNAILS: {
5885                 if (helper.isInternal()) {
5886                     throw new UnsupportedOperationException(
5887                             "Writing to internal storage is not supported.");
5888                 }
5889 
5890                 // Require that caller has write access to underlying media
5891                 final long imageId = initialValues.getAsLong(MediaStore.Images.Thumbnails.IMAGE_ID);
5892                 enforceCallingPermission(ContentUris.withAppendedId(
5893                         MediaStore.Images.Media.getContentUri(resolvedVolumeName), imageId),
5894                         extras, true);
5895 
5896                 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
5897 
5898                 rowId = qb.insert(helper, initialValues);
5899                 if (rowId > 0) {
5900                     newUri = ContentUris.withAppendedId(Images.Thumbnails.
5901                             getContentUri(originalVolumeName), rowId);
5902                 }
5903                 break;
5904             }
5905 
5906             case VIDEO_THUMBNAILS: {
5907                 if (helper.isInternal()) {
5908                     throw new UnsupportedOperationException(
5909                             "Writing to internal storage is not supported.");
5910                 }
5911 
5912                 // Require that caller has write access to underlying media
5913                 final long videoId = initialValues.getAsLong(MediaStore.Video.Thumbnails.VIDEO_ID);
5914                 enforceCallingPermission(ContentUris.withAppendedId(
5915                         MediaStore.Video.Media.getContentUri(resolvedVolumeName), videoId),
5916                         Bundle.EMPTY, true);
5917 
5918                 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
5919 
5920                 rowId = qb.insert(helper, initialValues);
5921                 if (rowId > 0) {
5922                     newUri = ContentUris.withAppendedId(Video.Thumbnails.
5923                             getContentUri(originalVolumeName), rowId);
5924                 }
5925                 break;
5926             }
5927 
5928             case AUDIO_MEDIA: {
5929                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
5930                 final boolean isDownload = maybeMarkAsDownload(initialValues);
5931                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
5932                         FileColumns.MEDIA_TYPE_AUDIO);
5933                 break;
5934             }
5935 
5936             case AUDIO_MEDIA_ID_GENRES: {
5937                 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
5938             }
5939 
5940             case AUDIO_GENRES: {
5941                 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
5942             }
5943 
5944             case AUDIO_GENRES_ID_MEMBERS: {
5945                 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
5946             }
5947 
5948             case AUDIO_PLAYLISTS: {
5949                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
5950                 final boolean isDownload = maybeMarkAsDownload(initialValues);
5951                 ContentValues values = new ContentValues(initialValues);
5952                 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
5953                 // Playlist names are stored as display names, but leave
5954                 // values untouched if the caller is ModernMediaScanner
5955                 if (!isCallingPackageSelf()) {
5956                     if (values.containsKey(Playlists.NAME)) {
5957                         values.put(MediaColumns.DISPLAY_NAME, values.getAsString(Playlists.NAME));
5958                     }
5959                     if (!values.containsKey(MediaColumns.MIME_TYPE)) {
5960                         values.put(MediaColumns.MIME_TYPE, "audio/mpegurl");
5961                     }
5962                 }
5963                 newUri = insertFile(qb, helper, match, uri, extras, values,
5964                         FileColumns.MEDIA_TYPE_PLAYLIST);
5965                 if (newUri != null) {
5966                     // Touch empty playlist file on disk so its ready for renames
5967                     if (Binder.getCallingUid() != android.os.Process.myUid()) {
5968                         try (OutputStream out = ContentResolver.wrap(this)
5969                                 .openOutputStream(newUri)) {
5970                         } catch (IOException ignored) {
5971                         }
5972                     }
5973                 }
5974                 break;
5975             }
5976 
5977             case VIDEO_MEDIA: {
5978                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
5979                 final boolean isDownload = maybeMarkAsDownload(initialValues);
5980                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
5981                         FileColumns.MEDIA_TYPE_VIDEO);
5982                 break;
5983             }
5984 
5985             case AUDIO_ALBUMART: {
5986                 if (helper.isInternal()) {
5987                     throw new UnsupportedOperationException("no internal album art allowed");
5988                 }
5989 
5990                 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
5991 
5992                 rowId = qb.insert(helper, initialValues);
5993                 if (rowId > 0) {
5994                     newUri = ContentUris.withAppendedId(uri, rowId);
5995                 }
5996                 break;
5997             }
5998 
5999             case FILES: {
6000                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
6001                 final boolean isDownload = maybeMarkAsDownload(initialValues);
6002                 final String mimeType = initialValues.getAsString(MediaColumns.MIME_TYPE);
6003                 final int mediaType = MimeUtils.resolveMediaType(mimeType);
6004                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
6005                         mediaType);
6006                 break;
6007             }
6008 
6009             case DOWNLOADS:
6010                 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
6011                 initialValues.put(FileColumns.IS_DOWNLOAD, 1);
6012                 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
6013                         FileColumns.MEDIA_TYPE_NONE);
6014                 break;
6015 
6016             default:
6017                 throw new UnsupportedOperationException("Invalid URI " + uri);
6018         }
6019 
6020         // Remember that caller is owner of this item, to speed up future
6021         // permission checks for this caller
6022         mCallingIdentity.get().setOwned(rowId, true);
6023 
6024         if (path != null && path.toLowerCase(Locale.ROOT).endsWith("/.nomedia")) {
6025             scanFileAsMediaProvider(new File(path).getParentFile());
6026         }
6027 
6028         return newUri;
6029     }
6030 
6031     @Override
6032     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
6033                 throws OperationApplicationException {
6034         // Open transactions on databases for requested volumes
6035         final Set<DatabaseHelper> transactions = new ArraySet<>();
6036         try {
6037             for (ContentProviderOperation op : operations) {
6038                 final DatabaseHelper helper = getDatabaseForUri(op.getUri());
6039                 if (transactions.contains(helper)) continue;
6040 
6041                 if (!helper.isTransactionActive()) {
6042                     helper.beginTransaction();
6043                     transactions.add(helper);
6044                 } else {
6045                     // We normally don't allow nested transactions (since we
6046                     // don't have a good way to selectively roll them back) but
6047                     // if the incoming operation is ignoring exceptions, then we
6048                     // don't need to worry about partial rollback and can
6049                     // piggyback on the larger active transaction
6050                     if (!op.isExceptionAllowed()) {
6051                         throw new IllegalStateException("Nested transactions not supported");
6052                     }
6053                 }
6054             }
6055 
6056             final ContentProviderResult[] result = super.applyBatch(operations);
6057             for (DatabaseHelper helper : transactions) {
6058                 helper.setTransactionSuccessful();
6059             }
6060             return result;
6061         } catch (VolumeNotFoundException e) {
6062             throw e.rethrowAsIllegalArgumentException();
6063         } finally {
6064             for (DatabaseHelper helper : transactions) {
6065                 helper.endTransaction();
6066             }
6067         }
6068     }
6069 
6070     private void appendWhereStandaloneMatch(@NonNull SQLiteQueryBuilder qb,
6071             @NonNull String column, /* @Match */ int match, Uri uri) {
6072         switch (match) {
6073             case MATCH_INCLUDE:
6074                 // No special filtering needed
6075                 break;
6076             case MATCH_EXCLUDE:
6077                 appendWhereStandalone(qb, getWhereClauseForMatchExclude(column));
6078                 break;
6079             case MATCH_ONLY:
6080                 appendWhereStandalone(qb, column + "=?", 1);
6081                 break;
6082             case MATCH_VISIBLE_FOR_FILEPATH:
6083                 final String whereClause =
6084                         getWhereClauseForMatchableVisibleFromFilePath(uri, column);
6085                 if (whereClause != null) {
6086                     appendWhereStandalone(qb, whereClause);
6087                 }
6088                 break;
6089             default:
6090                 throw new IllegalArgumentException();
6091         }
6092     }
6093 
6094     private static void appendWhereStandalone(@NonNull SQLiteQueryBuilder qb,
6095             @Nullable String selection, @Nullable Object... selectionArgs) {
6096         qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs));
6097     }
6098 
6099     private static void appendWhereStandaloneFilter(@NonNull SQLiteQueryBuilder qb,
6100             @NonNull String[] columns, @Nullable String filter) {
6101         if (TextUtils.isEmpty(filter)) return;
6102         for (String filterWord : filter.split("\\s+")) {
6103             appendWhereStandalone(qb, String.join("||", columns) + " LIKE ? ESCAPE '\\'",
6104                     "%" + DatabaseUtils.escapeForLike(Audio.keyFor(filterWord)) + "%");
6105         }
6106     }
6107 
6108     /**
6109      * Gets {@link LocalCallingIdentity} for the calling package
6110      * TODO(b/170465810) Change the method name after refactoring.
6111      */
6112     LocalCallingIdentity getCachedCallingIdentityForTranscoding(int uid) {
6113         return getCachedCallingIdentityForFuse(uid);
6114     }
6115 
6116     /**
6117      * Gets shared packages names for given {@code packageName}
6118      */
6119     private String[] getSharedPackagesForPackage(String packageName) {
6120         try {
6121             final int packageUid = getContext().getPackageManager()
6122                     .getPackageUid(packageName, 0);
6123             return getContext().getPackageManager().getPackagesForUid(packageUid);
6124         } catch (NameNotFoundException ignored) {
6125             return new String[] {packageName};
6126         }
6127     }
6128 
6129     private static final int TYPE_QUERY = 0;
6130     private static final int TYPE_INSERT = 1;
6131     private static final int TYPE_UPDATE = 2;
6132     private static final int TYPE_DELETE = 3;
6133 
6134     /**
6135      * Creating a new method for Transcoding to avoid any merge conflicts.
6136      * TODO(b/170465810): Remove this when getQueryBuilder code is refactored.
6137      */
6138     @NonNull SQLiteQueryBuilder getQueryBuilderForTranscoding(int type, int match,
6139             @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) {
6140         // Force MediaProvider calling identity when accessing the db from transcoding to avoid
6141         // generating 'strict' SQL e.g forcing owner_package_name matches
6142         // We already handle the required permission checks for the app before we get here
6143         final LocalCallingIdentity token = clearLocalCallingIdentity();
6144         try {
6145             return getQueryBuilder(type, match, uri, extras, honored);
6146         } finally {
6147             restoreLocalCallingIdentity(token);
6148         }
6149     }
6150 
6151     /**
6152      * Generate a {@link SQLiteQueryBuilder} that is filtered based on the
6153      * runtime permissions and/or {@link Uri} grants held by the caller.
6154      * <ul>
6155      * <li>If caller holds a {@link Uri} grant, access is allowed according to
6156      * that grant.
6157      * <li>If caller holds the write permission for a collection, they can
6158      * read/write all contents of that collection.
6159      * <li>If caller holds the read permission for a collection, they can read
6160      * all contents of that collection, but writes are limited to content they
6161      * own.
6162      * <li>If caller holds no permissions for a collection, all reads/write are
6163      * limited to content they own.
6164      * </ul>
6165      */
6166     private @NonNull SQLiteQueryBuilder getQueryBuilder(int type, int match,
6167             @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) {
6168         Trace.beginSection("MP.getQueryBuilder");
6169         try {
6170             return getQueryBuilderInternal(type, match, uri, extras, honored);
6171         } finally {
6172             Trace.endSection();
6173         }
6174     }
6175 
6176     private @NonNull SQLiteQueryBuilder getQueryBuilderInternal(int type, int match,
6177             @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) {
6178         final boolean forWrite;
6179         switch (type) {
6180             case TYPE_QUERY: forWrite = false; break;
6181             case TYPE_INSERT: forWrite = true; break;
6182             case TYPE_UPDATE: forWrite = true; break;
6183             case TYPE_DELETE: forWrite = true; break;
6184             default: throw new IllegalStateException();
6185         }
6186 
6187         if (forWrite) {
6188             final Uri redactedUri = extras.getParcelable(QUERY_ARG_REDACTED_URI);
6189             if (redactedUri != null) {
6190                 throw new UnsupportedOperationException(
6191                         "Writes on: " + redactedUri.toString() + " are not supported");
6192             }
6193         }
6194 
6195         final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
6196         if (uri.getBooleanQueryParameter("distinct", false)) {
6197             qb.setDistinct(true);
6198         }
6199         qb.setStrict(true);
6200         if (isCallingPackageSelf()) {
6201             // When caller is system, such as the media scanner, we're willing
6202             // to let them access any columns they want
6203         } else {
6204             qb.setTargetSdkVersion(getCallingPackageTargetSdkVersion());
6205             qb.setStrictColumns(true);
6206             qb.setStrictGrammar(true);
6207         }
6208 
6209         // TODO: throw when requesting a currently unmounted volume
6210         final String volumeName = MediaStore.getVolumeName(uri);
6211         final String includeVolumes;
6212         if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
6213             includeVolumes = bindList(mVolumeCache.getExternalVolumeNames().toArray());
6214         } else {
6215             includeVolumes = bindList(volumeName);
6216         }
6217 
6218         int matchPending = extras.getInt(QUERY_ARG_MATCH_PENDING, MATCH_DEFAULT);
6219         int matchTrashed = extras.getInt(QUERY_ARG_MATCH_TRASHED, MATCH_DEFAULT);
6220         int matchFavorite = extras.getInt(QUERY_ARG_MATCH_FAVORITE, MATCH_DEFAULT);
6221 
6222 
6223         // Handle callers using legacy arguments
6224         if (MediaStore.getIncludePending(uri)) matchPending = MATCH_INCLUDE;
6225 
6226         // Resolve any remaining default options
6227         final int defaultMatchForPendingAndTrashed;
6228         if (isFuseThread()) {
6229             // Write operations always check for file ownership, we don't need additional write
6230             // permission check for is_pending and is_trashed.
6231             defaultMatchForPendingAndTrashed =
6232                     forWrite ? MATCH_INCLUDE : MATCH_VISIBLE_FOR_FILEPATH;
6233         } else {
6234             defaultMatchForPendingAndTrashed = MATCH_EXCLUDE;
6235         }
6236         if (matchPending == MATCH_DEFAULT) matchPending = defaultMatchForPendingAndTrashed;
6237         if (matchTrashed == MATCH_DEFAULT) matchTrashed = defaultMatchForPendingAndTrashed;
6238         if (matchFavorite == MATCH_DEFAULT) matchFavorite = MATCH_INCLUDE;
6239 
6240         // Handle callers using legacy filtering
6241         final String filter = uri.getQueryParameter("filter");
6242 
6243         // Only accept ALL_VOLUMES parameter up until R, because we're not convinced we want
6244         // to commit to this as an API.
6245         final boolean includeAllVolumes = shouldIncludeRecentlyUnmountedVolumes(uri, extras);
6246 
6247         appendAccessCheckQuery(qb, forWrite, uri, match, extras, volumeName);
6248 
6249         switch (match) {
6250             case IMAGES_MEDIA_ID:
6251                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
6252                 matchPending = MATCH_INCLUDE;
6253                 matchTrashed = MATCH_INCLUDE;
6254                 // fall-through
6255             case IMAGES_MEDIA: {
6256                 if (type == TYPE_QUERY) {
6257                     qb.setTables("images");
6258                     qb.setProjectionMap(
6259                             getProjectionMap(Images.Media.class));
6260                 } else {
6261                     qb.setTables("files");
6262                     qb.setProjectionMap(
6263                             getProjectionMap(Images.Media.class, Files.FileColumns.class));
6264                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
6265                             FileColumns.MEDIA_TYPE_IMAGE);
6266                 }
6267                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
6268                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
6269                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
6270                 if (honored != null) {
6271                     honored.accept(QUERY_ARG_MATCH_PENDING);
6272                     honored.accept(QUERY_ARG_MATCH_TRASHED);
6273                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
6274                 }
6275                 if (!includeAllVolumes) {
6276                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
6277                 }
6278                 break;
6279             }
6280             case IMAGES_THUMBNAILS_ID:
6281                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
6282                 // fall-through
6283             case IMAGES_THUMBNAILS: {
6284                 qb.setTables("thumbnails");
6285 
6286                 final ArrayMap<String, String> projectionMap = new ArrayMap<>(
6287                         getProjectionMap(Images.Thumbnails.class));
6288                 projectionMap.put(Images.Thumbnails.THUMB_DATA,
6289                         "NULL AS " + Images.Thumbnails.THUMB_DATA);
6290                 qb.setProjectionMap(projectionMap);
6291 
6292                 break;
6293             }
6294             case AUDIO_MEDIA_ID:
6295                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
6296                 matchPending = MATCH_INCLUDE;
6297                 matchTrashed = MATCH_INCLUDE;
6298                 // fall-through
6299             case AUDIO_MEDIA: {
6300                 if (type == TYPE_QUERY) {
6301                     qb.setTables("audio");
6302                     qb.setProjectionMap(
6303                             getProjectionMap(Audio.Media.class));
6304                 } else {
6305                     qb.setTables("files");
6306                     qb.setProjectionMap(
6307                             getProjectionMap(Audio.Media.class, Files.FileColumns.class));
6308                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
6309                             FileColumns.MEDIA_TYPE_AUDIO);
6310                 }
6311                 appendWhereStandaloneFilter(qb, new String[] {
6312                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
6313                 }, filter);
6314                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
6315                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
6316                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
6317                 if (honored != null) {
6318                     honored.accept(QUERY_ARG_MATCH_PENDING);
6319                     honored.accept(QUERY_ARG_MATCH_TRASHED);
6320                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
6321                 }
6322                 if (!includeAllVolumes) {
6323                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
6324                 }
6325                 break;
6326             }
6327             case AUDIO_MEDIA_ID_GENRES_ID:
6328                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5));
6329                 // fall-through
6330             case AUDIO_MEDIA_ID_GENRES: {
6331                 if (type == TYPE_QUERY) {
6332                     qb.setTables("audio_genres");
6333                     qb.setProjectionMap(getProjectionMap(Audio.Genres.class));
6334                 } else {
6335                     throw new UnsupportedOperationException("Genres cannot be directly modified");
6336                 }
6337                 appendWhereStandalone(qb, "_id IN (SELECT genre_id FROM " +
6338                         "audio WHERE _id=?)", uri.getPathSegments().get(3));
6339                 break;
6340             }
6341             case AUDIO_GENRES_ID:
6342                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
6343                 // fall-through
6344             case AUDIO_GENRES: {
6345                 qb.setTables("audio_genres");
6346                 qb.setProjectionMap(getProjectionMap(Audio.Genres.class));
6347                 break;
6348             }
6349             case AUDIO_GENRES_ID_MEMBERS:
6350                 appendWhereStandalone(qb, "genre_id=?", uri.getPathSegments().get(3));
6351                 // fall-through
6352             case AUDIO_GENRES_ALL_MEMBERS: {
6353                 if (type == TYPE_QUERY) {
6354                     qb.setTables("audio");
6355 
6356                     final ArrayMap<String, String> projectionMap = new ArrayMap<>(
6357                             getProjectionMap(Audio.Genres.Members.class));
6358                     projectionMap.put(Audio.Genres.Members.AUDIO_ID,
6359                             "_id AS " + Audio.Genres.Members.AUDIO_ID);
6360                     qb.setProjectionMap(projectionMap);
6361                 } else {
6362                     throw new UnsupportedOperationException("Genres cannot be directly modified");
6363                 }
6364                 appendWhereStandaloneFilter(qb, new String[] {
6365                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
6366                 }, filter);
6367                 // In order to be consistent with other audio views like audio_artist, audio_albums,
6368                 // and audio_genres, exclude pending and trashed item
6369                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, MATCH_EXCLUDE, uri);
6370                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, MATCH_EXCLUDE, uri);
6371                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
6372                 if (honored != null) {
6373                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
6374                 }
6375                 if (!includeAllVolumes) {
6376                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
6377                 }
6378                 break;
6379             }
6380             case AUDIO_PLAYLISTS_ID:
6381                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
6382                 matchPending = MATCH_INCLUDE;
6383                 matchTrashed = MATCH_INCLUDE;
6384                 // fall-through
6385             case AUDIO_PLAYLISTS: {
6386                 if (type == TYPE_QUERY) {
6387                     qb.setTables("audio_playlists");
6388                     qb.setProjectionMap(
6389                             getProjectionMap(Audio.Playlists.class));
6390                 } else {
6391                     qb.setTables("files");
6392                     qb.setProjectionMap(
6393                             getProjectionMap(Audio.Playlists.class, Files.FileColumns.class));
6394                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
6395                             FileColumns.MEDIA_TYPE_PLAYLIST);
6396                 }
6397                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
6398                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
6399                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
6400                 if (honored != null) {
6401                     honored.accept(QUERY_ARG_MATCH_PENDING);
6402                     honored.accept(QUERY_ARG_MATCH_TRASHED);
6403                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
6404                 }
6405                 if (!includeAllVolumes) {
6406                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
6407                 }
6408                 break;
6409             }
6410             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
6411                 appendWhereStandalone(qb, "audio_playlists_map._id=?",
6412                         uri.getPathSegments().get(5));
6413                 // fall-through
6414             case AUDIO_PLAYLISTS_ID_MEMBERS: {
6415                 appendWhereStandalone(qb, "playlist_id=?", uri.getPathSegments().get(3));
6416                 if (type == TYPE_QUERY) {
6417                     qb.setTables("audio_playlists_map, audio");
6418 
6419                     final ArrayMap<String, String> projectionMap = new ArrayMap<>(
6420                             getProjectionMap(Audio.Playlists.Members.class));
6421                     projectionMap.put(Audio.Playlists.Members._ID,
6422                             "audio_playlists_map._id AS " + Audio.Playlists.Members._ID);
6423                     qb.setProjectionMap(projectionMap);
6424 
6425                     appendWhereStandalone(qb, "audio._id = audio_id");
6426                     // Since we use audio table along with audio_playlists_map
6427                     // for querying, we should only include database rows of
6428                     // the attached volumes.
6429                     if (!includeAllVolumes) {
6430                         appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN "
6431                              + includeVolumes);
6432                     }
6433                 } else {
6434                     qb.setTables("audio_playlists_map");
6435                     qb.setProjectionMap(getProjectionMap(Audio.Playlists.Members.class));
6436                 }
6437                 appendWhereStandaloneFilter(qb, new String[] {
6438                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
6439                 }, filter);
6440                 break;
6441             }
6442             case AUDIO_ALBUMART_ID:
6443                 appendWhereStandalone(qb, "album_id=?", uri.getPathSegments().get(3));
6444                 // fall-through
6445             case AUDIO_ALBUMART: {
6446                 qb.setTables("album_art");
6447 
6448                 final ArrayMap<String, String> projectionMap = new ArrayMap<>(
6449                         getProjectionMap(Audio.Thumbnails.class));
6450                 projectionMap.put(Audio.Thumbnails._ID,
6451                         "album_id AS " + Audio.Thumbnails._ID);
6452                 qb.setProjectionMap(projectionMap);
6453 
6454                 break;
6455             }
6456             case AUDIO_ARTISTS_ID_ALBUMS: {
6457                 if (type == TYPE_QUERY) {
6458                     qb.setTables("audio_artists_albums");
6459                     qb.setProjectionMap(getProjectionMap(Audio.Artists.Albums.class));
6460 
6461                     final String artistId = uri.getPathSegments().get(3);
6462                     appendWhereStandalone(qb, "artist_id=?", artistId);
6463                 } else {
6464                     throw new UnsupportedOperationException("Albums cannot be directly modified");
6465                 }
6466                 appendWhereStandaloneFilter(qb, new String[] {
6467                         AudioColumns.ALBUM_KEY
6468                 }, filter);
6469                 break;
6470             }
6471             case AUDIO_ARTISTS_ID:
6472                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
6473                 // fall-through
6474             case AUDIO_ARTISTS: {
6475                 if (type == TYPE_QUERY) {
6476                     qb.setTables("audio_artists");
6477                     qb.setProjectionMap(getProjectionMap(Audio.Artists.class));
6478                 } else {
6479                     throw new UnsupportedOperationException("Artists cannot be directly modified");
6480                 }
6481                 appendWhereStandaloneFilter(qb, new String[] {
6482                         AudioColumns.ARTIST_KEY
6483                 }, filter);
6484                 break;
6485             }
6486             case AUDIO_ALBUMS_ID:
6487                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
6488                 // fall-through
6489             case AUDIO_ALBUMS: {
6490                 if (type == TYPE_QUERY) {
6491                     qb.setTables("audio_albums");
6492                     qb.setProjectionMap(getProjectionMap(Audio.Albums.class));
6493                 } else {
6494                     throw new UnsupportedOperationException("Albums cannot be directly modified");
6495                 }
6496                 appendWhereStandaloneFilter(qb, new String[] {
6497                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY
6498                 }, filter);
6499                 break;
6500             }
6501             case VIDEO_MEDIA_ID:
6502                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
6503                 matchPending = MATCH_INCLUDE;
6504                 matchTrashed = MATCH_INCLUDE;
6505                 // fall-through
6506             case VIDEO_MEDIA: {
6507                 if (type == TYPE_QUERY) {
6508                     qb.setTables("video");
6509                     qb.setProjectionMap(
6510                             getProjectionMap(Video.Media.class));
6511                 } else {
6512                     qb.setTables("files");
6513                     qb.setProjectionMap(
6514                             getProjectionMap(Video.Media.class, Files.FileColumns.class));
6515                     appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
6516                             FileColumns.MEDIA_TYPE_VIDEO);
6517                 }
6518                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
6519                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
6520                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
6521                 if (honored != null) {
6522                     honored.accept(QUERY_ARG_MATCH_PENDING);
6523                     honored.accept(QUERY_ARG_MATCH_TRASHED);
6524                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
6525                 }
6526                 if (!includeAllVolumes) {
6527                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
6528                 }
6529                 break;
6530             }
6531             case VIDEO_THUMBNAILS_ID:
6532                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
6533                 // fall-through
6534             case VIDEO_THUMBNAILS: {
6535                 qb.setTables("videothumbnails");
6536                 qb.setProjectionMap(getProjectionMap(Video.Thumbnails.class));
6537                 break;
6538             }
6539             case FILES_ID:
6540                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2));
6541                 matchPending = MATCH_INCLUDE;
6542                 matchTrashed = MATCH_INCLUDE;
6543                 // fall-through
6544             case FILES: {
6545                 qb.setTables("files");
6546                 qb.setProjectionMap(getProjectionMap(Files.FileColumns.class));
6547 
6548                 appendWhereStandaloneFilter(qb, new String[] {
6549                         AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
6550                 }, filter);
6551                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
6552                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
6553                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
6554                 if (honored != null) {
6555                     honored.accept(QUERY_ARG_MATCH_PENDING);
6556                     honored.accept(QUERY_ARG_MATCH_TRASHED);
6557                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
6558                 }
6559                 if (!includeAllVolumes) {
6560                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
6561                 }
6562                 break;
6563             }
6564             case DOWNLOADS_ID:
6565                 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2));
6566                 matchPending = MATCH_INCLUDE;
6567                 matchTrashed = MATCH_INCLUDE;
6568                 // fall-through
6569             case DOWNLOADS: {
6570                 if (type == TYPE_QUERY) {
6571                     qb.setTables("downloads");
6572                     qb.setProjectionMap(
6573                             getProjectionMap(Downloads.class));
6574                 } else {
6575                     qb.setTables("files");
6576                     qb.setProjectionMap(
6577                             getProjectionMap(Downloads.class, Files.FileColumns.class));
6578                     appendWhereStandalone(qb, FileColumns.IS_DOWNLOAD + "=1");
6579                 }
6580 
6581                 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
6582                 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
6583                 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
6584                 if (honored != null) {
6585                     honored.accept(QUERY_ARG_MATCH_PENDING);
6586                     honored.accept(QUERY_ARG_MATCH_TRASHED);
6587                     honored.accept(QUERY_ARG_MATCH_FAVORITE);
6588                 }
6589                 if (!includeAllVolumes) {
6590                     appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
6591                 }
6592                 break;
6593             }
6594             default:
6595                 throw new UnsupportedOperationException(
6596                         "Unknown or unsupported URL: " + uri.toString());
6597         }
6598 
6599         // To ensure we're enforcing our security model, all operations must
6600         // have a projection map configured
6601         if (qb.getProjectionMap() == null) {
6602             throw new IllegalStateException("All queries must have a projection map");
6603         }
6604 
6605         // If caller is an older app, we're willing to let through a
6606         // allowlist of technically invalid columns
6607         if (getCallingPackageTargetSdkVersion() < Build.VERSION_CODES.Q) {
6608             qb.setProjectionAllowlist(sAllowlist);
6609         }
6610 
6611         // Starting U, if owner package name is used in query arguments,
6612         // we are restricting result set to only self-owned packages.
6613         if (shouldFilterOwnerPackageNameFlag()
6614                 && shouldFilterOwnerPackageNameInSelection(extras, type)) {
6615             Log.d(TAG, "Restricting result set to only packages owned by calling package: "
6616                     + mCallingIdentity.get().getSharedPackagesAsString());
6617             final String ownerPackageMatchClause = getWhereForOwnerPackageMatch(
6618                     mCallingIdentity.get());
6619             appendWhereStandalone(qb, ownerPackageMatchClause);
6620         }
6621 
6622         // Prevent a query from returning results if the selection clauses query on latitude and
6623         // longitude. Only return results if these columns are present in the sort clause to avoid
6624         // breaking any existing usage but return them in any arbitrary fashion instead of actually
6625         // sorting them.
6626         List<String> filterClauses = getClausesForFilteringGeolocationData(extras, type);
6627         if (indexMediaLatitudeLongitude() && !isCallingPackageSelf() && !filterClauses.isEmpty()) {
6628             if (filterClauses.contains(QUERY_ARG_SQL_SORT_ORDER)) {
6629                 String sortArgs = extras.getString(QUERY_ARG_SQL_SORT_ORDER);
6630                 if (sortArgs != null) {
6631                     if (sortArgs.contains(LATITUDE)) {
6632                         sortArgs = sortArgs.replace(LATITUDE, /* replacement */ "NULL");
6633                     }
6634                     if (sortArgs.contains(LONGITUDE)) {
6635                         sortArgs = sortArgs.replace(LONGITUDE, /* replacement */ "NULL");
6636                     }
6637                     extras.putString(QUERY_ARG_SQL_SORT_ORDER, sortArgs);
6638                 }
6639             } else {
6640                 final String geolocationClause = "FALSE";
6641                 appendWhereStandalone(qb, geolocationClause);
6642             }
6643         }
6644         return qb;
6645     }
6646 
6647     private List<String> getClausesForFilteringGeolocationData(
6648             Bundle queryArgs, int type) {
6649         if (type == TYPE_QUERY) {
6650             return getClausesForFilteringGeolocationData(queryArgs);
6651         }
6652         return List.of();
6653     }
6654 
6655     private List<String> getClausesForFilteringGeolocationData(Bundle queryArgs) {
6656         final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION, "")
6657                 .toLowerCase(Locale.ROOT);
6658         final String groupBy = queryArgs.getString(QUERY_ARG_SQL_GROUP_BY, "")
6659                 .toLowerCase(Locale.ROOT);
6660         final String sort = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER, "")
6661                 .toLowerCase(Locale.ROOT);
6662         final String having = queryArgs.getString(QUERY_ARG_SQL_HAVING, "")
6663                 .toLowerCase(Locale.ROOT);
6664 
6665         List<String> filteringClauses = new ArrayList<>();
6666         if (sort.contains(LATITUDE) || sort.contains(LONGITUDE)) {
6667             filteringClauses.add(QUERY_ARG_SQL_SORT_ORDER);
6668         }
6669         if (selection.contains(LATITUDE) || selection.contains(LONGITUDE)
6670                 || groupBy.contains(LATITUDE) || groupBy.contains(LONGITUDE)
6671                 || having.contains(LATITUDE) || having.contains(LONGITUDE)) {
6672             filteringClauses.add(QUERY_ARG_SQL_SELECTION);
6673             filteringClauses.add(QUERY_ARG_SQL_GROUP_BY);
6674             filteringClauses.add(QUERY_ARG_SQL_HAVING);
6675         }
6676         return filteringClauses;
6677     }
6678 
6679     private boolean shouldFilterOwnerPackageNameInSelection(Bundle queryArgs, int type) {
6680         return type == TYPE_QUERY && containsOwnerPackageName(queryArgs)
6681                 && isApplicableForOwnerPackageNameFiltering();
6682     }
6683 
6684     private boolean containsOwnerPackageName(Bundle queryArgs) {
6685         final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION, "")
6686                 .toLowerCase(Locale.ROOT);
6687         final String groupBy = queryArgs.getString(QUERY_ARG_SQL_GROUP_BY, "")
6688                 .toLowerCase(Locale.ROOT);
6689         final String sort = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER, "")
6690                 .toLowerCase(Locale.ROOT);
6691         final String having = queryArgs.getString(QUERY_ARG_SQL_HAVING, "")
6692                 .toLowerCase(Locale.ROOT);
6693 
6694         return selection.contains(OWNER_PACKAGE_NAME) || groupBy.contains(OWNER_PACKAGE_NAME)
6695                 || sort.contains(OWNER_PACKAGE_NAME) || having.contains(OWNER_PACKAGE_NAME);
6696     }
6697 
6698     private void appendAccessCheckQuery(@NonNull SQLiteQueryBuilder qb, boolean forWrite,
6699             @NonNull Uri uri, int uriType, @NonNull Bundle extras, @NonNull String volumeName) {
6700         Objects.requireNonNull(extras);
6701         final Uri redactedUri = extras.getParcelable(QUERY_ARG_REDACTED_URI);
6702 
6703         final boolean allowGlobal;
6704         if (redactedUri != null) {
6705             allowGlobal = checkCallingPermissionGlobal(redactedUri, false);
6706         } else {
6707             allowGlobal = checkCallingPermissionGlobal(uri, forWrite);
6708         }
6709 
6710         if (allowGlobal) {
6711             return;
6712         }
6713 
6714         if (hasAccessToCollection(mCallingIdentity.get(), uriType, forWrite)) {
6715             // has direct access to whole collection, no special filtering needed.
6716             return;
6717         }
6718 
6719         final ArrayList<String> options = new ArrayList<>();
6720         boolean isLatestSelectionOnlyRequired = extras.getBoolean(QUERY_ARG_LATEST_SELECTION_ONLY,
6721                 false);
6722         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)
6723                 && hasUserSelectedAccess(mCallingIdentity.get(), uriType, forWrite)) {
6724             // If app has READ_MEDIA_VISUAL_USER_SELECTED permission, allow access on files granted
6725             // via PhotoPicker launched for Permission. These grants are defined in media_grants
6726             // table.
6727             // We exclude volume internal from the query because media_grants are not supported.
6728             if (isLatestSelectionOnlyRequired) {
6729                 // If the query arg to include only recent selection has been received then include
6730                 // this as filter while doing the access check for grants from the media_grants
6731                 // table. This reduces the clauses needed in the query and makes it more efficient.
6732                 Log.d(TAG, "In user_select mode, recent selection only is required.");
6733                 options.add(getWhereForLatestSelection(mCallingIdentity.get(), uriType));
6734             } else {
6735                 Log.d(TAG, "In user_select mode, recent selection only is not required.");
6736                 options.add(getWhereForUserSelectedAccess(mCallingIdentity.get(), uriType));
6737                 // Allow access to files which are owned by the caller. Or allow access to files
6738                 // based on legacy or any other special access permissions.
6739                 options.add(getWhereForConstrainedAccess(mCallingIdentity.get(), uriType, forWrite,
6740                         extras));
6741             }
6742         } else {
6743             if (isLatestSelectionOnlyRequired) {
6744                 Log.w(TAG, "Latest selection request cannot be honored in the current"
6745                         + " access mode.");
6746             }
6747             // Allow access to files which are owned by the caller. Or allow access to files
6748             // based on legacy or any other special access permissions.
6749             options.add(getWhereForConstrainedAccess(mCallingIdentity.get(), uriType, forWrite,
6750                     extras));
6751         }
6752 
6753         appendWhereStandalone(qb, TextUtils.join(" OR ", options));
6754     }
6755 
6756     /**
6757      * @return {@code true} if app requests to include database rows from
6758      * recently unmounted volume.
6759      * {@code false} otherwise.
6760      */
6761     private boolean shouldIncludeRecentlyUnmountedVolumes(Uri uri, Bundle extras) {
6762         if (isFuseThread()) {
6763             // File path requests don't require to query from unmounted volumes.
6764             return false;
6765         }
6766 
6767         boolean isIncludeVolumesChangeEnabled = SdkLevel.isAtLeastS() &&
6768                 CompatChanges.isChangeEnabled(ENABLE_INCLUDE_ALL_VOLUMES, Binder.getCallingUid());
6769         if ("1".equals(uri.getQueryParameter(ALL_VOLUMES))) {
6770             // Support uri parameter only in R OS and below. Apps should use
6771             // MediaStore#QUERY_ARG_RECENTLY_UNMOUNTED_VOLUMES on S OS onwards.
6772             if (!isIncludeVolumesChangeEnabled) {
6773                 return true;
6774             }
6775             throw new IllegalArgumentException("Unsupported uri parameter \"all_volumes\"");
6776         }
6777         if (isIncludeVolumesChangeEnabled) {
6778             // MediaStore#QUERY_ARG_INCLUDE_RECENTLY_UNMOUNTED_VOLUMES is only supported on S OS and
6779             // for app targeting targetSdk>=S.
6780             return extras.getBoolean(MediaStore.QUERY_ARG_INCLUDE_RECENTLY_UNMOUNTED_VOLUMES,
6781                     false);
6782         }
6783         return false;
6784     }
6785 
6786     /**
6787      * Determine if given {@link Uri} has a
6788      * {@link MediaColumns#OWNER_PACKAGE_NAME} column.
6789      */
6790     private boolean hasOwnerPackageName(Uri uri) {
6791         // It's easier to maintain this as an inverted list
6792         final int table = matchUri(uri, true);
6793         switch (table) {
6794             case IMAGES_THUMBNAILS_ID:
6795             case IMAGES_THUMBNAILS:
6796             case VIDEO_THUMBNAILS_ID:
6797             case VIDEO_THUMBNAILS:
6798             case AUDIO_ALBUMART:
6799             case AUDIO_ALBUMART_ID:
6800             case AUDIO_ALBUMART_FILE_ID:
6801                 return false;
6802             default:
6803                 return true;
6804         }
6805     }
6806 
6807     /**
6808      * @deprecated all operations should be routed through the overload that
6809      *             accepts a {@link Bundle} of extras.
6810      */
6811     @Override
6812     @Deprecated
6813     public int delete(Uri uri, String selection, String[] selectionArgs) {
6814         return delete(uri,
6815                 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null));
6816     }
6817 
6818     @Override
6819     public int delete(@NonNull Uri uri, @Nullable Bundle extras) {
6820         Trace.beginSection(safeTraceSectionNameWithUri("delete", uri));
6821         try {
6822             return deleteInternal(uri, extras);
6823         } catch (FallbackException e) {
6824             return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion());
6825         } finally {
6826             Trace.endSection();
6827         }
6828     }
6829 
6830     private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras)
6831             throws FallbackException {
6832         final String volumeName = getVolumeName(uri);
6833         PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
6834 
6835         extras = (extras != null) ? extras : new Bundle();
6836         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
6837         extras.remove(QUERY_ARG_REDACTED_URI);
6838 
6839         if (isRedactedUri(uri)) {
6840             // we don't support deletion on redacted uris.
6841             return 0;
6842         }
6843 
6844         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
6845         extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
6846 
6847         uri = safeUncanonicalize(uri);
6848         final boolean allowHidden = isCallingPackageAllowedHidden();
6849         final int match = matchUri(uri, allowHidden);
6850 
6851         switch (match) {
6852             case AUDIO_MEDIA_ID:
6853             case AUDIO_PLAYLISTS_ID:
6854             case VIDEO_MEDIA_ID:
6855             case IMAGES_MEDIA_ID:
6856             case DOWNLOADS_ID:
6857             case FILES_ID: {
6858                 if (!isFuseThread() && getCachedCallingIdentityForFuse(Binder.getCallingUid()).
6859                         removeDeletedRowId(Long.parseLong(uri.getLastPathSegment()))) {
6860                     // Apps sometimes delete the file via filePath and then try to delete the db row
6861                     // using MediaProvider#delete. Since we would have already deleted the db row
6862                     // during the filePath operation, the latter will result in a security
6863                     // exception. Apps which don't expect an exception will break here. Since we
6864                     // have already deleted the db row, silently return zero as deleted count.
6865                     return 0;
6866                 }
6867             }
6868             break;
6869             default:
6870                 // For other match types, given uri will not correspond to a valid file.
6871                 break;
6872         }
6873 
6874         final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION);
6875         final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);
6876 
6877         int count = 0;
6878 
6879         // handle MEDIA_SCANNER before calling getDatabaseForUri()
6880         if (match == MEDIA_SCANNER) {
6881             if (mMediaScannerVolume == null) {
6882                 return 0;
6883             }
6884 
6885             final DatabaseHelper helper = getDatabaseForUri(
6886                     MediaStore.Files.getContentUri(mMediaScannerVolume));
6887 
6888             helper.mScanStopTime = SystemClock.elapsedRealtime();
6889 
6890             mMediaScannerVolume = null;
6891             return 1;
6892         }
6893 
6894         if (match == VOLUMES_ID) {
6895             detachVolume(uri);
6896             count = 1;
6897         }
6898 
6899         final DatabaseHelper helper = getDatabaseForUri(uri);
6900         switch (match) {
6901             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
6902                 extras.putString(QUERY_ARG_SQL_SELECTION,
6903                         BaseColumns._ID + "=" + uri.getPathSegments().get(5));
6904                 // fall-through
6905             case AUDIO_PLAYLISTS_ID_MEMBERS: {
6906                 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
6907                 final Uri playlistUri = ContentUris.withAppendedId(
6908                         MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);
6909 
6910                 // Playlist contents are always persisted directly into playlist
6911                 // files on disk to ensure that we can reliably migrate between
6912                 // devices and recover from database corruption
6913                 int numOfRemovedPlaylistMembers = removePlaylistMembers(playlistUri, extras);
6914                 if (numOfRemovedPlaylistMembers > 0) {
6915                     acceptWithExpansion(helper::notifyDelete, volumeName, playlistId,
6916                             FileColumns.MEDIA_TYPE_PLAYLIST, false);
6917                 }
6918                 return numOfRemovedPlaylistMembers;
6919             }
6920         }
6921 
6922         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null);
6923 
6924         {
6925             // Give callers interacting with a specific media item a chance to
6926             // escalate access if they don't already have it
6927             switch (match) {
6928                 case AUDIO_MEDIA_ID:
6929                 case VIDEO_MEDIA_ID:
6930                 case IMAGES_MEDIA_ID:
6931                     enforceCallingPermission(uri, extras, true);
6932             }
6933 
6934             final String[] projection = new String[] {
6935                     FileColumns.MEDIA_TYPE,
6936                     FileColumns.DATA,
6937                     FileColumns._ID,
6938                     FileColumns.IS_DOWNLOAD,
6939                     FileColumns.MIME_TYPE,
6940             };
6941             final boolean isFilesTable = qb.getTables().equals("files");
6942             final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>();
6943             final int[] countPerMediaType = new int[FileColumns.MEDIA_TYPE_COUNT];
6944             if (isFilesTable) {
6945                 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
6946 
6947                 // if calling package is not self and its target SDK version is greater than U,
6948                 // ignore the deleteparam and do not allow use by apps
6949                 if (!isCallingPackageSelf() && getCallingPackageTargetSdkVersion()
6950                         > Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
6951                     deleteparam = null;
6952                     Log.w(TAG, "Ignoring param:deletedata post U for external apps");
6953                 }
6954 
6955                 if (deleteparam == null || ! deleteparam.equals("false")) {
6956                     Cursor c = qb.query(helper, projection, userWhere, userWhereArgs,
6957                             null, null, null, null, null);
6958                     try {
6959                         while (c.moveToNext()) {
6960                             final int mediaType = c.getInt(0);
6961                             final String data = c.getString(1);
6962                             final long id = c.getLong(2);
6963                             final int isDownload = c.getInt(3);
6964                             final String mimeType = c.getString(4);
6965 
6966                             // TODO(b/188782594) Consider logging mime type access on delete too.
6967 
6968                             // Forget that caller is owner of this item
6969                             mCallingIdentity.get().setOwned(id, false);
6970 
6971                             deleteIfAllowed(uri, extras, data);
6972                             int res = qb.delete(helper, BaseColumns._ID + "=" + id, null);
6973                             count += res;
6974                             // Avoid ArrayIndexOutOfBounds if more mediaTypes are added,
6975                             // but mediaTypeSize is not updated
6976                             if (res > 0 && mediaType < countPerMediaType.length) {
6977                                 countPerMediaType[mediaType] += res;
6978                             }
6979 
6980                             if (isDownload == 1) {
6981                                 deletedDownloadIds.put(id, mimeType);
6982                             }
6983                         }
6984                     } finally {
6985                         FileUtils.closeQuietly(c);
6986                     }
6987                     // Do not allow deletion if the file/object is referenced as parent
6988                     // by some other entries. It could cause database corruption.
6989                     appendWhereStandalone(qb, ID_NOT_PARENT_CLAUSE);
6990                 }
6991             }
6992 
6993             switch (match) {
6994                 case AUDIO_GENRES_ID_MEMBERS:
6995                     throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
6996 
6997                 case IMAGES_THUMBNAILS_ID:
6998                 case IMAGES_THUMBNAILS:
6999                 case VIDEO_THUMBNAILS_ID:
7000                 case VIDEO_THUMBNAILS:
7001                     // Delete the referenced files first.
7002                     Cursor c = qb.query(helper, sDataOnlyColumn, userWhere, userWhereArgs, null,
7003                             null, null, null, null);
7004                     if (c != null) {
7005                         try {
7006                             while (c.moveToNext()) {
7007                                 deleteIfAllowed(uri, extras, c.getString(0));
7008                             }
7009                         } finally {
7010                             FileUtils.closeQuietly(c);
7011                         }
7012                     }
7013                     count += deleteRecursive(qb, helper, userWhere, userWhereArgs);
7014                     break;
7015 
7016                 default:
7017                     count += deleteRecursive(qb, helper, userWhere, userWhereArgs);
7018                     break;
7019             }
7020 
7021             if (deletedDownloadIds.size() > 0) {
7022                 notifyDownloadManagerOnDelete(helper, deletedDownloadIds);
7023             }
7024 
7025             // Check for other URI format grants for File API call only. Check right before
7026             // returning count = 0, to leave positive cases performance unaffected.
7027             if (count == 0 && isFuseThread()) {
7028                 count += deleteWithOtherUriGrants(uri, helper, projection, userWhere, userWhereArgs,
7029                         extras);
7030             }
7031 
7032             if (isFilesTable && !isCallingPackageSelf()) {
7033                 Metrics.logDeletion(volumeName, mCallingIdentity.get().uid,
7034                         getCallingPackageOrSelf(), count, countPerMediaType);
7035             }
7036         }
7037 
7038         return count;
7039     }
7040 
7041     private int deleteWithOtherUriGrants(@NonNull Uri uri, DatabaseHelper helper,
7042             String[] projection, String userWhere, String[] userWhereArgs,
7043             @Nullable Bundle extras) {
7044         try (Cursor c = queryForSingleItemAsMediaProvider(uri, projection, userWhere, userWhereArgs,
7045                     null)) {
7046             final int mediaType = c.getInt(0);
7047             final String data = c.getString(1);
7048             final long id = c.getLong(2);
7049             final int isDownload = c.getInt(3);
7050             final String mimeType = c.getString(4);
7051 
7052             final Uri uriGranted = getOtherUriGrantsForPath(data, mediaType, Long.toString(id),
7053                     /* forWrite */ true);
7054             if (uriGranted != null) {
7055                 // 1. delete file
7056                 deleteIfAllowed(uriGranted, extras, data);
7057                 // 2. delete file row from the db
7058                 final boolean allowHidden = isCallingPackageAllowedHidden();
7059                 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE,
7060                         matchUri(uriGranted, allowHidden), uriGranted, extras, null);
7061                 int count = qb.delete(helper, BaseColumns._ID + "=" + id, null);
7062 
7063                 if (isDownload == 1) {
7064                     final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>();
7065                     deletedDownloadIds.put(id, mimeType);
7066                     notifyDownloadManagerOnDelete(helper, deletedDownloadIds);
7067                 }
7068                 return count;
7069             }
7070         } catch (FileNotFoundException ignored) {
7071             // Do nothing. Returns 0 files deleted.
7072         }
7073         return 0;
7074     }
7075 
7076     private void notifyDownloadManagerOnDelete(DatabaseHelper helper,
7077             LongSparseArray<String> deletedDownloadIds) {
7078         // Do this on a background thread, since we don't want to make binder
7079         // calls as part of a FUSE call.
7080         helper.postBackground(() -> {
7081             DownloadManager dm = getContext().getSystemService(DownloadManager.class);
7082             if (dm != null) {
7083                 dm.onMediaStoreDownloadsDeleted(deletedDownloadIds);
7084             }
7085         });
7086     }
7087 
7088     /**
7089      * Executes identical delete repeatedly within a single transaction until
7090      * stability is reached. Combined with {@link #ID_NOT_PARENT_CLAUSE}, this
7091      * can be used to recursively delete all matching entries, since it only
7092      * deletes parents when no references remaining.
7093      */
7094     private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere,
7095             String[] userWhereArgs) {
7096         return helper.runWithTransaction((db) -> {
7097             synchronized (mDirectoryCache) {
7098                 mDirectoryCache.clear();
7099             }
7100 
7101             int n = 0;
7102             int total = 0;
7103             do {
7104                 n = qb.delete(helper, userWhere, userWhereArgs);
7105                 total += n;
7106             } while (n > 0);
7107             return total;
7108         });
7109     }
7110 
7111     @Nullable
7112     @VisibleForTesting
7113     Uri getRedactedUri(@NonNull Uri uri) {
7114         if (!isUriSupportedForRedaction(uri)) {
7115             return null;
7116         }
7117 
7118         DatabaseHelper helper;
7119         try {
7120             helper = getDatabaseForUri(uri);
7121         } catch (VolumeNotFoundException e) {
7122             throw e.rethrowAsIllegalArgumentException();
7123         }
7124 
7125         try (final Cursor c = helper.runWithoutTransaction(
7126                 (db) -> db.query("files",
7127                         new String[]{FileColumns.REDACTED_URI_ID}, FileColumns._ID + "=?",
7128                         new String[]{uri.getLastPathSegment()}, null, null, null))) {
7129             // Database entry for uri not found.
7130             if (!c.moveToFirst()) return null;
7131 
7132             String redactedUriID = c.getString(c.getColumnIndex(FileColumns.REDACTED_URI_ID));
7133             if (redactedUriID == null) {
7134                 // No redacted has even been created for this uri. Create a new redacted URI ID for
7135                 // the uri and store it in the DB.
7136                 redactedUriID = REDACTED_URI_ID_PREFIX + UUID.randomUUID().toString().replace("-",
7137                         "");
7138 
7139                 ContentValues cv = new ContentValues();
7140                 cv.put(FileColumns.REDACTED_URI_ID, redactedUriID);
7141                 int rowsAffected = helper.runWithTransaction(
7142                         (db) -> db.update("files", cv, FileColumns._ID + "=?",
7143                                 new String[]{uri.getLastPathSegment()}));
7144                 if (rowsAffected == 0) {
7145                     // this shouldn't happen ideally, only reason this might happen is if the db
7146                     // entry got deleted in b/w in which case we should return null.
7147                     return null;
7148                 }
7149             }
7150 
7151             // Create and return a uri with ID = redactedUriID.
7152             final Uri.Builder builder = ContentUris.removeId(uri).buildUpon();
7153             builder.appendPath(redactedUriID);
7154 
7155             return builder.build();
7156         }
7157     }
7158 
7159     @NonNull
7160     @VisibleForTesting
7161     List<Uri> getRedactedUri(@NonNull List<Uri> uris) {
7162         ArrayList<Uri> redactedUris = new ArrayList<>();
7163         for (Uri uri : uris) {
7164             redactedUris.add(getRedactedUri(uri));
7165         }
7166 
7167         return redactedUris;
7168     }
7169 
7170     @Override
7171     public Bundle call(String method, String arg, Bundle extras) {
7172         Trace.beginSection("MP.call [" + method + ']');
7173         try {
7174             return callInternal(method, arg, extras);
7175         } finally {
7176             Trace.endSection();
7177         }
7178     }
7179 
7180     private Bundle callInternal(String method, String arg, Bundle extras) {
7181         switch (method) {
7182             case MediaStore.RESOLVE_PLAYLIST_MEMBERS_CALL: {
7183                 return getResultForResolvePlaylistMembers(extras);
7184             }
7185             case MediaStore.SET_STABLE_URIS_FLAG: {
7186                 return getResultForSetStableUrisFlag(arg, extras);
7187             }
7188             case MediaStore.RUN_IDLE_MAINTENANCE_CALL: {
7189                 return getResultForRunIdleMaintenance();
7190             }
7191             case MediaStore.WAIT_FOR_IDLE_CALL: {
7192                 return getResultForWaitForIdle();
7193             }
7194             case MediaStore.SCAN_FILE_CALL: {
7195                 return getResultForScanFile(arg);
7196             }
7197             case MediaStore.SCAN_VOLUME_CALL: {
7198                 return getResultForScanVolume(arg);
7199             }
7200             case MediaStore.GET_VERSION_CALL: {
7201                 return getResultForGetVersion(extras);
7202             }
7203             case MediaStore.GET_GENERATION_CALL: {
7204                 return getResultForGetGeneration(extras);
7205             }
7206             case MediaStore.GET_DOCUMENT_URI_CALL: {
7207                 return getResultForGetDocumentUri(method, extras);
7208             }
7209             case MediaStore.GET_MEDIA_URI_CALL: {
7210                 return getResultForGetMediaUri(method, extras);
7211             }
7212             case MediaStore.GET_REDACTED_MEDIA_URI_CALL: {
7213                 return getResultForGetRedactedMediaUri(extras);
7214             }
7215             case MediaStore.GET_REDACTED_MEDIA_URI_LIST_CALL: {
7216                 return getResultForGetRedactedMediaUriList(extras);
7217             }
7218             case MediaStore.GRANT_MEDIA_READ_FOR_PACKAGE_CALL: {
7219                 return getResultForGrantMediaReadForPackage(extras);
7220             }
7221             case MediaStore.REVOKE_READ_GRANT_FOR_PACKAGE_CALL: {
7222                 return getResultForRevokeReadGrantForPackage(extras);
7223             }
7224             case MediaStore.CREATE_WRITE_REQUEST_CALL:
7225             case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
7226             case MediaStore.CREATE_TRASH_REQUEST_CALL:
7227             case MediaStore.CREATE_DELETE_REQUEST_CALL: {
7228                 return getResultForCreateOperationsRequest(method, extras);
7229             }
7230             case MediaStore.MARK_MEDIA_AS_FAVORITE: {
7231                 return markMediaAsFavorite(extras);
7232             }
7233             case MediaStore.CREATE_CANCELLATION_SIGNAL_CALL: {
7234                 return getResultForCreateCancellationSignal();
7235             }
7236             case MediaStore.OPEN_FILE_CALL: {
7237                 return getResultForOpenFile(extras);
7238             }
7239             case MediaStore.OPEN_ASSET_FILE_CALL: {
7240                 return getResultForOpenAssetFile(extras);
7241             }
7242             case MediaStore.IS_SYSTEM_GALLERY_CALL:
7243                 return getResultForIsSystemGallery(arg, extras);
7244             case MediaStore.PICKER_MEDIA_INIT_CALL: {
7245                 return getResultForPickerMediaInit(extras);
7246             }
7247             case MediaStore.PICKER_MEDIA_IN_MEDIA_SET_INIT_CALL: {
7248                 initMediaInMediaSet(extras);
7249                 return new Bundle();
7250             }
7251             case MediaStore.PICKER_INTERNAL_SEARCH_MEDIA_INIT_CALL: {
7252                 return getResultForPickerSearchMediaInit(extras);
7253             }
7254             case MediaStore.PICKER_MEDIA_SETS_INIT_CALL: {
7255                 initMediaSets(extras);
7256                 return new Bundle();
7257             }
7258             case MediaStore.PICKER_GET_SEARCH_PROVIDERS_CALL: {
7259                 return getPickerSearchProviders();
7260             }
7261             case MediaStore.PICKER_TRANSCODE_CALL: {
7262                 return getResultForPickerTranscode(extras);
7263             }
7264             case MediaStore.GET_CLOUD_PROVIDER_CALL: {
7265                 return getResultForGetCloudProvider();
7266             }
7267             case MediaStore.GET_CLOUD_PROVIDER_LABEL_CALL: {
7268                 return getResultForGetCloudProviderLabel();
7269             }
7270             case MediaStore.GET_CLOUD_PROVIDER_DETAILS: {
7271                 if (isCallerPhotoPicker()) {
7272                     return PickerDataLayerV2.getCloudProviderDetails(extras);
7273                 } else  {
7274                     throw new SecurityException(
7275                             getSecurityExceptionMessage("GET_CLOUD_PROVIDER_DETAILS"));
7276                 }
7277             }
7278             case MediaStore.ENSURE_PROVIDERS_CALL: {
7279                 if (isCallerPhotoPicker()) {
7280                     PickerDataLayerV2.ensureProviders();
7281                     return new Bundle();
7282                 } else  {
7283                     throw new SecurityException(
7284                             getSecurityExceptionMessage("ENSURE_PROVIDERS_CALL"));
7285                 }
7286             }
7287             case MediaStore.SET_CLOUD_PROVIDER_CALL: {
7288                 return getResultForSetCloudProvider(extras);
7289             }
7290             case MediaStore.SYNC_PROVIDERS_CALL: {
7291                 return getResultForSyncProviders();
7292             }
7293             case MediaStore.IS_SUPPORTED_CLOUD_PROVIDER_CALL: {
7294                 return getResultForIsSupportedCloudProvider(arg);
7295             }
7296             case MediaStore.IS_CURRENT_CLOUD_PROVIDER_CALL: {
7297                 return getResultForIsCurrentCloudProviderCall(arg);
7298             }
7299             case MediaStore.NOTIFY_CLOUD_MEDIA_CHANGED_EVENT_CALL: {
7300                 return getResultForNotifyCloudMediaChangedEvent(arg, extras);
7301             }
7302             case MediaStore.USES_FUSE_PASSTHROUGH: {
7303                 return getResultForUsesFusePassThrough(arg);
7304             }
7305             case MediaStore.RUN_IDLE_MAINTENANCE_FOR_STABLE_URIS: {
7306                 return getResultForIdleMaintenanceForStableUris();
7307             }
7308             case READ_BACKUP: {
7309                 return getResultForReadBackup(arg, extras);
7310             }
7311             case GET_OWNER_PACKAGE_NAME: {
7312                 return getResultForGetOwnerPackageName(arg);
7313             }
7314             case MediaStore.DELETE_BACKED_UP_FILE_PATHS: {
7315                 return getResultForDeleteBackedUpFilePaths(arg);
7316             }
7317             case MediaStore.GET_BACKUP_FILES: {
7318                 return getResultForGetBackupFiles();
7319             }
7320             case MediaStore.GET_RECOVERY_DATA: {
7321                 return getResultForGetRecoveryData();
7322             }
7323             case MediaStore.REMOVE_RECOVERY_DATA: {
7324                 removeRecoveryData();
7325                 return new Bundle();
7326             }
7327             case MediaStore.BULK_UPDATE_OEM_METADATA_CALL: {
7328                 callForBulkUpdateOemMetadataColumn();
7329                 return new Bundle();
7330             }
7331             default:
7332                 throw new UnsupportedOperationException("Unsupported call: " + method);
7333         }
7334     }
7335 
7336     private void callForBulkUpdateOemMetadataColumn() {
7337         if (!Flags.enableOemMetadataUpdate()) {
7338             return;
7339         }
7340 
7341         enforcePermissionCheckForOemMetadataUpdate();
7342         Set<String> oemSupportedMimeTypes = mMediaScanner.getOemSupportedMimeTypes();
7343         if (oemSupportedMimeTypes == null || oemSupportedMimeTypes.isEmpty()) {
7344             // Nothing to update
7345             return;
7346         }
7347 
7348         // Get media types to update rows based on media type
7349         Set<Integer> mediaTypesToBeUpdated = new HashSet<>();
7350         for (String mimeType : oemSupportedMimeTypes) {
7351             // Convert to media type to avoid using like clause on mime types to protect against
7352             // SQL injection
7353             mediaTypesToBeUpdated.add(MimeUtils.resolveMediaType(mimeType));
7354         }
7355 
7356         if (mediaTypesToBeUpdated.isEmpty()) {
7357             // For invalid mime types, do not bother
7358             return;
7359         }
7360 
7361         final LocalCallingIdentity token = clearLocalCallingIdentity();
7362         try {
7363             ContentValues values = new ContentValues();
7364             values.putNull(OEM_METADATA);
7365             // Mark _modifier as _MODIFIER_CR to allow metadata update on next scan. This
7366             // is explicitly required when calling update with MediaProvider identity
7367             values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR);
7368             Bundle extras = new Bundle();
7369             extras.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
7370                     appendMediaTypeClause(mediaTypesToBeUpdated));
7371             Log.v(TAG, "Trigger bulk update of OEM metadata");
7372             update(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), values, extras);
7373         } finally {
7374             restoreLocalCallingIdentity(token);
7375         }
7376     }
7377 
7378     private String appendMediaTypeClause(Set<Integer> mediaTypesToBeUpdated) {
7379         List<String> whereMediaTypesCondition = new ArrayList<String>();
7380         for (Integer mediaType : mediaTypesToBeUpdated) {
7381             whereMediaTypesCondition.add(
7382                     String.format(Locale.ROOT, "%s=%d", MEDIA_TYPE, mediaType));
7383         }
7384 
7385         StringBuilder sb = new StringBuilder();
7386         sb.append("(");
7387         sb.append(TextUtils.join(" OR ", whereMediaTypesCondition));
7388         sb.append(")");
7389         return sb.toString();
7390     }
7391 
7392     @VisibleForTesting
7393     protected void enforcePermissionCheckForOemMetadataUpdate() {
7394         if (!isCallingPackageSelf()
7395                 && !mCallingIdentity.get().checkCallingPermissionToUpdateOemMetadata()) {
7396             throw new SecurityException(
7397                     "Calling package does not have permission to update OEM metadata");
7398         }
7399     }
7400 
7401     @Nullable
7402     private Bundle getResultForRevokeReadGrantForPackage(Bundle extras) {
7403         final int caller = Binder.getCallingUid();
7404         final Boolean isCallForRevokeAll = extras.getBoolean(
7405                 REVOKED_ALL_READ_GRANTS_FOR_PACKAGE_CALL);
7406         int userId;
7407         List<Uri> uris = null;
7408         String[] packageNames;
7409         int packageUid;
7410         if (checkPermissionShell(caller)) {
7411             // If the caller is the shell, the accepted parameter is EXTRA_PACKAGE_NAME
7412             // (as string).
7413             if (!extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) {
7414                 throw new IllegalArgumentException(
7415                         "Missing required extras arguments: EXTRA_URI or"
7416                                 + " EXTRA_PACKAGE_NAME");
7417             }
7418             String packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME);
7419             packageNames = new String[]{packageName};
7420             try {
7421                 packageUid = mPackageManager.getPackageUid(packageName, 0);
7422             } catch (NameNotFoundException e) {
7423                 Log.e(TAG, "No packageUid found for packageName " + packageName, e);
7424                 throw new RuntimeException(e);
7425             }
7426             // Uris are not a requirement for revoke all call
7427             if (!isCallForRevokeAll) {
7428                 uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI)));
7429             }
7430             // Caller is always shell which may not have the desired userId. Hence, use
7431             // UserId from the MediaProvider process itself.
7432             userId = UserHandle.myUserId();
7433         } else if (checkPermissionSelf(caller) || isCallerPhotoPicker()) {
7434             final PackageManager pm = getContext().getPackageManager();
7435             packageUid = extras.getInt(Intent.EXTRA_UID);
7436             packageNames = pm.getPackagesForUid(packageUid);
7437             // Get the userId from packageUid as the initiator could be a cloned app, which
7438             // accesses Media via MP of its parent user and Binder's callingUid reflects
7439             // the latter.
7440             userId = uidToUserId(packageUid);
7441             // Uris are not a requirement for revoke all call
7442             if (!isCallForRevokeAll) {
7443                 uris = extras.getParcelableArrayList(EXTRA_URI_LIST);
7444             }
7445         } else {
7446             // All other callers are unauthorized.
7447             throw new SecurityException(
7448                     getSecurityExceptionMessage("revoke media grants"));
7449         }
7450 
7451         if (isCallForRevokeAll && !isOwnedPhotosEnabled(packageUid)) {
7452             mMediaGrants.removeAllMediaGrantsForPackages(packageNames, "user de-selections",
7453                     userId);
7454         } else if (uris != null) {
7455             mMediaGrants.removeMediaGrantsForPackage(packageNames, uris, userId);
7456             if (isOwnedPhotosEnabled(packageUid)) {
7457                 mFilesOwnershipUtils.removeOwnerPackageNameForUris(packageNames, uris,
7458                         userId);
7459             }
7460         }
7461         return null;
7462     }
7463 
7464     @Nullable
7465     private Bundle getResultForResolvePlaylistMembers(Bundle extras) {
7466         final LocalCallingIdentity token = clearLocalCallingIdentity();
7467         final CallingIdentity providerToken = clearCallingIdentity();
7468         try {
7469             final Uri playlistUri = extras.getParcelable(MediaStore.EXTRA_URI);
7470             resolvePlaylistMembers(playlistUri);
7471         } finally {
7472             restoreCallingIdentity(providerToken);
7473             restoreLocalCallingIdentity(token);
7474         }
7475         return null;
7476     }
7477 
7478     @Nullable
7479     private Bundle getResultForSetStableUrisFlag(String volumeName, Bundle extras) {
7480         // WRITE_MEDIA_STORAGE is a privileged permission which only MediaProvider and some other
7481         // system apps have.
7482         getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
7483                 "Permission missing to call SET_STABLE_URIS by uid:" + Binder.getCallingUid());
7484         final LocalCallingIdentity token = clearLocalCallingIdentity();
7485         final CallingIdentity providerToken = clearCallingIdentity();
7486         try {
7487             final boolean isEnabled = extras.getBoolean(EXTRA_IS_STABLE_URIS_ENABLED);
7488             mDatabaseBackupAndRecovery.setStableUrisGlobalFlag(volumeName, isEnabled);
7489         } finally {
7490             restoreCallingIdentity(providerToken);
7491             restoreLocalCallingIdentity(token);
7492         }
7493         return null;
7494     }
7495 
7496     @Nullable
7497     private Bundle getResultForRunIdleMaintenance() {
7498         // Protect ourselves from random apps by requiring a generic
7499         // permission held by common debugging components, such as shell
7500         getContext().enforceCallingOrSelfPermission(
7501                 Manifest.permission.DUMP, TAG);
7502         final LocalCallingIdentity token = clearLocalCallingIdentity();
7503         final CallingIdentity providerToken = clearCallingIdentity();
7504         try {
7505             onIdleMaintenance(new CancellationSignal());
7506         } finally {
7507             restoreCallingIdentity(providerToken);
7508             restoreLocalCallingIdentity(token);
7509         }
7510         return null;
7511     }
7512 
7513     /**
7514      * Triggers backup for MediaProvider.
7515      */
7516     public void triggerBackup() {
7517         mExternalPrimaryBackupExecutor.doBackup(null);
7518     }
7519 
7520     @Nullable
7521     private Bundle getResultForWaitForIdle() {
7522         // TODO(b/195009139): Remove after overriding wait for idle in test to sync picker
7523         // Syncing the picker while waiting for idle fixes tests with the picker db
7524         // flag enabled because the picker db is in a consistent state with the external
7525         // db after the sync
7526         syncAllMedia();
7527         ForegroundThread.waitForIdle();
7528         final CountDownLatch latch = new CountDownLatch(1);
7529         BackgroundThread.getExecutor().execute(latch::countDown);
7530         try {
7531             latch.await(30, TimeUnit.SECONDS);
7532         } catch (InterruptedException e) {
7533             throw new IllegalStateException(e);
7534         }
7535         return null;
7536     }
7537 
7538     @NotNull
7539     private Bundle getResultForScanFile(String arg) {
7540         final LocalCallingIdentity token = clearLocalCallingIdentity();
7541         final CallingIdentity providerToken = clearCallingIdentity();
7542 
7543         final String filePath = arg;
7544         final Uri uri;
7545         try {
7546             File file;
7547             try {
7548                 file = FileUtils.getCanonicalFile(filePath);
7549             } catch (IOException e) {
7550                 file = null;
7551             }
7552 
7553             uri = file != null ? scanFile(file, REASON_DEMAND) : null;
7554         } finally {
7555             restoreCallingIdentity(providerToken);
7556             restoreLocalCallingIdentity(token);
7557         }
7558 
7559         // TODO(b/262244882): maybe enforceCallingPermissionInternal(uri, ...)
7560 
7561         final Bundle res = new Bundle();
7562         res.putParcelable(Intent.EXTRA_STREAM, uri);
7563         return res;
7564     }
7565 
7566     private Bundle getResultForScanVolume(String arg) {
7567         final int userId = uidToUserId(Binder.getCallingUid());
7568         final LocalCallingIdentity token = clearLocalCallingIdentity();
7569         final CallingIdentity providerToken = clearCallingIdentity();
7570 
7571         final String volumeName = arg;
7572         try {
7573             final MediaVolume volume = mVolumeCache.findVolume(volumeName,
7574                     UserHandle.of(userId));
7575             MediaService.onScanVolume(getContext(), volume, REASON_DEMAND);
7576         } catch (FileNotFoundException e) {
7577             Log.w(TAG, "Failed to find volume " + volumeName, e);
7578         } catch (IOException e) {
7579             throw new RuntimeException(e);
7580         } finally {
7581             restoreCallingIdentity(providerToken);
7582             restoreLocalCallingIdentity(token);
7583         }
7584         return Bundle.EMPTY;
7585     }
7586 
7587     @NotNull
7588     private Bundle getResultForGetVersion(Bundle extras) {
7589         final String volumeName = extras.getString(Intent.EXTRA_TEXT);
7590 
7591         final DatabaseHelper helper;
7592         try {
7593             helper = getDatabaseForUri(Files.getContentUri(volumeName));
7594         } catch (VolumeNotFoundException e) {
7595             throw e.rethrowAsIllegalArgumentException();
7596         }
7597 
7598         final String version =
7599                 helper.runWithoutTransaction(
7600                     (db) -> {
7601                         final String dbUuid = DatabaseHelper.getOrCreateUuid(db);
7602                         if (shouldLockdownMediaStoreVersion()) {
7603                             final String input = dbUuid + mCallingIdentity.get().uid;
7604                             final HashCode uuidHashCode =
7605                                     Hashing.farmHashFingerprint64()
7606                                        .hashString(input, StandardCharsets.UTF_8);
7607                             return uuidHashCode.toString();
7608                         } else {
7609                             return db.getVersion() + ":" + dbUuid;
7610                         }
7611                     });
7612         final Bundle res = new Bundle();
7613         res.putString(Intent.EXTRA_TEXT, version);
7614         return res;
7615     }
7616 
7617     @VisibleForTesting
7618     boolean shouldLockdownMediaStoreVersion() {
7619         return versionLockdown() && CompatChanges.isChangeEnabled(
7620                 LOCKDOWN_MEDIASTORE_VERSION, mCallingIdentity.get().uid);
7621     }
7622 
7623     @NotNull
7624     private Bundle getResultForGetGeneration(Bundle extras) {
7625         final String volumeName = extras.getString(Intent.EXTRA_TEXT);
7626 
7627         final DatabaseHelper helper;
7628         try {
7629             helper = getDatabaseForUri(Files.getContentUri(volumeName));
7630         } catch (VolumeNotFoundException e) {
7631             throw e.rethrowAsIllegalArgumentException();
7632         }
7633 
7634         final long generation = helper.runWithoutTransaction(DatabaseHelper::getGeneration);
7635 
7636         final Bundle res = new Bundle();
7637         res.putLong(Intent.EXTRA_INDEX, generation);
7638         return res;
7639     }
7640 
7641     private Bundle getResultForGetDocumentUri(String method, Bundle extras) {
7642         final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI);
7643         enforceCallingPermission(mediaUri, extras, false);
7644 
7645         final Uri fileUri;
7646         final LocalCallingIdentity token = clearLocalCallingIdentity();
7647         try {
7648             fileUri = Uri.fromFile(queryForDataFile(mediaUri, null));
7649         } catch (FileNotFoundException e) {
7650             throw new IllegalArgumentException(e);
7651         } finally {
7652             restoreLocalCallingIdentity(token);
7653         }
7654 
7655         try (ContentProviderClient client = getContext().getContentResolver()
7656                 .acquireUnstableContentProviderClient(
7657                         getExternalStorageProviderAuthority())) {
7658             extras.putParcelable(MediaStore.EXTRA_URI, fileUri);
7659             return client.call(method, null, extras);
7660         } catch (RemoteException e) {
7661             throw new IllegalStateException(e);
7662         }
7663     }
7664 
7665     @NotNull
7666     private Bundle getResultForGetMediaUri(String method, Bundle extras) {
7667         final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI);
7668         getContext().enforceCallingUriPermission(documentUri,
7669                 Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG);
7670 
7671         final int callingPid = mCallingIdentity.get().pid;
7672         final int callingUid = mCallingIdentity.get().uid;
7673         final String callingPackage = getCallingPackage();
7674         final CallingIdentity token = clearCallingIdentity();
7675         final String authority = documentUri.getAuthority();
7676 
7677         if (!authority.equals(MediaDocumentsProvider.AUTHORITY)
7678                 && !authority.equals(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
7679             throw new IllegalArgumentException("Provider for this Uri is not supported.");
7680         }
7681 
7682         try (ContentProviderClient client = getContext().getContentResolver()
7683                 .acquireUnstableContentProviderClient(authority)) {
7684             final Bundle clientRes = client.call(method, null, extras);
7685             final Uri fileUri = clientRes.getParcelable(MediaStore.EXTRA_URI);
7686             final Bundle res = new Bundle();
7687             final Uri mediaStoreUri = fileUri.getAuthority().equals(MediaStore.AUTHORITY)
7688                     ? fileUri : queryForMediaUri(new File(fileUri.getPath()), null);
7689             copyUriPermissionGrants(documentUri, mediaStoreUri, callingPid,
7690                     callingUid, callingPackage);
7691             res.putParcelable(MediaStore.EXTRA_URI, mediaStoreUri);
7692             return res;
7693         } catch (FileNotFoundException e) {
7694             throw new IllegalArgumentException(e);
7695         } catch (RemoteException e) {
7696             throw new IllegalStateException(e);
7697         } finally {
7698             restoreCallingIdentity(token);
7699         }
7700     }
7701 
7702     @NotNull
7703     private Bundle getResultForGetRedactedMediaUri(Bundle extras) {
7704         final Uri uri = extras.getParcelable(MediaStore.EXTRA_URI);
7705         // NOTE: It is ok to update the DB and return a redacted URI for the cases when
7706         // the user code only has read access, hence we don't check for write permission.
7707         enforceCallingPermission(uri, Bundle.EMPTY, false);
7708         final LocalCallingIdentity token = clearLocalCallingIdentity();
7709         try {
7710             final Bundle res = new Bundle();
7711             res.putParcelable(MediaStore.EXTRA_URI, getRedactedUri(uri));
7712             return res;
7713         } finally {
7714             restoreLocalCallingIdentity(token);
7715         }
7716     }
7717 
7718     @NotNull
7719     private Bundle getResultForGetRedactedMediaUriList(Bundle extras) {
7720         final List<Uri> uris = extras.getParcelableArrayList(EXTRA_URI_LIST);
7721         // NOTE: It is ok to update the DB and return a redacted URI for the cases when
7722         // the user code only has read access, hence we don't check for write permission.
7723         enforceCallingPermission(uris, false);
7724         final LocalCallingIdentity token = clearLocalCallingIdentity();
7725         try {
7726             final Bundle res = new Bundle();
7727             res.putParcelableArrayList(EXTRA_URI_LIST,
7728                     (ArrayList<? extends Parcelable>) getRedactedUri(uris));
7729             return res;
7730         } finally {
7731             restoreLocalCallingIdentity(token);
7732         }
7733     }
7734 
7735     @Nullable
7736     private Bundle getResultForGrantMediaReadForPackage(Bundle extras) {
7737         final int caller = Binder.getCallingUid();
7738         int userId;
7739         final List<Uri> uris;
7740         String packageName;
7741         if (checkPermissionShell(caller)) {
7742             // If the caller is the shell, the accepted parameters are EXTRA_URI (as string)
7743             // and EXTRA_PACKAGE_NAME (as string).
7744             if (!extras.containsKey(MediaStore.EXTRA_URI)
7745                     && !extras.containsKey(Intent.EXTRA_PACKAGE_NAME)) {
7746                 throw new IllegalArgumentException(
7747                         "Missing required extras arguments: EXTRA_URI or" + " EXTRA_PACKAGE_NAME");
7748             }
7749             packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME);
7750             uris = List.of(Uri.parse(extras.getString(MediaStore.EXTRA_URI)));
7751             // Caller is always shell which may not have the desired userId. Hence, use
7752             // UserId from the MediaProvider process itself.
7753             userId = UserHandle.myUserId();
7754 
7755         } else if (checkPermissionSelf(caller) || isCallerPhotoPicker()) {
7756             // If the caller is MediaProvider the accepted parameters are EXTRA_URI_LIST
7757             // and EXTRA_UID.
7758             if (!extras.containsKey(EXTRA_URI_LIST)
7759                     && !extras.containsKey(Intent.EXTRA_UID)) {
7760                 throw new IllegalArgumentException(
7761                         "Missing required extras arguments: EXTRA_URI_LIST or" + " EXTRA_UID");
7762             }
7763             uris = extras.getParcelableArrayList(EXTRA_URI_LIST);
7764             final PackageManager pm = getContext().getPackageManager();
7765             final int packageUid = extras.getInt(Intent.EXTRA_UID);
7766             final String[] packages = pm.getPackagesForUid(packageUid);
7767             if (packages == null || packages.length == 0) {
7768                 throw new IllegalArgumentException(
7769                         String.format(
7770                                 "Could not find packages for media_grants with uid: %d",
7771                                 packageUid));
7772             }
7773             // Use the first package in the returned list for grants. In the case this
7774             // uid has multiple shared packages, the eventual queries to check for file
7775             // access will use all of the packages in this list, so just one is needed
7776             // to create the grants.
7777             packageName = packages[0];
7778             // Get the userId from packageUid as the initiator could be a cloned app, which
7779             // accesses Media via MP of its parent user and Binder's callingUid reflects
7780             // the latter.
7781             userId = uidToUserId(packageUid);
7782         } else {
7783             // All other callers are unauthorized.
7784 
7785             throw new SecurityException(getSecurityExceptionMessage("Create media grants"));
7786         }
7787 
7788         mMediaGrants.addMediaGrantsForPackage(packageName, uris, userId);
7789         return null;
7790     }
7791 
7792     @NotNull
7793     private Bundle getResultForCreateOperationsRequest(String method, Bundle extras) {
7794         final PendingIntent pi = createRequest(method, extras);
7795         final Bundle res = new Bundle();
7796         res.putParcelable(MediaStore.EXTRA_RESULT, pi);
7797         return res;
7798     }
7799 
7800     private Bundle markMediaAsFavorite(Bundle extras) {
7801         final ContentValues values = extras.getParcelable(MediaStore.EXTRA_CONTENT_VALUES);
7802         final ClipData clipData = extras.getParcelable(MediaStore.EXTRA_CLIP_DATA);
7803         final List<Uri> uris = collectUris(clipData);
7804 
7805         if (!isCallingPackageManager()) {
7806             for (Uri uri : uris) {
7807                 if (!AccessChecker.hasAccessToCollection(mCallingIdentity.get(),
7808                         matchUri(uri, isCallingPackageAllowedHidden()), /* forWrite= */false)) {
7809                     throw new UnsupportedOperationException("Uri " + uri
7810                             + " does not have required permission to mark media as favorite");
7811                 }
7812             }
7813         }
7814 
7815         final LocalCallingIdentity token = clearLocalCallingIdentity();
7816         try {
7817             for (Uri uri : uris) {
7818                 update(uri, values, null);
7819             }
7820         } finally {
7821             restoreLocalCallingIdentity(token);
7822         }
7823         return null;
7824     }
7825 
7826     @NotNull
7827     private Bundle getResultForCreateCancellationSignal() {
7828         final Bundle res = new Bundle();
7829         res.putBinder(MediaStore.CREATE_CANCELLATION_SIGNAL_RESULT,
7830                 (new MPCancellationSignal()).asBinder());
7831         return res;
7832     }
7833 
7834     @NotNull
7835     private Bundle getResultForOpenFile(Bundle extras) {
7836         OpenFileRequest request = extras.getParcelable(EXTRA_OPEN_FILE_REQUEST);
7837         if (!isPickerUri(request.getUri())) {
7838             throw new IllegalArgumentException("Given Uri " + request.getUri()
7839                     + " should be a picker URI");
7840         }
7841         mAsyncPickerFileOpener.scheduleOpenFileAsync(request, mCallingIdentity.get());
7842         return new Bundle();
7843     }
7844 
7845     @NotNull
7846     private Bundle getResultForOpenAssetFile(Bundle extras) {
7847         OpenAssetFileRequest request = extras.getParcelable(EXTRA_OPEN_ASSET_FILE_REQUEST);
7848         if (!isPickerUri(request.getUri())) {
7849             throw new IllegalArgumentException("Given Uri " + request.getUri()
7850                     + " should be a picker URI");
7851         }
7852         mAsyncPickerFileOpener.scheduleOpenAssetFileAsync(request, mCallingIdentity.get());
7853         return new Bundle();
7854     }
7855 
7856     @NotNull
7857     private Bundle getResultForIsSystemGallery(String arg, Bundle extras) {
7858         final LocalCallingIdentity token = clearLocalCallingIdentity();
7859         try {
7860             String packageName = arg;
7861             int uid = extras.getInt(MediaStore.EXTRA_IS_SYSTEM_GALLERY_UID);
7862             boolean isSystemGallery = PermissionUtils.checkWriteImagesOrVideoAppOps(
7863                     getContext(), uid, packageName, getContext().getAttributionTag(),
7864                     /*forDataDelivery*/ false);
7865             Bundle res = new Bundle();
7866             res.putBoolean(MediaStore.EXTRA_IS_SYSTEM_GALLERY_RESPONSE, isSystemGallery);
7867             return res;
7868         } finally {
7869             restoreLocalCallingIdentity(token);
7870         }
7871     }
7872 
7873     @Nullable
7874     private Bundle getResultForPickerMediaInit(Bundle extras) {
7875         Log.i(TAG, "Received media init query for extras: " + extras);
7876         if (!checkPermissionShell(Binder.getCallingUid())
7877                 && !checkPermissionSelf(Binder.getCallingUid())
7878                 && !isCallerPhotoPicker()) {
7879             throw new SecurityException(
7880                     getSecurityExceptionMessage("Picker media init"));
7881         }
7882         mPickerDataLayer.initMediaData(PickerSyncRequestExtras.fromBundle(extras));
7883         return null;
7884     }
7885 
7886     private void initMediaInMediaSet(@NonNull Bundle extras) {
7887         Objects.requireNonNull(extras);
7888         Log.i(TAG, "Extras received for media in media set init: " + extras);
7889         if (!checkPermissionSelf(Binder.getCallingUid()) && !isCallerPhotoPicker()) {
7890             throw new SecurityException(
7891                     getSecurityExceptionMessage("Picker media in media set init"));
7892         }
7893         PickerDataLayerV2.triggerMediaSyncForMediaSet(extras, getContext());
7894     }
7895 
7896     @Nullable
7897     private Bundle getPickerSearchProviders() {
7898         Log.i(TAG, "Received picker internal call to get available search providers.");
7899         if (!checkPermissionShell(Binder.getCallingUid())
7900                 && !checkPermissionSelf(Binder.getCallingUid())
7901                 && !isCallerPhotoPicker()) {
7902             throw new SecurityException(
7903                     getSecurityExceptionMessage("Picker get search providers"));
7904         }
7905         return PickerDataLayerV2.getSearchProviders(getContext());
7906     }
7907 
7908     /**
7909      * Checks if the caller has the permission to handle picker search media init. If not,
7910      * this method throws a security exception.
7911      */
7912     @NonNull
7913     private Bundle getResultForPickerSearchMediaInit(@NonNull Bundle extras) {
7914         Log.i(TAG, "Received search media init query for extras: " + extras);
7915         if (!checkPermissionSelf(Binder.getCallingUid())
7916                 && !isCallerPhotoPicker()) {
7917             throw new SecurityException(
7918                     getSecurityExceptionMessage("Picker search media init"));
7919         }
7920         return PickerDataLayerV2.handleSearchResultsInit(getContext(), extras);
7921     }
7922 
7923     private void initMediaSets(@NonNull Bundle extras) {
7924         Objects.requireNonNull(extras);
7925         Log.i(TAG, "Extras received for media sets init: " + extras);
7926         if (!checkPermissionSelf(Binder.getCallingUid()) && !isCallerPhotoPicker()) {
7927             throw new SecurityException(
7928                     getSecurityExceptionMessage("Picker media sets init"));
7929         }
7930         PickerDataLayerV2.triggerMediaSetsSync(extras, getContext());
7931     }
7932 
7933     @NotNull
7934     private Bundle getResultForPickerTranscode(@NonNull Bundle extras) {
7935         Log.i(TAG, "Received media transcode request for extras: " + extras);
7936 
7937         // Check the caller.
7938         if (!checkPermissionShell(Binder.getCallingUid())
7939                 && !checkPermissionSelf(Binder.getCallingUid())
7940                 && !isCallerPhotoPicker()) {
7941             throw new SecurityException(getSecurityExceptionMessage("Picker media transcode"));
7942         }
7943 
7944         // Transcode the media.
7945         final Uri uri = Objects.requireNonNull(extras).getParcelable(MediaStore.EXTRA_URI);
7946         if (uri == null) {
7947             throw new IllegalArgumentException("Extras does not contains a URI for transcoding.");
7948         }
7949         boolean transcodeResult = mPhotoPickerTranscodeHelper.transcode(getContext(), uri);
7950 
7951         // Return the result.
7952         final Bundle bundle = new Bundle();
7953         bundle.putBoolean(MediaStore.PICKER_TRANSCODE_RESULT, transcodeResult);
7954         return bundle;
7955     }
7956 
7957     @NotNull
7958     private Bundle getResultForGetCloudProvider() {
7959         if (!checkPermissionShell(Binder.getCallingUid())
7960                 && !checkPermissionSelf(Binder.getCallingUid())) {
7961             throw new SecurityException(
7962                     getSecurityExceptionMessage("Get cloud provider"));
7963         }
7964         final Bundle bundle = new Bundle();
7965         bundle.putString(MediaStore.GET_CLOUD_PROVIDER_RESULT,
7966                 mPickerSyncController.getCloudProvider());
7967         return bundle;
7968     }
7969 
7970     @NotNull
7971     private Bundle getResultForGetCloudProviderLabel() {
7972         if (!checkPermissionSystem(Binder.getCallingUid())) {
7973             throw new SecurityException(getSecurityExceptionMessage("Get cloud provider label"));
7974         }
7975         final Bundle res = new Bundle();
7976         String cloudProviderLabel = null;
7977         try {
7978             cloudProviderLabel = mPickerSyncController.getCurrentCloudProviderLocalizedLabel();
7979         } catch (UnableToAcquireLockException e) {
7980             Log.d(TAG, "Timed out while attempting to acquire the cloud provider lock when getting "
7981                     + "the cloud provider label.", e);
7982         }
7983         res.putString(META_DATA_PREFERENCE_SUMMARY, cloudProviderLabel);
7984         return res;
7985     }
7986 
7987     @NotNull
7988     private Bundle getResultForSetCloudProvider(Bundle extras) {
7989         final String cloudProvider = extras.getString(MediaStore.EXTRA_CLOUD_PROVIDER);
7990         Log.i(TAG, "Request received to set cloud provider to " + cloudProvider);
7991         boolean isUpdateSuccessful = false;
7992         if (checkPermissionSelf(Binder.getCallingUid())) {
7993             isUpdateSuccessful = mPickerSyncController.setCloudProvider(cloudProvider);
7994         } else if (checkPermissionShell(Binder.getCallingUid())) {
7995             isUpdateSuccessful =
7996                     mPickerSyncController.forceSetCloudProvider(cloudProvider);
7997         } else {
7998             throw new SecurityException(
7999                     getSecurityExceptionMessage("Set cloud provider"));
8000         }
8001 
8002         if (isUpdateSuccessful) {
8003             Log.i(TAG, "Completed request to set cloud provider to " + cloudProvider);
8004         }
8005         final Bundle bundle = new Bundle();
8006         bundle.putBoolean(MediaStore.SET_CLOUD_PROVIDER_RESULT, isUpdateSuccessful);
8007         return bundle;
8008     }
8009 
8010     @NotNull
8011     private Bundle getResultForSyncProviders() {
8012         syncAllMedia();
8013         return new Bundle();
8014     }
8015 
8016     @NotNull
8017     private Bundle getResultForIsSupportedCloudProvider(String arg) {
8018         final boolean isSupported = mPickerSyncController.isProviderSupported(arg,
8019                 Binder.getCallingUid());
8020 
8021         Bundle bundle = new Bundle();
8022         bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isSupported);
8023         return bundle;
8024     }
8025 
8026     @NotNull
8027     private Bundle getResultForIsCurrentCloudProviderCall(String arg) {
8028         Bundle bundle = new Bundle();
8029         boolean isEnabled = false;
8030 
8031         if (mConfigStore.isCloudMediaInPhotoPickerEnabled()) {
8032             isEnabled =
8033                     mPickerSyncController.isProviderEnabled(
8034                             arg, Binder.getCallingUid());
8035         }
8036 
8037         bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT, isEnabled);
8038         return bundle;
8039     }
8040 
8041     @NotNull
8042     private Bundle getResultForNotifyCloudMediaChangedEvent(String arg, Bundle extras) {
8043         final boolean notifyCloudEventResult;
8044         if (mPickerSyncController.isProviderEnabled(arg, Binder.getCallingUid())) {
8045             mPickerDataLayer.handleMediaEventNotification(/*localOnly=*/ false, arg, extras);
8046             notifyCloudEventResult = true;
8047         } else {
8048             notifyCloudEventResult = false;
8049         }
8050 
8051         Bundle bundle = new Bundle();
8052         bundle.putBoolean(MediaStore.EXTRA_CLOUD_PROVIDER_RESULT,
8053                 notifyCloudEventResult);
8054         return bundle;
8055     }
8056 
8057     @NotNull
8058     private Bundle getResultForUsesFusePassThrough(String arg) {
8059         boolean isEnabled = false;
8060         try {
8061             FuseDaemon daemon = getFuseDaemonForFile(new File(arg), mVolumeCache);
8062             if (daemon != null) {
8063                 isEnabled = daemon.usesFusePassthrough();
8064             }
8065         } catch (FileNotFoundException e) {
8066         }
8067 
8068         Bundle bundle = new Bundle();
8069         bundle.putBoolean(MediaStore.USES_FUSE_PASSTHROUGH_RESULT, isEnabled);
8070         return bundle;
8071     }
8072 
8073     @NotNull
8074     private Bundle getResultForIdleMaintenanceForStableUris() {
8075         backupDatabases(null);
8076         return new Bundle();
8077     }
8078 
8079     @NotNull
8080     private Bundle getResultForReadBackup(String arg, Bundle extras) {
8081         getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
8082                 "Permission missing to call READ_BACKUP by uid:" + Binder.getCallingUid());
8083         Bundle bundle = new Bundle();
8084         Optional<BackupIdRow> backupIdRowOptional =
8085                 mDatabaseBackupAndRecovery.readDataFromBackup(arg, extras.getString(
8086                         FileColumns.DATA));
8087         String data = null;
8088         try {
8089             data = backupIdRowOptional.isPresent() ? BackupIdRow.serialize(
8090                     backupIdRowOptional.get()) : null;
8091         } catch (IOException e) {
8092             throw new RuntimeException(e);
8093         }
8094         bundle.putString(READ_BACKUP, data);
8095         return bundle;
8096     }
8097 
8098     @NotNull
8099     private Bundle getResultForGetOwnerPackageName(String arg) {
8100         getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
8101                 "Permission missing to call GET_OWNER_PACKAGE_NAME by "
8102                         + "uid:" + Binder.getCallingUid());
8103         try {
8104             String ownerPackageName = mDatabaseBackupAndRecovery.readOwnerPackageName(arg);
8105             Bundle result = new Bundle();
8106             result.putString(GET_OWNER_PACKAGE_NAME, ownerPackageName);
8107             return result;
8108         } catch (IOException e) {
8109             throw new RuntimeException(e);
8110         }
8111     }
8112 
8113     @NotNull
8114     private Bundle getResultForDeleteBackedUpFilePaths(String arg) {
8115         getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
8116                 "Permission missing to call DELETE_BACKED_UP_FILE_PATHS by "
8117                         + "uid:" + Binder.getCallingUid());
8118         mDatabaseBackupAndRecovery.deleteBackupForVolume(arg);
8119         return new Bundle();
8120     }
8121 
8122     @NotNull
8123     private Bundle getResultForGetBackupFiles() {
8124         getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
8125                 "Permission missing to call GET_BACKUP_FILES by "
8126                         + "uid:" + Binder.getCallingUid());
8127         List<File> backupFiles = mDatabaseBackupAndRecovery.getBackupFiles();
8128         List<String> fileNames = new ArrayList<>();
8129         for (File file : backupFiles) {
8130             fileNames.add(file.getName());
8131         }
8132         Bundle bundle = new Bundle();
8133         Object[] values = fileNames.toArray();
8134         String[] resultArray = Arrays.copyOf(values, values.length, String[].class);
8135         bundle.putStringArray(GET_BACKUP_FILES, resultArray);
8136         return bundle;
8137     }
8138 
8139     @NotNull
8140     private Bundle getResultForGetRecoveryData() {
8141         getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
8142                 "Permission missing to call GET_RECOVERY_DATA by "
8143                         + "uid:" + Binder.getCallingUid());
8144 
8145         String[] xattrs = null;
8146         try {
8147            xattrs = Os.listxattr("/data/media/0");
8148         } catch (ErrnoException e) {
8149             Log.w(TAG, "Error in getting xattr list ", e);
8150         }
8151 
8152         Bundle bundle = new Bundle();
8153         bundle.putStringArray(MediaStore.GET_RECOVERY_DATA, xattrs);
8154         return bundle;
8155     }
8156 
8157     private void removeRecoveryData() {
8158         getContext().enforceCallingPermission(Manifest.permission.WRITE_MEDIA_STORAGE,
8159                 "Permission missing to call REMOVE_RECOVERY_DATA by "
8160                         + "uid:" + Binder.getCallingUid());
8161 
8162         List<String> validUsers = mUserManager.getUserHandles(/* excludeDying */ true).stream()
8163                 .map(userHandle -> String.valueOf(userHandle.getIdentifier())).collect(
8164                         Collectors.toList());
8165         Log.i(TAG, "Active user ids are:" + validUsers);
8166         mDatabaseBackupAndRecovery.removeRecoveryDataExceptValidUsers(validUsers);
8167     }
8168 
8169     private String getSecurityExceptionMessage(String method) {
8170         int callingUid = Binder.getCallingUid();
8171         return String.format("%s not allowed. Calling app ID: %d, Calling UID %d. "
8172                         + "Media Provider app ID: %d, Media Provider UID: %d.",
8173                 method,
8174                 UserHandle.getAppId(callingUid),
8175                 callingUid,
8176                 UserHandle.getAppId(MY_UID),
8177                 MY_UID);
8178     }
8179 
8180     public void backupDatabases(CancellationSignal signal) {
8181         mDatabaseBackupAndRecovery.backupDatabases(mInternalDatabase, mExternalDatabase, signal);
8182     }
8183 
8184     public void recoverPublicVolume(MediaVolume volume) {
8185         if (volume.isPublicVolume()
8186                 && mDatabaseBackupAndRecovery.isStableUrisEnabled(volume.getName())) {
8187             Log.d(TAG, "Querying external_primary to make sure it's available");
8188             try (Cursor cursor = getContext().getContentResolver()
8189                     .query(MediaStore.Images.Media.getContentUri(VOLUME_EXTERNAL_PRIMARY),
8190                             new String[]{FileColumns._ID}, null, null)) {
8191             } catch (Exception e) {
8192                 Log.e(TAG, "Can't restore public volume because EXTERNAL_PRIMARY is not available");
8193                 return;
8194             }
8195 
8196             try {
8197                 mExternalDatabase.tryRecoverPublicVolume(volume.getName());
8198             } catch (Exception e) {
8199                 Log.e(TAG, "Exception in public volume recovery for " + volume.getName(), e);
8200             }
8201         }
8202     }
8203 
8204     private void syncAllMedia() {
8205         // Clear the binder calling identity so that we can sync the unexported
8206         // local_provider while running as MediaProvider
8207         final long t = Binder.clearCallingIdentity();
8208         try {
8209             Log.v(TAG, "Test initiated cloud provider sync");
8210             mPickerSyncController.syncAllMedia();
8211         } finally {
8212             Binder.restoreCallingIdentity(t);
8213         }
8214     }
8215 
8216     private AssetFileDescriptor getOriginalMediaFormatFileDescriptor(Bundle extras)
8217             throws FileNotFoundException {
8218         try (ParcelFileDescriptor inputPfd =
8219                 extras.getParcelable(MediaStore.EXTRA_FILE_DESCRIPTOR)) {
8220             File file = getFileFromFileDescriptor(inputPfd);
8221             // Convert from FUSE file to lower fs file because the supportsTranscode() check below
8222             // expects a lower fs file format
8223             file = fromFuseFile(file);
8224             if (!mTranscodeHelper.supportsTranscode(file.getPath())) {
8225                 // Note that we should be checking if a file is a modern format and not just
8226                 // that it supports transcoding, unfortunately, checking modern format
8227                 // requires either a db query or media scan which can lead to ANRs if apps
8228                 // or the system implicitly call this method as part of a
8229                 // MediaPlayer#setDataSource.
8230                 throw new FileNotFoundException("Input file descriptor is already original");
8231             }
8232 
8233             FuseDaemon fuseDaemon = getFuseDaemonForFile(file, mVolumeCache);
8234             int uid = Binder.getCallingUid();
8235 
8236             FdAccessResult result = fuseDaemon.checkFdAccess(inputPfd, uid);
8237             if (!result.isSuccess()) {
8238                 throw new FileNotFoundException("Invalid path for original media format file");
8239             }
8240 
8241             String outputPath = result.filePath;
8242             boolean shouldRedact = result.shouldRedact;
8243 
8244             int posixMode = Os.fcntlInt(inputPfd.getFileDescriptor(), F_GETFL,
8245                     0 /* args */);
8246             int modeBits = FileUtils.translateModePosixToPfd(posixMode);
8247 
8248             ParcelFileDescriptor pfd = openWithFuse(outputPath, uid, 0 /* mediaCapabilitiesUid */,
8249                     modeBits, shouldRedact, false /* shouldTranscode */,
8250                     0 /* transcodeReason */);
8251             return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
8252         } catch (IOException e) {
8253             throw new FileNotFoundException("Failed to fetch original file descriptor");
8254         } catch (ErrnoException e) {
8255             Log.w(TAG, "Failed to fetch access mode for file descriptor", e);
8256             throw new FileNotFoundException("Failed to fetch access mode for file descriptor");
8257         }
8258     }
8259 
8260     /**
8261      * Grant similar read/write access for mediaStoreUri as the caller has for documentsUri.
8262      *
8263      * Note: This function assumes that read permission check for documentsUri is already enforced.
8264      * Note: This function currently does not check/grant for persisted Uris. Support for this can
8265      * be added eventually, but the calling application will have to call
8266      * ContentResolver#takePersistableUriPermission(Uri, int) for the mediaStoreUri to persist.
8267      *
8268      * @param documentsUri DocumentsProvider format content Uri
8269      * @param mediaStoreUri MediaStore format content Uri
8270      * @param callingPid pid of the caller
8271      * @param callingUid uid of the caller
8272      * @param callingPackage package name of the caller
8273      */
8274     private void copyUriPermissionGrants(Uri documentsUri, Uri mediaStoreUri,
8275             int callingPid, int callingUid, String callingPackage) {
8276         // No need to check for read permission, as we enforce it already.
8277         int modeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
8278         if (getContext().checkUriPermission(documentsUri, callingPid, callingUid,
8279                 Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PERMISSION_GRANTED) {
8280             modeFlags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
8281         }
8282         getContext().grantUriPermission(callingPackage, mediaStoreUri, modeFlags);
8283     }
8284 
8285     static List<Uri> collectUris(ClipData clipData) {
8286         final ArrayList<Uri> res = new ArrayList<>();
8287         for (int i = 0; i < clipData.getItemCount(); i++) {
8288             res.add(clipData.getItemAt(i).getUri());
8289         }
8290         return res;
8291     }
8292 
8293     /**
8294      * Return the filesystem path of the real file on disk that is represented
8295      * by the given {@link ParcelFileDescriptor}.
8296      *
8297      * Note that the file may be a FUSE or lower fs file and depending on the purpose might need
8298      * to be converted with {@link FileUtils#toFuseFile} or {@link FileUtils#fromFuseFile}.
8299      *
8300      * Copied from {@link ParcelFileDescriptor#getFile}
8301      */
8302     private static File getFileFromFileDescriptor(ParcelFileDescriptor fileDescriptor)
8303             throws IOException {
8304         try {
8305             final String path = Os.readlink("/proc/self/fd/" + fileDescriptor.getFd());
8306             if (OsConstants.S_ISREG(Os.stat(path).st_mode)) {
8307                 return new File(path);
8308             } else {
8309                 throw new IOException("Not a regular file: " + path);
8310             }
8311         } catch (ErrnoException e) {
8312             throw e.rethrowAsIOException();
8313         }
8314     }
8315 
8316     /**
8317      * Generate the {@link PendingIntent} for the given grant request. This
8318      * method also checks the incoming arguments for security purposes
8319      * before creating the privileged {@link PendingIntent}.
8320      */
8321     @NonNull
8322     private PendingIntent createRequest(@NonNull String method, @NonNull Bundle extras) {
8323         final ClipData clipData = extras.getParcelable(MediaStore.EXTRA_CLIP_DATA);
8324         final List<Uri> uris = collectUris(clipData);
8325 
8326         if (getCallingPackageTargetSdkVersion() > Build.VERSION_CODES.VANILLA_ICE_CREAM
8327                 && CompatChanges.isChangeEnabled(LIMIT_CREATE_REQUEST_URIS) && uris.size() > 2000) {
8328             throw new IllegalArgumentException("URI list restricted to 2000 per request");
8329         }
8330 
8331         for (Uri uri : uris) {
8332             final int match = matchUri(uri, false);
8333             switch (match) {
8334                 case IMAGES_MEDIA_ID:
8335                 case AUDIO_MEDIA_ID:
8336                 case VIDEO_MEDIA_ID:
8337                 case AUDIO_PLAYLISTS_ID:
8338                     // Caller is requesting a specific media item by its ID,
8339                     // which means it's valid for requests
8340                     break;
8341                 case FILES_ID:
8342                     // Allow only subtitle files
8343                     if (!isSubtitleFile(uri)) {
8344                         throw new IllegalArgumentException(
8345                                 "All requested items must be Media items");
8346                     }
8347                     break;
8348                 default:
8349                     throw new IllegalArgumentException(
8350                             "All requested items must be referenced by specific ID");
8351             }
8352         }
8353 
8354         // Enforce that limited set of columns can be mutated
8355         final ContentValues values = extras.getParcelable(MediaStore.EXTRA_CONTENT_VALUES);
8356         final List<String> allowedColumns;
8357         switch (method) {
8358             case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
8359                 allowedColumns = Collections.singletonList(
8360                         MediaColumns.IS_FAVORITE);
8361                 break;
8362             case MediaStore.CREATE_TRASH_REQUEST_CALL:
8363                 allowedColumns = Collections.singletonList(
8364                         MediaColumns.IS_TRASHED);
8365                 break;
8366             default:
8367                 allowedColumns = Collections.emptyList();
8368                 break;
8369         }
8370         if (values != null) {
8371             for (String key : values.keySet()) {
8372                 if (!allowedColumns.contains(key)) {
8373                     throw new IllegalArgumentException("Invalid column " + key);
8374                 }
8375             }
8376         }
8377 
8378         final Context context = getContext();
8379         final Intent intent = new Intent(method, null, context, PermissionActivity.class);
8380         intent.putExtras(extras);
8381         final ActivityOptions options = ActivityOptions.makeBasic();
8382         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
8383             options.setPendingIntentCreatorBackgroundActivityStartMode(
8384                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
8385         }
8386         return PendingIntent.getActivity(context, PermissionActivity.REQUEST_CODE, intent,
8387                 FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, options.toBundle());
8388     }
8389 
8390     /**
8391      * @return true if the given Files uri has media_type=MEDIA_TYPE_SUBTITLE
8392      */
8393     private boolean isSubtitleFile(Uri uri) {
8394         final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
8395         try (Cursor cursor = queryForSingleItem(uri, new String[]{FileColumns.MEDIA_TYPE}, null,
8396                 null, null)) {
8397             return cursor.getInt(0) == FileColumns.MEDIA_TYPE_SUBTITLE;
8398         } catch (FileNotFoundException e) {
8399             Log.e(TAG, "Couldn't find database row for requested uri " + uri, e);
8400         } finally {
8401             restoreLocalCallingIdentity(tokenInner);
8402         }
8403         return false;
8404     }
8405 
8406     /**
8407      * Ensure that all local databases have a custom collator registered for the
8408      * given {@link ULocale} locale.
8409      *
8410      * @return the corresponding custom collation name to be used in
8411      *         {@code ORDER BY} clauses.
8412      */
8413     @NonNull
8414     private String ensureCustomCollator(@NonNull String locale) {
8415         // Quick check that requested locale looks reasonable
8416         new ULocale(locale);
8417 
8418         final String collationName = "custom_" + locale.replaceAll("[^a-zA-Z]", "");
8419         synchronized (mCustomCollators) {
8420             if (!mCustomCollators.contains(collationName)) {
8421                 for (DatabaseHelper helper : new DatabaseHelper[] {
8422                         mInternalDatabase,
8423                         mExternalDatabase
8424                 }) {
8425                     helper.runWithoutTransaction((db) -> {
8426                         db.execPerConnectionSQL("SELECT icu_load_collation(?, ?);",
8427                                 new String[] { locale, collationName });
8428                         return null;
8429                     });
8430                 }
8431                 mCustomCollators.add(collationName);
8432             }
8433         }
8434         return collationName;
8435     }
8436 
8437     private int pruneThumbnails(@NonNull SQLiteDatabase db, @NonNull CancellationSignal signal) {
8438         int prunedCount = 0;
8439 
8440         // Determine all known media items
8441         final LongArray knownIds = new LongArray();
8442         try (Cursor c = db.query(true, "files", new String[] { BaseColumns._ID },
8443                 null, null, null, null, null, null, signal)) {
8444             while (c.moveToNext()) {
8445                 knownIds.add(c.getLong(0));
8446             }
8447         }
8448 
8449         final long[] knownIdsRaw = knownIds.toArray();
8450         Arrays.sort(knownIdsRaw);
8451 
8452         for (MediaVolume volume : mVolumeCache.getExternalVolumes()) {
8453             final List<File> thumbDirs;
8454             try {
8455                 thumbDirs = getThumbnailDirectories(volume);
8456             } catch (FileNotFoundException e) {
8457                 Log.w(TAG, "Failed to resolve volume " + volume.getName(), e);
8458                 continue;
8459             }
8460 
8461             // Reconcile all thumbnails, deleting stale items
8462             for (File thumbDir : thumbDirs) {
8463                 // Possibly bail before digging into each directory
8464                 signal.throwIfCanceled();
8465 
8466                 final File[] files = thumbDir.listFiles();
8467                 for (File thumbFile : (files != null) ? files : new File[0]) {
8468                     if (Objects.equals(thumbFile.getName(), FILE_DATABASE_UUID)) continue;
8469                     if (Objects.equals(thumbFile.getName(), MEDIA_IGNORE_FILENAME)) continue;
8470                     final String name = FileUtils.extractFileName(thumbFile.getName());
8471                     try {
8472                         final long id = Long.parseLong(name);
8473                         if (Arrays.binarySearch(knownIdsRaw, id) >= 0) {
8474                             // Thumbnail belongs to known media, keep it
8475                             continue;
8476                         }
8477                     } catch (NumberFormatException e) {
8478                     }
8479 
8480                     Log.v(TAG, "Deleting stale thumbnail " + thumbFile);
8481                     deleteAndInvalidate(thumbFile);
8482                     prunedCount++;
8483                 }
8484             }
8485         }
8486 
8487         // Also delete stale items from legacy tables
8488         db.execSQL("delete from thumbnails "
8489                 + "where image_id not in (select _id from images)");
8490         db.execSQL("delete from videothumbnails "
8491                 + "where video_id not in (select _id from video)");
8492 
8493         return prunedCount;
8494     }
8495 
8496     abstract class Thumbnailer {
8497         final String directoryName;
8498 
8499         public Thumbnailer(String directoryName) {
8500             this.directoryName = directoryName;
8501         }
8502 
8503         private File getThumbnailFile(Uri uri) throws IOException {
8504             // Always save generated thumbnails to primary storage
8505             final File volumePath = getVolumePath(MediaStore.VOLUME_EXTERNAL_PRIMARY);
8506             return FileUtils.buildPath(volumePath, directoryName,
8507                     DIRECTORY_THUMBNAILS, ContentUris.parseId(uri) + ".jpg");
8508         }
8509 
8510         public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal)
8511                 throws IOException;
8512 
8513         public ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
8514                 throws IOException {
8515             // First attempt to fast-path by opening the thumbnail; if it
8516             // doesn't exist we fall through to create it below
8517             final File thumbFile = getThumbnailFile(uri);
8518             try {
8519                 return FileUtils.openSafely(thumbFile,
8520                         ParcelFileDescriptor.MODE_READ_ONLY);
8521             } catch (FileNotFoundException ignored) {
8522             }
8523 
8524             final File thumbDir = thumbFile.getParentFile();
8525             thumbDir.mkdirs();
8526 
8527             // When multiple threads race for the same thumbnail, the second
8528             // thread could return a file with a thumbnail still in
8529             // progress. We could add heavy per-ID locking to mitigate this
8530             // rare race condition, but it's simpler to have both threads
8531             // generate the same thumbnail using temporary files and rename
8532             // them into place once finished.
8533             final File thumbTempFile = File.createTempFile("thumb", null, thumbDir);
8534 
8535             ParcelFileDescriptor thumbWrite = null;
8536             ParcelFileDescriptor thumbRead = null;
8537             try {
8538                 // Open our temporary file twice: once for local writing, and
8539                 // once for remote reading. Both FDs point at the same
8540                 // underlying inode on disk, so they're stable across renames
8541                 // to avoid race conditions between threads.
8542                 thumbWrite = FileUtils.openSafely(thumbTempFile,
8543                         ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE);
8544                 thumbRead = FileUtils.openSafely(thumbTempFile,
8545                         ParcelFileDescriptor.MODE_READ_ONLY);
8546 
8547                 final Bitmap thumbnail = getThumbnailBitmap(uri, signal);
8548                 thumbnail.compress(Bitmap.CompressFormat.JPEG, 90,
8549                         new FileOutputStream(thumbWrite.getFileDescriptor()));
8550 
8551                 try {
8552                     // Use direct syscall for better failure logs
8553                     Os.rename(thumbTempFile.getAbsolutePath(), thumbFile.getAbsolutePath());
8554                 } catch (ErrnoException e) {
8555                     e.rethrowAsIOException();
8556                 }
8557 
8558                 // Everything above went peachy, so return a duplicate of our
8559                 // already-opened read FD to keep our finally logic below simple
8560                 return thumbRead.dup();
8561 
8562             } finally {
8563                 // Regardless of success or failure, try cleaning up any
8564                 // remaining temporary file and close all our local FDs
8565                 FileUtils.closeQuietly(thumbWrite);
8566                 FileUtils.closeQuietly(thumbRead);
8567                 deleteAndInvalidate(thumbTempFile);
8568             }
8569         }
8570 
8571         public void invalidateThumbnail(Uri uri) throws IOException {
8572             deleteAndInvalidate(getThumbnailFile(uri));
8573         }
8574     }
8575 
8576     private final Thumbnailer mAudioThumbnailer = new Thumbnailer(Environment.DIRECTORY_MUSIC) {
8577         @Override
8578         public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
8579             return ThumbnailUtils.createAudioThumbnail(queryForDataFile(uri, signal),
8580                     mThumbSize, signal);
8581         }
8582     };
8583 
8584     private final Thumbnailer mVideoThumbnailer = new Thumbnailer(Environment.DIRECTORY_MOVIES) {
8585         @Override
8586         public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
8587             return ThumbnailUtils.createVideoThumbnail(queryForDataFile(uri, signal),
8588                     mThumbSize, signal);
8589         }
8590     };
8591 
8592     private final Thumbnailer mImageThumbnailer = new Thumbnailer(Environment.DIRECTORY_PICTURES) {
8593         @Override
8594         public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
8595             return ThumbnailUtils.createImageThumbnail(queryForDataFile(uri, signal),
8596                     mThumbSize, signal);
8597         }
8598     };
8599 
8600     private List<File> getThumbnailDirectories(MediaVolume volume) throws FileNotFoundException {
8601         final File volumePath = volume.getPath();
8602         return Arrays.asList(
8603                 FileUtils.buildPath(volumePath, Environment.DIRECTORY_MUSIC, DIRECTORY_THUMBNAILS),
8604                 FileUtils.buildPath(volumePath, Environment.DIRECTORY_MOVIES, DIRECTORY_THUMBNAILS),
8605                 FileUtils.buildPath(volumePath, Environment.DIRECTORY_PICTURES,
8606                         DIRECTORY_THUMBNAILS));
8607     }
8608 
8609     private void invalidateThumbnails(Uri uri) {
8610         Trace.beginSection("MP.invalidateThumbnails");
8611         try {
8612             invalidateThumbnailsInternal(uri);
8613         } finally {
8614             Trace.endSection();
8615         }
8616     }
8617 
8618     private void invalidateThumbnailsInternal(Uri uri) {
8619         final long id = ContentUris.parseId(uri);
8620         try {
8621             mAudioThumbnailer.invalidateThumbnail(uri);
8622             mVideoThumbnailer.invalidateThumbnail(uri);
8623             mImageThumbnailer.invalidateThumbnail(uri);
8624         } catch (IOException ignored) {
8625         }
8626 
8627         final DatabaseHelper helper;
8628         try {
8629             helper = getDatabaseForUri(uri);
8630         } catch (VolumeNotFoundException e) {
8631             Log.w(TAG, e);
8632             return;
8633         }
8634 
8635         helper.runWithTransaction((db) -> {
8636             final String idString = Long.toString(id);
8637             try (Cursor c = db.rawQuery("select _data from thumbnails where image_id=?"
8638                     + " union all select _data from videothumbnails where video_id=?",
8639                     new String[] { idString, idString })) {
8640                 while (c.moveToNext()) {
8641                     String path = c.getString(0);
8642                     deleteIfAllowed(uri, Bundle.EMPTY, path);
8643                 }
8644             }
8645 
8646             db.execSQL("delete from thumbnails where image_id=?", new String[] { idString });
8647             db.execSQL("delete from videothumbnails where video_id=?", new String[] { idString });
8648             return null;
8649         });
8650     }
8651 
8652     /**
8653      * @deprecated all operations should be routed through the overload that
8654      *             accepts a {@link Bundle} of extras.
8655      */
8656     @Override
8657     @Deprecated
8658     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
8659         return update(uri, values,
8660                 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null));
8661     }
8662 
8663     @Override
8664     public int update(@NonNull Uri uri, @Nullable ContentValues values,
8665             @Nullable Bundle extras) {
8666         Trace.beginSection(safeTraceSectionNameWithUri("update", uri));
8667         try {
8668             return updateInternal(uri, values, extras);
8669         } catch (FallbackException e) {
8670             return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion());
8671         } finally {
8672             Trace.endSection();
8673         }
8674     }
8675 
8676     private int updateInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
8677             @Nullable Bundle extras) throws FallbackException {
8678         final String volumeName = getVolumeName(uri);
8679         PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
8680 
8681         extras = (extras != null) ? extras : new Bundle();
8682         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
8683         extras.remove(QUERY_ARG_REDACTED_URI);
8684 
8685         if (isRedactedUri(uri)) {
8686             // we don't support update on redacted uris.
8687             return 0;
8688         }
8689 
8690         // Related items are only considered for new media creation, and they
8691         // can't be leveraged to move existing content into blocked locations
8692         extras.remove(QUERY_ARG_RELATED_URI);
8693         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
8694         extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
8695 
8696         final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION);
8697         final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);
8698 
8699         // Limit the hacky workaround to camera targeting Q and below, to allow newer versions
8700         // of camera that does the right thing to work correctly.
8701         if ("com.google.android.GoogleCamera".equals(getCallingPackageOrSelf())
8702                 && getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) {
8703             if (matchUri(uri, false) == IMAGES_MEDIA_ID) {
8704                 Log.w(TAG, "Working around app bug in b/111966296");
8705                 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri));
8706             } else if (matchUri(uri, false) == VIDEO_MEDIA_ID) {
8707                 Log.w(TAG, "Working around app bug in b/112246630");
8708                 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri));
8709             }
8710         }
8711 
8712         uri = safeUncanonicalize(uri);
8713 
8714         int count;
8715 
8716         final boolean allowHidden = isCallingPackageAllowedHidden();
8717         final int match = matchUri(uri, allowHidden);
8718         final DatabaseHelper helper = getDatabaseForUri(uri);
8719 
8720         switch (match) {
8721             case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
8722                 extras.putString(QUERY_ARG_SQL_SELECTION,
8723                         BaseColumns._ID + "=" + uri.getPathSegments().get(5));
8724                 // fall-through
8725             case AUDIO_PLAYLISTS_ID_MEMBERS: {
8726                 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
8727                 final Uri playlistUri = ContentUris.withAppendedId(
8728                         MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);
8729                 if (uri.getBooleanQueryParameter("move", false)) {
8730                     // Convert explicit request into query; sigh, moveItem()
8731                     // uses zero-based indexing instead of one-based indexing
8732                     final int from = Integer.parseInt(uri.getPathSegments().get(5)) + 1;
8733                     final int to = initialValues.getAsInteger(Playlists.Members.PLAY_ORDER) + 1;
8734                     extras.putString(QUERY_ARG_SQL_SELECTION,
8735                             Playlists.Members.PLAY_ORDER + "=" + from);
8736                     initialValues.put(Playlists.Members.PLAY_ORDER, to);
8737                 }
8738 
8739                 // Playlist contents are always persisted directly into playlist
8740                 // files on disk to ensure that we can reliably migrate between
8741                 // devices and recover from database corruption
8742                 final int index;
8743                 if (initialValues.containsKey(Playlists.Members.PLAY_ORDER)) {
8744                     index = movePlaylistMembers(playlistUri, initialValues, extras);
8745                 } else {
8746                     index = resolvePlaylistIndex(playlistUri, extras);
8747                 }
8748                 if (initialValues.containsKey(Playlists.Members.AUDIO_ID)) {
8749                     final Bundle queryArgs = new Bundle();
8750                     queryArgs.putString(QUERY_ARG_SQL_SELECTION,
8751                             Playlists.Members.PLAY_ORDER + "=" + (index + 1));
8752                     removePlaylistMembers(playlistUri, queryArgs);
8753 
8754                     final ContentValues values = new ContentValues();
8755                     values.put(Playlists.Members.AUDIO_ID,
8756                             initialValues.getAsString(Playlists.Members.AUDIO_ID));
8757                     values.put(Playlists.Members.PLAY_ORDER, (index + 1));
8758                     addPlaylistMembers(playlistUri, values);
8759                 }
8760 
8761                 acceptWithExpansion(helper::notifyUpdate, volumeName, playlistId,
8762                         FileColumns.MEDIA_TYPE_PLAYLIST, false);
8763                 return 1;
8764             }
8765         }
8766 
8767         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null);
8768 
8769         // Give callers interacting with a specific media item a chance to
8770         // escalate access if they don't already have it
8771         switch (match) {
8772             case AUDIO_MEDIA_ID:
8773             case VIDEO_MEDIA_ID:
8774             case IMAGES_MEDIA_ID:
8775                 enforceCallingPermission(uri, extras, true);
8776         }
8777 
8778         boolean triggerInvalidate = false;
8779         boolean triggerScan = false;
8780         boolean isUriPublished = false;
8781         if (initialValues != null) {
8782             // IDs are forever; nobody should be editing them
8783             initialValues.remove(MediaColumns._ID);
8784 
8785             // Expiration times are hard-coded; let's derive them
8786             FileUtils.computeDateExpires(initialValues);
8787 
8788             // Ignore or augment incoming raw filesystem paths
8789             for (String column : sDataColumns.keySet()) {
8790                 if (!initialValues.containsKey(column)) continue;
8791 
8792                 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
8793                     // Mutation allowed
8794                 } else {
8795                     Log.w(TAG, "Ignoring mutation of  " + column + " from "
8796                             + getCallingPackageOrSelf());
8797                     initialValues.remove(column);
8798                 }
8799             }
8800 
8801             // Enforce allowed ownership transfers
8802             if (initialValues.containsKey(MediaColumns.OWNER_PACKAGE_NAME)) {
8803                 if (isCallingPackageSelf() || isCallingPackageShell()) {
8804                     // When the caller is the media scanner or the shell, we let
8805                     // them change ownership however they see fit; nothing to do
8806                 } else if (isCallingPackageDelegator()) {
8807                     // When the caller is a delegator, allow them to shift
8808                     // ownership only when current owner, or when ownerless
8809                     final String currentOwner;
8810                     final String proposedOwner = initialValues
8811                             .getAsString(MediaColumns.OWNER_PACKAGE_NAME);
8812                     final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
8813                             ContentUris.parseId(uri));
8814                     try (Cursor c = queryForSingleItem(genericUri,
8815                             new String[] { MediaColumns.OWNER_PACKAGE_NAME }, null, null, null)) {
8816                         currentOwner = c.getString(0);
8817                     } catch (FileNotFoundException e) {
8818                         throw new IllegalStateException(e);
8819                     }
8820                     final boolean transferAllowed = (currentOwner == null)
8821                             || Arrays.asList(getSharedPackagesForPackage(getCallingPackageOrSelf()))
8822                                     .contains(currentOwner);
8823                     if (transferAllowed) {
8824                         Log.v(TAG, "Ownership transfer from " + currentOwner + " to "
8825                                 + proposedOwner + " allowed");
8826                     } else {
8827                         Log.w(TAG, "Ownership transfer from " + currentOwner + " to "
8828                                 + proposedOwner + " blocked");
8829                         initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
8830                     }
8831                 } else {
8832                     // Otherwise no ownership changes are allowed
8833                     initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
8834                 }
8835             }
8836 
8837             if (initialValues.containsKey(FileColumns.GENERATION_MODIFIED)
8838                     && !isCallingPackageSelf()) {
8839                 // We only allow MediaScanner to send updates for generation modified
8840                 initialValues.remove(FileColumns.GENERATION_MODIFIED);
8841             }
8842 
8843             // Enforce oem_metadata permission if caller is not MediaProvider
8844             if (Flags.enableOemMetadataUpdate() && initialValues.containsKey(OEM_METADATA)) {
8845                 enforcePermissionCheckForOemMetadataUpdate();
8846             }
8847 
8848             if (!isCallingPackageSelf()) {
8849                 Trace.beginSection("MP.filter");
8850 
8851                 // We default to filtering mutable columns, except when we know
8852                 // the single item being updated is pending; when it's finally
8853                 // published we'll overwrite these values.
8854                 final Uri finalUri = uri;
8855                 final Supplier<Boolean> isPending = new CachedSupplier<>(() -> {
8856                     return isPending(finalUri);
8857                 });
8858 
8859                 // Column values controlled by media scanner aren't writable by
8860                 // apps, since any edits here don't reflect the metadata on
8861                 // disk, and they'd be overwritten during a rescan.
8862                 for (String column : new ArraySet<>(initialValues.keySet())) {
8863                     if (sMutableColumns.contains(column)) {
8864                         // Mutation normally allowed
8865                     } else if (isPending.get()) {
8866                         // Mutation relaxed while pending
8867                     } else {
8868                         Log.w(TAG, "Ignoring mutation of " + column + " from "
8869                                 + getCallingPackageOrSelf());
8870                         initialValues.remove(column);
8871                         triggerScan = true;
8872                     }
8873 
8874                     // If we're publishing this item, perform a blocking scan to
8875                     // make sure metadata is updated
8876                     if (MediaColumns.IS_PENDING.equals(column)) {
8877                         triggerScan = true;
8878                         isUriPublished = true;
8879                         // Explicitly clear columns used to ignore no-op scans,
8880                         // since we need to force a scan on publish
8881                         initialValues.putNull(MediaColumns.DATE_MODIFIED);
8882                         initialValues.putNull(MediaColumns.SIZE);
8883                     }
8884                 }
8885 
8886                 Trace.endSection();
8887             }
8888 
8889             if ("files".equals(qb.getTables())) {
8890                 maybeMarkAsDownload(initialValues);
8891             }
8892 
8893             // We no longer track location metadata
8894             if (!isCallingPackageSelf()) {
8895                 if (initialValues.containsKey(LATITUDE)) {
8896                     initialValues.putNull(LATITUDE);
8897                 }
8898                 if (initialValues.containsKey(LONGITUDE)) {
8899                     initialValues.putNull(LONGITUDE);
8900                 }
8901             }
8902 
8903             if (getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) {
8904                 // These columns are removed in R.
8905                 if (initialValues.containsKey("primary_directory")) {
8906                     initialValues.remove("primary_directory");
8907                 }
8908                 if (initialValues.containsKey("secondary_directory")) {
8909                     initialValues.remove("secondary_directory");
8910                 }
8911             }
8912         }
8913 
8914         // If we're not updating anything, then we can skip
8915         if (initialValues.isEmpty()) return 0;
8916 
8917         final boolean isThumbnail;
8918         switch (match) {
8919             case IMAGES_THUMBNAILS:
8920             case IMAGES_THUMBNAILS_ID:
8921             case VIDEO_THUMBNAILS:
8922             case VIDEO_THUMBNAILS_ID:
8923             case AUDIO_ALBUMART:
8924             case AUDIO_ALBUMART_ID:
8925                 isThumbnail = true;
8926                 break;
8927             default:
8928                 isThumbnail = false;
8929                 break;
8930         }
8931 
8932         switch (match) {
8933             case AUDIO_PLAYLISTS:
8934             case AUDIO_PLAYLISTS_ID:
8935                 // Playlist names are stored as display names, but leave
8936                 // values untouched if the caller is ModernMediaScanner
8937                 if (!isCallingPackageSelf()) {
8938                     if (initialValues.containsKey(Playlists.NAME)) {
8939                         initialValues.put(MediaColumns.DISPLAY_NAME,
8940                                 initialValues.getAsString(Playlists.NAME));
8941                     }
8942                     if (!initialValues.containsKey(MediaColumns.MIME_TYPE)) {
8943                         initialValues.put(MediaColumns.MIME_TYPE, "audio/mpegurl");
8944                     }
8945                 }
8946                 break;
8947         }
8948 
8949         // If we're touching columns that would change placement of a file,
8950         // blend in current values and recalculate path
8951         final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT,
8952                 !isCallingPackageSelf());
8953         if (containsAny(initialValues.keySet(), sPlacementColumns)
8954                 && !initialValues.containsKey(MediaColumns.DATA)
8955                 && !isThumbnail
8956                 && allowMovement) {
8957             Trace.beginSection("MP.movement");
8958 
8959             // We only support movement under well-defined collections
8960             switch (match) {
8961                 case AUDIO_MEDIA_ID:
8962                 case AUDIO_PLAYLISTS_ID:
8963                 case VIDEO_MEDIA_ID:
8964                 case IMAGES_MEDIA_ID:
8965                 case DOWNLOADS_ID:
8966                 case FILES_ID:
8967                     // Check if the caller has the required permissions to do placement
8968                     enforceCallingPermission(uri, extras, true);
8969                     break;
8970                 default:
8971                     throw new IllegalArgumentException("Movement of " + uri
8972                             + " which isn't part of well-defined collection not allowed");
8973             }
8974 
8975             final LocalCallingIdentity token = clearLocalCallingIdentity();
8976             final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
8977                     ContentUris.parseId(uri));
8978             try (Cursor c = queryForSingleItem(genericUri,
8979                     sPlacementColumns.toArray(new String[0]), userWhere, userWhereArgs, null)) {
8980                 for (int i = 0; i < c.getColumnCount(); i++) {
8981                     final String column = c.getColumnName(i);
8982                     if (!initialValues.containsKey(column)) {
8983                         initialValues.put(column, c.getString(i));
8984                     }
8985                 }
8986             } catch (FileNotFoundException e) {
8987                 throw new IllegalStateException(e);
8988             } finally {
8989                 restoreLocalCallingIdentity(token);
8990             }
8991 
8992             // Regenerate path using blended values; this will throw if caller
8993             // is attempting to place file into invalid location
8994             final String beforePath = initialValues.getAsString(MediaColumns.DATA);
8995             final String beforeVolume = extractVolumeName(beforePath);
8996             final String beforeOwner = extractPathOwnerPackageName(beforePath);
8997 
8998             if (beforeVolume != null && MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
8999                 // Replace "external" with the volumeName
9000                 uri = replaceExternalUriWithVolumeName(uri, beforeVolume);
9001             }
9002 
9003             initialValues.remove(MediaColumns.DATA);
9004             ensureNonUniqueFileColumns(match, uri, extras, initialValues, beforePath);
9005 
9006             final String probePath = initialValues.getAsString(MediaColumns.DATA);
9007             final String probeVolume = extractVolumeName(probePath);
9008             final String probeOwner = extractPathOwnerPackageName(probePath);
9009             if (StringUtils.equalIgnoreCase(beforePath, probePath)) {
9010                 Log.d(TAG, "Identical paths " + beforePath + "; not moving");
9011             } else if (!Objects.equals(beforeVolume, probeVolume)) {
9012                 throw new IllegalArgumentException("Changing volume from " + beforePath + " to "
9013                         + probePath + " not allowed");
9014             } else if (!isUpdateAllowedForOwnedPath(beforeOwner, probeOwner, beforePath,
9015                     probePath)) {
9016                 throw new IllegalArgumentException("Changing ownership from " + beforePath + " to "
9017                         + probePath + " not allowed");
9018             } else {
9019                 // Now that we've confirmed an actual movement is taking place,
9020                 // ensure we have a unique destination
9021                 initialValues.remove(MediaColumns.DATA);
9022                 ensureUniqueFileColumns(match, uri, extras, initialValues, beforePath);
9023 
9024                 String afterPath = initialValues.getAsString(MediaColumns.DATA);
9025 
9026                 if (isCrossUserEnabled()) {
9027                     String afterVolume = extractVolumeName(afterPath);
9028                     String afterVolumePath =  extractVolumePath(afterPath);
9029                     String beforeVolumePath = extractVolumePath(beforePath);
9030 
9031                     if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equals(beforeVolume)
9032                             && beforeVolume.equals(afterVolume)
9033                             && !beforeVolumePath.equals(afterVolumePath)) {
9034                         // On cross-user enabled devices, it can happen that a rename intended as
9035                         // /storage/emulated/999/foo -> /storage/emulated/999/foo can end up as
9036                         // /storage/emulated/999/foo -> /storage/emulated/0/foo. We now fix-up
9037                         afterPath = afterPath.replaceFirst(afterVolumePath, beforeVolumePath);
9038                     }
9039                 }
9040 
9041                 Log.d(TAG, "Moving " + beforePath + " to " + afterPath);
9042                 try {
9043                     Os.rename(beforePath, afterPath);
9044                     invalidateFuseDentry(beforePath);
9045                     invalidateFuseDentry(afterPath);
9046                 } catch (ErrnoException e) {
9047                     if (e.errno == OsConstants.ENOENT) {
9048                         Log.d(TAG, "Missing file at " + beforePath + "; continuing anyway");
9049                     } else {
9050                         throw new IllegalStateException(e);
9051                     }
9052                 }
9053                 initialValues.put(MediaColumns.DATA, afterPath);
9054 
9055                 // Some indexed metadata may have been derived from the path on
9056                 // disk, so scan this item again to update it
9057                 triggerScan = true;
9058             }
9059 
9060             Trace.endSection();
9061         }
9062 
9063         assertPrivatePathNotInValues(initialValues);
9064 
9065         // Make sure any updated paths look consistent
9066         assertFileColumnsConsistent(match, uri, initialValues);
9067 
9068         if (initialValues.containsKey(FileColumns.DATA)) {
9069             // If we're changing paths, invalidate any thumbnails
9070             triggerInvalidate = true;
9071 
9072             // If the new file exists, trigger a scan to adjust any metadata
9073             // that might be derived from the path
9074             final String data = initialValues.getAsString(FileColumns.DATA);
9075             if (!TextUtils.isEmpty(data) && new File(data).exists()) {
9076                 triggerScan = true;
9077             }
9078         }
9079 
9080         // If we're already doing this update from an internal scan, no need to
9081         // kick off another no-op scan
9082         if (isCallingPackageSelf()) {
9083             triggerScan = false;
9084         }
9085 
9086         // Since the update mutation may prevent us from matching items after
9087         // it's applied, we need to snapshot affected IDs here
9088         final LongArray updatedIds = new LongArray();
9089         if (triggerInvalidate || triggerScan) {
9090             Trace.beginSection("MP.snapshot");
9091             final LocalCallingIdentity token = clearLocalCallingIdentity();
9092             try (Cursor c = qb.query(helper, new String[] { FileColumns._ID },
9093                     userWhere, userWhereArgs, null, null, null, null, null)) {
9094                 while (c.moveToNext()) {
9095                     updatedIds.add(c.getLong(0));
9096                 }
9097             } finally {
9098                 restoreLocalCallingIdentity(token);
9099                 Trace.endSection();
9100             }
9101         }
9102 
9103         final ContentValues values = new ContentValues(initialValues);
9104         switch (match) {
9105             case AUDIO_MEDIA_ID:
9106             case AUDIO_PLAYLISTS_ID:
9107             case VIDEO_MEDIA_ID:
9108             case IMAGES_MEDIA_ID:
9109             case FILES_ID:
9110             case DOWNLOADS_ID: {
9111                 FileUtils.computeValuesFromData(values, isFuseThread());
9112                 break;
9113             }
9114         }
9115 
9116         if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) {
9117             final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE);
9118             switch (mediaType) {
9119                 case FileColumns.MEDIA_TYPE_AUDIO: {
9120                     computeAudioLocalizedValues(values);
9121                     computeAudioKeyValues(values);
9122                     break;
9123                 }
9124             }
9125         }
9126 
9127         boolean deferScan = false;
9128         if (triggerScan) {
9129             if (SdkLevel.isAtLeastS() &&
9130                     CompatChanges.isChangeEnabled(ENABLE_DEFERRED_SCAN, Binder.getCallingUid())) {
9131                 if (extras.containsKey(QUERY_ARG_DO_ASYNC_SCAN)) {
9132                     throw new IllegalArgumentException("Unsupported argument " +
9133                             QUERY_ARG_DO_ASYNC_SCAN + " used in extras");
9134                 }
9135                 deferScan = extras.getBoolean(QUERY_ARG_DEFER_SCAN, false);
9136                 if (deferScan && initialValues.containsKey(MediaColumns.IS_PENDING) &&
9137                         (initialValues.getAsInteger(MediaColumns.IS_PENDING) == 1)) {
9138                     // if the scan runs in async, ensure that the database row is excluded in
9139                     // default query until the metadata is updated by deferred scan.
9140                     // Apps will still be able to see this database row when queried with
9141                     // QUERY_ARG_MATCH_PENDING=MATCH_INCLUDE
9142                     values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR_PENDING_METADATA);
9143                     qb.allowColumn(FileColumns._MODIFIER);
9144                 }
9145             } else {
9146                 // Allow apps to use QUERY_ARG_DO_ASYNC_SCAN if the device is R or app is targeting
9147                 // targetSDK<=R.
9148                 deferScan = extras.getBoolean(QUERY_ARG_DO_ASYNC_SCAN, false);
9149             }
9150         }
9151 
9152         count = updateAllowingReplace(qb, helper, values, userWhere, userWhereArgs);
9153 
9154         // If the caller tried (and failed) to update metadata, the file on disk
9155         // might have changed, to scan it to collect the latest metadata.
9156         if (triggerInvalidate || triggerScan) {
9157             Trace.beginSection("MP.invalidate");
9158             final LocalCallingIdentity token = clearLocalCallingIdentity();
9159             try {
9160                 for (int i = 0; i < updatedIds.size(); i++) {
9161                     final long updatedId = updatedIds.get(i);
9162                     final Uri updatedUri = Files.getContentUri(volumeName, updatedId);
9163                     helper.postBackground(() -> {
9164                         invalidateThumbnails(updatedUri);
9165                     });
9166 
9167                     if (triggerScan) {
9168                         try (Cursor c = queryForSingleItem(updatedUri,
9169                                 new String[] { FileColumns.DATA }, null, null, null)) {
9170                             final File file = new File(c.getString(0));
9171                             final boolean notifyTranscodeHelper = isUriPublished;
9172                             if (deferScan) {
9173                                 helper.postBackground(() -> {
9174                                     scanFileAsMediaProvider(file);
9175                                     if (notifyTranscodeHelper) {
9176                                         notifyTranscodeHelperOnUriPublished(updatedUri, file);
9177                                     }
9178                                 });
9179                             } else {
9180                                 helper.postBlocking(() -> {
9181                                     scanFileAsMediaProvider(file);
9182                                     if (notifyTranscodeHelper) {
9183                                         notifyTranscodeHelperOnUriPublished(updatedUri, file);
9184                                     }
9185                                 });
9186                             }
9187                         } catch (Exception e) {
9188                             Log.w(TAG, "Failed to update metadata for " + updatedUri, e);
9189                         }
9190                     }
9191                 }
9192             } finally {
9193                 restoreLocalCallingIdentity(token);
9194                 Trace.endSection();
9195             }
9196         }
9197 
9198         return count;
9199     }
9200 
9201     private boolean isUpdateAllowedForOwnedPath(@Nullable String srcOwner,
9202             @Nullable String destOwner, @NonNull String srcPath, @NonNull String destPath) {
9203         // 1. Allow if the update is within owned path
9204         // update() from /sdcard/Android/media/com.foo/ABC/image.jpeg to
9205         // /sdcard/Android/media/com.foo/XYZ/image.jpeg - Allowed
9206         if(Objects.equals(srcOwner, destOwner)) {
9207             return true;
9208         }
9209 
9210         // 2. Check if the calling package is a special app which has global access
9211         if (isCallingPackageManager() || (canSystemGalleryAccessTheFile(srcPath) &&
9212             (canSystemGalleryAccessTheFile(destPath)))) {
9213             return true;
9214         }
9215 
9216         // 3. Allow update from srcPath if the source is not a owned path or calling package is the
9217         // owner of the source path or calling package shares the UID with the owner of the source
9218         // path
9219         // update() from /sdcard/DCIM/Foo.jpeg - Allowed
9220         // update() from /sdcard/Android/media/com.foo/image.jpeg - Allowed for
9221         // callingPackage=com.foo, not allowed for callingPackage=com.bar
9222         final boolean isSrcUpdateAllowed = srcOwner == null
9223                 || isCallingIdentitySharedPackageName(srcOwner);
9224 
9225         // 4. Allow update to dstPath if the destination is not a owned path or calling package is
9226         // the owner of the destination path or calling package shares the UID with the owner of the
9227         // destination path
9228         // update() to /sdcard/Pictures/image.jpeg - Allowed
9229         // update() to /sdcard/Android/media/com.foo/image.jpeg - Allowed for
9230         // callingPackage=com.foo, not allowed for callingPackage=com.bar
9231         final boolean isDestUpdateAllowed = destOwner == null
9232                 || isCallingIdentitySharedPackageName(destOwner);
9233 
9234         return isSrcUpdateAllowed && isDestUpdateAllowed;
9235     }
9236 
9237     private void notifyTranscodeHelperOnUriPublished(Uri uri, File file) {
9238         if (!mTranscodeHelper.supportsTranscode(file.getPath())) {
9239             return;
9240         }
9241 
9242         BackgroundThread.getExecutor().execute(() -> {
9243             final LocalCallingIdentity token = clearLocalCallingIdentity();
9244             try {
9245                 mTranscodeHelper.onUriPublished(uri);
9246             } finally {
9247                 restoreLocalCallingIdentity(token);
9248             }
9249         });
9250     }
9251 
9252     private void notifyTranscodeHelperOnFileOpen(String path, String ioPath, int uid,
9253             int transformsReason) {
9254         if (!mTranscodeHelper.supportsTranscode(path)) {
9255             return;
9256         }
9257 
9258         BackgroundThread.getExecutor().execute(() -> {
9259             final LocalCallingIdentity token = clearLocalCallingIdentity();
9260             try {
9261                 mTranscodeHelper.onFileOpen(path, ioPath, uid, transformsReason);
9262             } finally {
9263                 restoreLocalCallingIdentity(token);
9264             }
9265         });
9266     }
9267 
9268     /**
9269      * Update row(s) that match {@code userWhere} in MediaProvider database with {@code values}.
9270      * Treats update as replace for updates with conflicts.
9271      */
9272     private int updateAllowingReplace(@NonNull SQLiteQueryBuilder qb,
9273             @NonNull DatabaseHelper helper, @NonNull ContentValues values, String userWhere,
9274             String[] userWhereArgs) throws SQLiteConstraintException {
9275         return helper.runWithTransaction((db) -> {
9276             try {
9277                 return qb.update(helper, values, userWhere, userWhereArgs);
9278             } catch (SQLiteConstraintException e) {
9279                 // b/155320967 Apps sometimes create a file via file path and then update another
9280                 // explicitly inserted db row to this file. We have to resolve this update with a
9281                 // replace.
9282 
9283                 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
9284                     // We don't support replace for non-legacy apps. Non legacy apps should have
9285                     // clearer interactions with MediaProvider.
9286                     throw e;
9287                 }
9288 
9289                 final String path = values.getAsString(FileColumns.DATA);
9290 
9291                 // We will only handle UNIQUE constraint error for FileColumns.DATA. We will not try
9292                 // update and replace if no file exists for conflicting db row.
9293                 if (path == null || !new File(path).exists()) {
9294                     throw e;
9295                 }
9296 
9297                 final Uri uri = FileUtils.getContentUriForPath(path);
9298                 final boolean allowHidden = isCallingPackageAllowedHidden();
9299                 // The db row which caused UNIQUE constraint error may not match all column values
9300                 // of the given queryBuilder, hence using a generic queryBuilder with Files uri.
9301                 Bundle extras = new Bundle();
9302                 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE);
9303                 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE);
9304                 final SQLiteQueryBuilder qbForReplace = getQueryBuilder(TYPE_DELETE,
9305                         matchUri(uri, allowHidden), uri, extras, null);
9306                 final long rowId = getIdIfPathOwnedByPackages(qbForReplace, helper, path,
9307                         mCallingIdentity.get().getSharedPackagesAsString());
9308 
9309                 if (rowId != -1 && qbForReplace.delete(helper, "_id=?",
9310                         new String[] {Long.toString(rowId)}) == 1) {
9311                     Log.i(TAG, "Retrying database update after deleting conflicting entry");
9312                     return qb.update(helper, values, userWhere, userWhereArgs);
9313                 }
9314                 // Rethrow SQLiteConstraintException if app doesn't own the conflicting db row.
9315                 throw e;
9316             }
9317         });
9318     }
9319 
9320     /**
9321      * Update the internal table of {@link MediaStore.Audio.Playlists.Members}
9322      * by parsing the playlist file on disk and resolving it against scanned
9323      * audio items.
9324      * <p>
9325      * When a playlist references a missing audio item, the associated
9326      * {@link Playlists.Members#PLAY_ORDER} is skipped, leaving a gap to ensure
9327      * that the playlist entry is retained to avoid user data loss.
9328      */
9329     private void resolvePlaylistMembers(@NonNull Uri playlistUri) {
9330         Trace.beginSection("MP.resolvePlaylistMembers");
9331         try {
9332             final DatabaseHelper helper;
9333             try {
9334                 helper = getDatabaseForUri(playlistUri);
9335             } catch (VolumeNotFoundException e) {
9336                 throw e.rethrowAsIllegalArgumentException();
9337             }
9338 
9339             helper.runWithTransaction((db) -> {
9340                 resolvePlaylistMembersInternal(playlistUri, db);
9341                 return null;
9342             });
9343         } finally {
9344             Trace.endSection();
9345         }
9346     }
9347 
9348     private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri,
9349             @NonNull SQLiteDatabase db) {
9350         try {
9351             // Refresh playlist members based on what we parse from disk
9352             final long playlistId = ContentUris.parseId(playlistUri);
9353             final Map<String, Long> membersMap = getAllPlaylistMembers(playlistId);
9354             db.delete("audio_playlists_map", "playlist_id=" + playlistId, null);
9355 
9356             final Path playlistPath = queryForDataFile(playlistUri, null).toPath();
9357             final Playlist playlist = new Playlist();
9358             playlist.read(playlistPath.toFile());
9359 
9360             final List<Path> members = playlist.asList();
9361             for (int i = 0; i < members.size(); i++) {
9362                 try {
9363                     final Path audioPath = playlistPath.getParent().resolve(members.get(i));
9364                     final long audioId = queryForPlaylistMember(audioPath, membersMap);
9365 
9366                     final ContentValues values = new ContentValues();
9367                     values.put(Playlists.Members.PLAY_ORDER, i + 1);
9368                     values.put(Playlists.Members.PLAYLIST_ID, playlistId);
9369                     values.put(Playlists.Members.AUDIO_ID, audioId);
9370                     db.insert("audio_playlists_map", null, values);
9371                 } catch (IOException e) {
9372                     Log.w(TAG, "Failed to resolve playlist member", e);
9373                 }
9374             }
9375         } catch (IOException e) {
9376             Log.w(TAG, "Failed to refresh playlist", e);
9377         }
9378     }
9379 
9380     private Map<String, Long> getAllPlaylistMembers(long playlistId) {
9381         final Map<String, Long> membersMap = new ArrayMap<>();
9382 
9383         final Uri uri = Playlists.Members.getContentUri(MediaStore.VOLUME_EXTERNAL, playlistId);
9384         final String[] projection = new String[] {
9385                 Playlists.Members.DATA,
9386                 Playlists.Members.AUDIO_ID
9387         };
9388         try (Cursor c = query(uri, projection, null, null)) {
9389             if (c == null) {
9390                 Log.e(TAG, "Cursor is null, failed to create cached playlist member info.");
9391                 return membersMap;
9392             }
9393             while (c.moveToNext()) {
9394                 membersMap.put(c.getString(0), c.getLong(1));
9395             }
9396         }
9397         return membersMap;
9398     }
9399 
9400     /**
9401      * Make two attempts to query this playlist member: first based on the exact
9402      * path, and if that fails, fall back to picking a single item matching the
9403      * display name. When there are multiple items with the same display name,
9404      * we can't resolve between them, and leave this member unresolved.
9405      */
9406     private long queryForPlaylistMember(@NonNull Path path, @NonNull Map<String, Long> membersMap)
9407             throws IOException {
9408         final String data = path.toFile().getCanonicalPath();
9409         if (membersMap.containsKey(data)) {
9410             return membersMap.get(data);
9411         }
9412         final Uri audioUri = Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
9413         try (Cursor c = queryForSingleItem(audioUri,
9414                 new String[] { BaseColumns._ID }, MediaColumns.DATA + "=?",
9415                 new String[] { data }, null)) {
9416             return c.getLong(0);
9417         } catch (FileNotFoundException ignored) {
9418         }
9419         try (Cursor c = queryForSingleItem(audioUri,
9420                 new String[] { BaseColumns._ID }, MediaColumns.DISPLAY_NAME + "=?",
9421                 new String[] { path.toFile().getName() }, null)) {
9422             return c.getLong(0);
9423         } catch (FileNotFoundException ignored) {
9424         }
9425         throw new FileNotFoundException();
9426     }
9427 
9428     /**
9429      * Add the given audio item to the given playlist. Defaults to adding at the
9430      * end of the playlist when no {@link Playlists.Members#PLAY_ORDER} is
9431      * defined.
9432      */
9433     private long addPlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values)
9434             throws FallbackException {
9435         final long audioId = values.getAsLong(Audio.Playlists.Members.AUDIO_ID);
9436         final String volumeName = MediaStore.VOLUME_INTERNAL.equals(getVolumeName(playlistUri))
9437                 ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL;
9438         final Uri audioUri = Audio.Media.getContentUri(volumeName, audioId);
9439 
9440         Integer playOrder = values.getAsInteger(Playlists.Members.PLAY_ORDER);
9441         playOrder = (playOrder != null) ? (playOrder - 1) : Integer.MAX_VALUE;
9442 
9443         try {
9444             final File playlistFile = queryForDataFile(playlistUri, null);
9445             final File audioFile = queryForDataFile(audioUri, null);
9446 
9447             final Playlist playlist = new Playlist();
9448             playlist.read(playlistFile);
9449             playOrder = playlist.add(playOrder,
9450                     playlistFile.toPath().getParent().relativize(audioFile.toPath()));
9451             playlist.write(playlistFile);
9452             invalidateFuseDentry(playlistFile);
9453 
9454             resolvePlaylistMembers(playlistUri);
9455 
9456             // Callers are interested in the actual ID we generated
9457             final Uri membersUri = Playlists.Members.getContentUri(volumeName,
9458                     ContentUris.parseId(playlistUri));
9459             try (Cursor c = query(membersUri, new String[] { BaseColumns._ID },
9460                     Playlists.Members.PLAY_ORDER + "=" + (playOrder + 1), null, null)) {
9461                 c.moveToFirst();
9462                 return c.getLong(0);
9463             }
9464         } catch (IOException e) {
9465             throw new FallbackException("Failed to update playlist", e,
9466                     android.os.Build.VERSION_CODES.R);
9467         }
9468     }
9469 
9470     private int addPlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues[] initialValues)
9471             throws FallbackException {
9472         final String volumeName = getVolumeName(playlistUri);
9473         final String audioVolumeName =
9474                 MediaStore.VOLUME_INTERNAL.equals(volumeName)
9475                         ? MediaStore.VOLUME_INTERNAL : MediaStore.VOLUME_EXTERNAL;
9476 
9477         try {
9478             final File playlistFile = queryForDataFile(playlistUri, null);
9479             final Playlist playlist = new Playlist();
9480             playlist.read(playlistFile);
9481 
9482             for (ContentValues values : initialValues) {
9483                 final long audioId = values.getAsLong(Audio.Playlists.Members.AUDIO_ID);
9484                 final Uri audioUri = Audio.Media.getContentUri(audioVolumeName, audioId);
9485                 final File audioFile = queryForDataFile(audioUri, null);
9486 
9487                 Integer playOrder = values.getAsInteger(Playlists.Members.PLAY_ORDER);
9488                 playOrder = (playOrder != null) ? (playOrder - 1) : Integer.MAX_VALUE;
9489                 playlist.add(playOrder,
9490                         playlistFile.toPath().getParent().relativize(audioFile.toPath()));
9491             }
9492             playlist.write(playlistFile);
9493 
9494             resolvePlaylistMembers(playlistUri);
9495         } catch (IOException e) {
9496             throw new FallbackException("Failed to update playlist", e,
9497                     android.os.Build.VERSION_CODES.R);
9498         }
9499 
9500         return initialValues.length;
9501     }
9502 
9503     /**
9504      * Move an audio item within the given playlist.
9505      */
9506     private int movePlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values,
9507             @NonNull Bundle queryArgs) throws FallbackException {
9508         final int fromIndex = resolvePlaylistIndex(playlistUri, queryArgs);
9509         final int toIndex = values.getAsInteger(Playlists.Members.PLAY_ORDER) - 1;
9510         if (fromIndex == -1) {
9511             throw new FallbackException("Failed to resolve playlist member " + queryArgs,
9512                     android.os.Build.VERSION_CODES.R);
9513         }
9514         try {
9515             final File playlistFile = queryForDataFile(playlistUri, null);
9516 
9517             final Playlist playlist = new Playlist();
9518             playlist.read(playlistFile);
9519             final int finalIndex = playlist.move(fromIndex, toIndex);
9520             playlist.write(playlistFile);
9521             invalidateFuseDentry(playlistFile);
9522 
9523             resolvePlaylistMembers(playlistUri);
9524             return finalIndex;
9525         } catch (IOException e) {
9526             throw new FallbackException("Failed to update playlist", e,
9527                     android.os.Build.VERSION_CODES.R);
9528         }
9529     }
9530 
9531     /**
9532      * Removes an audio item or multiple audio items(if targetSDK<R) from the given playlist.
9533      */
9534     private int removePlaylistMembers(@NonNull Uri playlistUri, @NonNull Bundle queryArgs)
9535             throws FallbackException {
9536         final int[] indexes = resolvePlaylistIndexes(playlistUri, queryArgs);
9537         try {
9538             final File playlistFile = queryForDataFile(playlistUri, null);
9539 
9540             final Playlist playlist = new Playlist();
9541             playlist.read(playlistFile);
9542             final int count;
9543             if (indexes.length == 0) {
9544                 // This means either no playlist members match the query or VolumeNotFoundException
9545                 // was thrown. So we don't have anything to delete.
9546                 count = 0;
9547             } else {
9548                 count = playlist.removeMultiple(indexes);
9549             }
9550             playlist.write(playlistFile);
9551             invalidateFuseDentry(playlistFile);
9552 
9553             resolvePlaylistMembers(playlistUri);
9554             return count;
9555         } catch (IOException e) {
9556             throw new FallbackException("Failed to update playlist", e,
9557                     android.os.Build.VERSION_CODES.R);
9558         }
9559     }
9560 
9561     /**
9562      * Remove an audio item from the given playlist since the playlist file or the audio file is
9563      * already removed.
9564      */
9565     private void removePlaylistMembers(int mediaType, long id) {
9566         final DatabaseHelper helper;
9567         try {
9568             helper = getDatabaseForUri(Audio.Media.EXTERNAL_CONTENT_URI);
9569         } catch (VolumeNotFoundException e) {
9570             Log.w(TAG, e);
9571             return;
9572         }
9573 
9574         helper.runWithTransaction((db) -> {
9575             final String where;
9576             if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
9577                 where = "playlist_id=?";
9578             } else {
9579                 where = "audio_id=?";
9580             }
9581             db.delete("audio_playlists_map", where, new String[] { "" + id });
9582             return null;
9583         });
9584     }
9585 
9586     /**
9587      * Resolve query arguments that are designed to select specific playlist
9588      * items using the playlist's {@link Playlists.Members#PLAY_ORDER}.
9589      *
9590      * @return an array of the indexes that match the query.
9591      */
9592     private int[] resolvePlaylistIndexes(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) {
9593         final Uri membersUri = Playlists.Members.getContentUri(
9594                 getVolumeName(playlistUri), ContentUris.parseId(playlistUri));
9595 
9596         final DatabaseHelper helper;
9597         final SQLiteQueryBuilder qb;
9598         try {
9599             helper = getDatabaseForUri(membersUri);
9600             qb = getQueryBuilder(TYPE_DELETE, AUDIO_PLAYLISTS_ID_MEMBERS,
9601                     membersUri, queryArgs, null);
9602         } catch (VolumeNotFoundException ignored) {
9603             return new int[0];
9604         }
9605 
9606         try (Cursor c = qb.query(helper,
9607                 new String[] { Playlists.Members.PLAY_ORDER }, queryArgs, null)) {
9608             if ((c.getCount() >= 1) && c.moveToFirst()) {
9609                 int size = c.getCount();
9610                 int[] res = new int[size];
9611                 for (int i = 0; i < size; ++i) {
9612                     res[i] = c.getInt(0) - 1;
9613                     c.moveToNext();
9614                 }
9615                 return res;
9616             } else {
9617                 // Cursor size is 0
9618                 return new int[0];
9619             }
9620         }
9621     }
9622 
9623     /**
9624      * Resolve query arguments that are designed to select a specific playlist
9625      * item using its {@link Playlists.Members#PLAY_ORDER}.
9626      *
9627      * @return if there's only 1 item that matches the query, returns its index. Returns -1
9628      * otherwise.
9629      */
9630     private int resolvePlaylistIndex(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) {
9631         int[] indexes = resolvePlaylistIndexes(playlistUri, queryArgs);
9632         if (indexes.length == 1) {
9633             return indexes[0];
9634         }
9635         return -1;
9636     }
9637 
9638     private boolean isPickerUri(Uri uri) {
9639         final int match = matchUri(uri, /* allowHidden */ isCallingPackageAllowedHidden());
9640         return match == PICKER_ID || match == PICKER_GET_CONTENT_ID
9641                 || match == PICKER_TRANSCODED_ID;
9642     }
9643 
9644     @Override
9645     public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
9646         return openFileCommon(uri, mode, /*signal*/ null, /*opts*/ null);
9647     }
9648 
9649     @Override
9650     public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
9651             throws FileNotFoundException {
9652         return openFileCommon(uri, mode, signal, /*opts*/ null);
9653     }
9654 
9655     private ParcelFileDescriptor openFileCommon(Uri uri, String mode, CancellationSignal signal,
9656             @Nullable Bundle opts)
9657             throws FileNotFoundException {
9658         opts = opts == null ? new Bundle() : opts;
9659         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
9660         opts.remove(QUERY_ARG_REDACTED_URI);
9661         if (isRedactedUri(uri)) {
9662             opts.putParcelable(QUERY_ARG_REDACTED_URI, uri);
9663             uri = getUriForRedactedUri(uri);
9664         }
9665         uri = safeUncanonicalize(uri);
9666 
9667         if (isPickerUri(uri)) {
9668             int tid = Process.myTid();
9669             synchronized (mPendingOpenInfo) {
9670                 mPendingOpenInfo.put(tid, new PendingOpenInfo(
9671                         Binder.getCallingUid(), /* mediaCapabilitiesUid */ 0, /* shouldRedact */
9672                         false, /* transcodeReason */ 0));
9673             }
9674 
9675             try {
9676                 return mPickerUriResolver.openFile(uri, mode, signal, mCallingIdentity.get());
9677             } finally {
9678                 synchronized (mPendingOpenInfo) {
9679                     mPendingOpenInfo.remove(tid);
9680                 }
9681             }
9682         }
9683 
9684         final boolean allowHidden = isCallingPackageAllowedHidden();
9685         final int match = matchUri(uri, allowHidden);
9686         final String volumeName = getVolumeName(uri);
9687 
9688         // Handle some legacy cases where we need to redirect thumbnails
9689         try {
9690             switch (match) {
9691                 case AUDIO_ALBUMART_ID: {
9692                     final long albumId = Long.parseLong(uri.getPathSegments().get(3));
9693                     final Uri targetUri = ContentUris
9694                             .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId);
9695                     return ensureThumbnail(targetUri, signal);
9696                 }
9697                 case AUDIO_ALBUMART_FILE_ID: {
9698                     final long audioId = Long.parseLong(uri.getPathSegments().get(3));
9699                     final Uri targetUri = ContentUris
9700                             .withAppendedId(Audio.Media.getContentUri(volumeName), audioId);
9701                     return ensureThumbnail(targetUri, signal);
9702                 }
9703                 case VIDEO_MEDIA_ID_THUMBNAIL: {
9704                     final long videoId = Long.parseLong(uri.getPathSegments().get(3));
9705                     final Uri targetUri = ContentUris
9706                             .withAppendedId(Video.Media.getContentUri(volumeName), videoId);
9707                     return ensureThumbnail(targetUri, signal);
9708                 }
9709                 case IMAGES_MEDIA_ID_THUMBNAIL: {
9710                     final long imageId = Long.parseLong(uri.getPathSegments().get(3));
9711                     final Uri targetUri = ContentUris
9712                             .withAppendedId(Images.Media.getContentUri(volumeName), imageId);
9713                     return ensureThumbnail(targetUri, signal);
9714                 }
9715                 case CLI: {
9716                     // Command Line Interface "file" - content://media/cli - may be opened only by
9717                     // Shell (or Root).
9718                     if (!isCallingPackageShell()) {
9719                         throw new SecurityException("Only shell (or root) is allowed to open "
9720                                 + "MediaProvider's CLI file (" + uri + ')');
9721                     }
9722 
9723                     // We expect the uri's query to hold a single parameter - "cmd" - which contains
9724                     // the command name followed by the arguments (if any), all joined with '+'
9725                     // symbols:
9726                     // ?cmd=command[+arg1+[arg2+[arg3...]]]
9727                     //
9728                     // For example:
9729                     // (1) ?cmd=version
9730                     // (2) ?cmd=set-cloud-provider=com.my.cloud.provider.authority
9731                     //
9732                     // We retrieve the command name and the argument (if any) with
9733                     // uri.getQueryParameter("cmd") call, which will replace all '+' delimiters with
9734                     // spaces.
9735 
9736                     final String[] cmdAndArgs = uri.getQueryParameter("cmd").split("\\s+");
9737                     Log.d(TAG, "MediaProvider CLI command: " + Arrays.toString(cmdAndArgs));
9738                     try {
9739                         // Create a UNIX pipe.
9740                         final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
9741                         // Pass the write end - pipe[1] - to our shell command.
9742                         final var cmd = new MediaProviderShellCommand(getContext(),
9743                                 mConfigStore,
9744                                 mPickerSyncController,
9745                                 /* out */ pipe[1]);
9746                         cmd.exec(cmdAndArgs);
9747                         // Return the read end - pipe[0] - to the caller.
9748                         return pipe[0];
9749                     } catch (IOException e) {
9750                         Log.e(TAG, "Could not create a pipe", e);
9751                         return null;
9752                     }
9753                 }
9754             }
9755         } finally {
9756             // We have to log separately here because openFileAndEnforcePathPermissionsHelper calls
9757             // a public MediaProvider API and so logs the access there.
9758             PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
9759         }
9760 
9761         return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal, opts);
9762     }
9763 
9764     @Override
9765     public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
9766             throws FileNotFoundException {
9767         return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, null);
9768     }
9769 
9770     @Override
9771     public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
9772             CancellationSignal signal) throws FileNotFoundException {
9773         return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, signal);
9774     }
9775 
9776     private AssetFileDescriptor openTypedAssetFileCommon(Uri uri, String mimeTypeFilter,
9777             Bundle opts, CancellationSignal signal) throws FileNotFoundException {
9778         final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE)
9779                 && StringUtils.startsWithIgnoreCase(mimeTypeFilter, "image/");
9780         String mode = "r";
9781 
9782         // If request is not for thumbnail and arising from MediaProvider, then check for EXTRA_MODE
9783         if (opts != null && !wantsThumb && isCallingPackageSelf()) {
9784             mode = opts.getString(MediaStore.EXTRA_MODE, "r");
9785         } else if (opts != null) {
9786             opts.remove(MediaStore.EXTRA_MODE);
9787         }
9788 
9789         if (opts != null && opts.containsKey(MediaStore.EXTRA_FILE_DESCRIPTOR)) {
9790             // This is called as part of MediaStore#getOriginalMediaFormatFileDescriptor
9791             // We don't need to use the |uri| because the input fd already identifies the file and
9792             // we actually don't have a valid URI, we are going to identify the file via the fd.
9793             // While identifying the file, we also perform the following security checks.
9794             // 1. Find the FUSE file with the associated inode
9795             // 2. Verify that the binder caller opened it
9796             // 3. Verify the access level the fd is opened with (r/w)
9797             // 4. Open the original (non-transcoded) file *with* redaction enabled and the access
9798             // level from #3
9799             // 5. Return the fd from #4 to the app or throw an exception if any of the conditions
9800             // are not met
9801             try {
9802                 return getOriginalMediaFormatFileDescriptor(opts);
9803             } finally {
9804                 // Clearing the Bundle closes the underlying Parcel, ensuring that the input fd
9805                 // owned by the Parcel is closed immediately and not at the next GC.
9806                 // This works around a change in behavior introduced by:
9807                 // aosp/Icfe8880cad00c3cd2afcbe4b92400ad4579e680e
9808                 opts.clear();
9809             }
9810         }
9811 
9812         // This is needed for thumbnail resolution as it doesn't go through openFileCommon
9813         if (isPickerUri(uri)) {
9814             int tid = Process.myTid();
9815             synchronized (mPendingOpenInfo) {
9816                 mPendingOpenInfo.put(tid, new PendingOpenInfo(
9817                         Binder.getCallingUid(), /* mediaCapabilitiesUid */ 0, /* shouldRedact */
9818                         false, /* transcodeReason */ 0));
9819             }
9820 
9821             try {
9822                 return mPickerUriResolver.openTypedAssetFile(uri, mimeTypeFilter, opts, signal,
9823                         mCallingIdentity.get(), wantsThumb);
9824             } finally {
9825                 synchronized (mPendingOpenInfo) {
9826                     mPendingOpenInfo.remove(tid);
9827                 }
9828             }
9829         }
9830 
9831         // Offer thumbnail of media, when requested
9832         if (wantsThumb) {
9833             final ParcelFileDescriptor pfd = ensureThumbnail(uri, signal);
9834             return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
9835         }
9836 
9837         // Worst case, return the underlying file
9838         return new AssetFileDescriptor(openFileCommon(uri, mode, signal, opts), 0,
9839                 AssetFileDescriptor.UNKNOWN_LENGTH);
9840     }
9841 
9842     private ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
9843             throws FileNotFoundException {
9844         final boolean allowHidden = isCallingPackageAllowedHidden();
9845         final int match = matchUri(uri, allowHidden);
9846 
9847         Trace.beginSection("MP.ensureThumbnail");
9848         checkAccessForThumbnail(uri, match, signal);
9849         final LocalCallingIdentity token = clearLocalCallingIdentity();
9850         try {
9851             switch (match) {
9852                 case AUDIO_ALBUMS_ID: {
9853                     final String volumeName = MediaStore.getVolumeName(uri);
9854                     final Uri baseUri = MediaStore.Audio.Media.getContentUri(volumeName);
9855                     final long albumId = ContentUris.parseId(uri);
9856                     try (Cursor c = query(baseUri, new String[] { MediaStore.Audio.Media._ID },
9857                             MediaStore.Audio.Media.ALBUM_ID + "=" + albumId, null, null, signal)) {
9858                         if (c.moveToFirst()) {
9859                             final long audioId = c.getLong(0);
9860                             final Uri targetUri = ContentUris.withAppendedId(baseUri, audioId);
9861                             return mAudioThumbnailer.ensureThumbnail(targetUri, signal);
9862                         } else {
9863                             throw new FileNotFoundException("No media for album " + uri);
9864                         }
9865                     }
9866                 }
9867                 case AUDIO_MEDIA_ID:
9868                     return mAudioThumbnailer.ensureThumbnail(uri, signal);
9869                 case VIDEO_MEDIA_ID:
9870                     return mVideoThumbnailer.ensureThumbnail(uri, signal);
9871                 case IMAGES_MEDIA_ID:
9872                     return mImageThumbnailer.ensureThumbnail(uri, signal);
9873                 case FILES_ID:
9874                 case DOWNLOADS_ID: {
9875                     // When item is referenced in a generic way, resolve to actual type
9876                     final int mediaType = MimeUtils.resolveMediaType(getType(uri));
9877                     switch (mediaType) {
9878                         case FileColumns.MEDIA_TYPE_AUDIO:
9879                             return mAudioThumbnailer.ensureThumbnail(uri, signal);
9880                         case FileColumns.MEDIA_TYPE_VIDEO:
9881                             return mVideoThumbnailer.ensureThumbnail(uri, signal);
9882                         case FileColumns.MEDIA_TYPE_IMAGE:
9883                             return mImageThumbnailer.ensureThumbnail(uri, signal);
9884                         default:
9885                             throw new FileNotFoundException();
9886                     }
9887                 }
9888                 default:
9889                     throw new FileNotFoundException();
9890             }
9891         } catch (IOException e) {
9892             Log.w(TAG, e);
9893             throw new FileNotFoundException(e.getMessage());
9894         } finally {
9895             restoreLocalCallingIdentity(token);
9896             Trace.endSection();
9897         }
9898     }
9899 
9900     private void checkAccessForThumbnail(Uri uri, int match, CancellationSignal signal)
9901             throws FileNotFoundException {
9902         int mediaType = -1;
9903         if (match == DOWNLOADS_ID || match == FILES_ID) {
9904             mediaType = MimeUtils.resolveMediaType(queryForTypeAsSelf(uri));
9905         }
9906 
9907         // check access only for image and video thumbnails
9908         // audio thumbnails have many legacy paths that we could break by checking for access
9909         // and it doesn't reveal much of data that could be a risk
9910         if (match == IMAGES_MEDIA_ID || match == VIDEO_MEDIA_ID
9911                 || mediaType == MEDIA_TYPE_IMAGE || mediaType == MEDIA_TYPE_VIDEO) {
9912 
9913             // First check existence of the file
9914             final String[] projection = new String[] { MediaColumns.DATA };
9915             final File file;
9916             try (Cursor c = queryForSingleItemAsMediaProvider(
9917                     uri, projection, null, null, signal)) {
9918                 final String data = c.getString(0);
9919                 if (TextUtils.isEmpty(data)) {
9920                     throw new FileNotFoundException("Missing path for " + uri);
9921                 } else {
9922                     file = new File(data).getCanonicalFile();
9923                 }
9924             } catch (IOException e) {
9925                 throw new FileNotFoundException(e.toString());
9926             }
9927 
9928             // Then check if the caller has access to the file
9929             checkAccess(uri, Bundle.EMPTY, file, false);
9930         }
9931     }
9932 
9933     /**
9934      * Update the metadata columns for the image residing at given {@link Uri}
9935      * by reading data from the underlying image.
9936      */
9937     private void updateImageMetadata(ContentValues values, File file) {
9938         final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options();
9939         bitmapOpts.inJustDecodeBounds = true;
9940         BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOpts);
9941 
9942         values.put(MediaColumns.WIDTH, bitmapOpts.outWidth);
9943         values.put(MediaColumns.HEIGHT, bitmapOpts.outHeight);
9944     }
9945 
9946     private void handleInsertedRowForFuse(long rowId) {
9947         if (isFuseThread()) {
9948             // Removes restored row ID saved list.
9949             mCallingIdentity.get().removeDeletedRowId(rowId);
9950         }
9951     }
9952 
9953     private void handleUpdatedRowForFuse(@NonNull String oldPath, @NonNull String ownerPackage,
9954             long oldRowId, long newRowId) {
9955         if (oldRowId == newRowId) {
9956             // Update didn't delete or add row ID. We don't need to save row ID or remove saved
9957             // deleted ID.
9958             return;
9959         }
9960 
9961         handleDeletedRowForFuse(oldPath, ownerPackage, oldRowId);
9962         handleInsertedRowForFuse(newRowId);
9963     }
9964 
9965     private void handleDeletedRowForFuse(@NonNull String path, @NonNull String ownerPackage,
9966             long rowId) {
9967         if (!isFuseThread()) {
9968             return;
9969         }
9970 
9971         // Invalidate saved owned ID's of the previous owner of the deleted path, this prevents old
9972         // owner from gaining access to newly created file with restored row ID.
9973         if (!ownerPackage.equals("null") && !ownerPackage.equals(getCallingPackageOrSelf())) {
9974             invalidateLocalCallingIdentityCache(ownerPackage, "owned_database_row_deleted:"
9975                     + path);
9976         }
9977         // Saves row ID corresponding to deleted path. Saved row ID will be restored on subsequent
9978         // create or rename.
9979         mCallingIdentity.get().addDeletedRowId(path, rowId);
9980     }
9981 
9982     private void handleOwnerPackageNameChange(@NonNull String oldPath,
9983             @NonNull String oldOwnerPackage, @NonNull String newOwnerPackage) {
9984         if (Objects.equals(oldOwnerPackage, newOwnerPackage)) {
9985             return;
9986         }
9987         // Invalidate saved owned ID's of the previous owner of the renamed path, this prevents old
9988         // owner from gaining access to replaced file.
9989         invalidateLocalCallingIdentityCache(oldOwnerPackage, "owner_package_changed:" + oldPath);
9990     }
9991 
9992     /**
9993      * Return the {@link MediaColumns#DATA} field for the given {@code Uri}.
9994      */
9995     File queryForDataFile(Uri uri, CancellationSignal signal)
9996             throws FileNotFoundException {
9997         return queryForDataFile(uri, null, null, signal);
9998     }
9999 
10000     /**
10001      * Return the {@link MediaColumns#DATA} field for the given {@code Uri}.
10002      */
10003     File queryForDataFile(Uri uri, String selection, String[] selectionArgs,
10004             CancellationSignal signal) throws FileNotFoundException {
10005         try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns.DATA },
10006                 selection, selectionArgs, signal)) {
10007             final String data = cursor.getString(0);
10008             if (TextUtils.isEmpty(data)) {
10009                 throw new FileNotFoundException("Missing path for " + uri);
10010             } else {
10011                 return new File(data);
10012             }
10013         }
10014     }
10015 
10016     /**
10017      * Return the {@link Uri} for the given {@code File}.
10018      */
10019     Uri queryForMediaUri(File file, CancellationSignal signal) throws FileNotFoundException {
10020         final String volumeName = FileUtils.getVolumeName(getContext(), file);
10021         final Uri uri = Files.getContentUri(volumeName);
10022         try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns._ID },
10023                 MediaColumns.DATA + "=?", new String[] { file.getAbsolutePath() }, signal)) {
10024             return ContentUris.withAppendedId(uri, cursor.getLong(0));
10025         }
10026     }
10027 
10028     /**
10029      * Query the given {@link Uri} as MediaProvider, expecting only a single item to be found.
10030      *
10031      * @throws FileNotFoundException if no items were found, or multiple items
10032      *             were found, or there was trouble reading the data.
10033      */
10034     Cursor queryForSingleItemAsMediaProvider(Uri uri, String[] projection, String selection,
10035             String[] selectionArgs, CancellationSignal signal)
10036             throws FileNotFoundException {
10037         final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
10038         try {
10039             return queryForSingleItem(uri, projection, selection, selectionArgs, signal);
10040         } finally {
10041             restoreLocalCallingIdentity(tokenInner);
10042         }
10043     }
10044 
10045     /**
10046      * Query the given {@link Uri}, expecting only a single item to be found.
10047      *
10048      * @throws FileNotFoundException if no items were found, or multiple items
10049      *             were found, or there was trouble reading the data.
10050      */
10051     Cursor queryForSingleItem(Uri uri, String[] projection, String selection,
10052             String[] selectionArgs, CancellationSignal signal) throws FileNotFoundException {
10053         Cursor c = null;
10054         try {
10055             c = query(uri, projection,
10056                     DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null),
10057                     signal, true);
10058         } catch (IllegalArgumentException  e) {
10059             throw new FileNotFoundException("Volume not found for " + uri);
10060         }
10061         if (c == null) {
10062             throw new FileNotFoundException("Missing cursor for " + uri);
10063         } else if (c.getCount() < 1) {
10064             FileUtils.closeQuietly(c);
10065             throw new FileNotFoundException("No item at " + uri);
10066         } else if (c.getCount() > 1) {
10067             FileUtils.closeQuietly(c);
10068             throw new FileNotFoundException("Multiple items at " + uri);
10069         }
10070 
10071         if (c.moveToFirst()) {
10072             return c;
10073         } else {
10074             FileUtils.closeQuietly(c);
10075             throw new FileNotFoundException("Failed to read row from " + uri);
10076         }
10077     }
10078 
10079     /**
10080      * Compares {@code itemOwner} with package name of {@link LocalCallingIdentity} and throws
10081      * {@link IllegalStateException} if it doesn't match.
10082      * Make sure to set calling identity properly before calling.
10083      */
10084     private void requireOwnershipForItem(@Nullable String itemOwner, Uri item) {
10085         final boolean hasOwner = (itemOwner != null);
10086         final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), itemOwner);
10087         if (hasOwner && !callerIsOwner) {
10088             throw new IllegalStateException(
10089                     "Only owner is able to interact with pending/trashed item " + item);
10090         }
10091     }
10092 
10093     private ParcelFileDescriptor openWithFuse(String filePath, int uid, int mediaCapabilitiesUid,
10094             int modeBits, boolean shouldRedact, boolean shouldTranscode, int transcodeReason)
10095             throws FileNotFoundException {
10096         Log.d(TAG, "Open with FUSE. FilePath: " + filePath
10097                 + ". Uid: " + uid
10098                 + ". Media Capabilities Uid: " + mediaCapabilitiesUid
10099                 + ". ShouldRedact: " + shouldRedact
10100                 + ". ShouldTranscode: " + shouldTranscode);
10101 
10102         int tid = android.os.Process.myTid();
10103         synchronized (mPendingOpenInfo) {
10104             mPendingOpenInfo.put(tid,
10105                     new PendingOpenInfo(uid, mediaCapabilitiesUid, shouldRedact, transcodeReason));
10106         }
10107 
10108         try {
10109             return FileUtils.openSafely(toFuseFile(new File(filePath)), modeBits);
10110         } finally {
10111             synchronized (mPendingOpenInfo) {
10112                 mPendingOpenInfo.remove(tid);
10113             }
10114         }
10115     }
10116 
10117     /**
10118      * @return {@link FuseDaemon} corresponding to a given file
10119      */
10120     @NonNull
10121     public static FuseDaemon getFuseDaemonForFile(@NonNull File file, VolumeCache volumeCache)
10122             throws FileNotFoundException {
10123         final FuseDaemon daemon = ExternalStorageServiceImpl.getFuseDaemon(
10124                 volumeCache.getVolumeId(file));
10125         if (daemon == null) {
10126             throw new FileNotFoundException("Missing FUSE daemon for " + file);
10127         } else {
10128             return daemon;
10129         }
10130     }
10131 
10132     @NonNull
10133     public static FuseDaemon getFuseDaemonForFileWithWait(@NonNull File file,
10134             VolumeCache volumeCache, long waitTimeInMilliseconds) throws FileNotFoundException {
10135         FuseDaemon fuseDaemon = null;
10136         long time = 0;
10137         while (time < waitTimeInMilliseconds) {
10138             fuseDaemon = ExternalStorageServiceImpl.getFuseDaemon(
10139                     volumeCache.getVolumeId(file));
10140             if (fuseDaemon != null) {
10141                 break;
10142             }
10143             SystemClock.sleep(POLLING_TIME_IN_MILLIS);
10144             time += POLLING_TIME_IN_MILLIS;
10145         }
10146 
10147         if (fuseDaemon == null) {
10148             throw new FileNotFoundException("Missing FUSE daemon for " + file);
10149         } else {
10150             return fuseDaemon;
10151         }
10152     }
10153 
10154     /**
10155      * Invalidate fuse dentry cache for filepath
10156      */
10157     public void invalidateFuseDentry(@NonNull File file) {
10158         invalidateFuseDentry(file.getAbsolutePath());
10159     }
10160 
10161     private void invalidateFuseDentry(@NonNull String path) {
10162         try {
10163             final FuseDaemon daemon = getFuseDaemonForFile(new File(path), mVolumeCache);
10164             if (isFuseThread()) {
10165                 // If we are on a FUSE thread, we don't need to invalidate,
10166                 // (and *must* not, otherwise we'd crash) because the invalidation
10167                 // is already reflected in the lower filesystem
10168                 return;
10169             } else {
10170                 daemon.invalidateFuseDentryCache(path);
10171             }
10172         } catch (FileNotFoundException e) {
10173             Log.w(TAG, "Failed to invalidate FUSE dentry", e);
10174         }
10175     }
10176 
10177     /**
10178      * Replacement for {@link #openFileHelper(Uri, String)} which enforces any
10179      * permissions applicable to the path before returning.
10180      *
10181      * <p>This function should never be called from the fuse thread since it tries to open
10182      * a "/mnt/user" path.
10183      */
10184     private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match,
10185             String mode, CancellationSignal signal, @NonNull Bundle opts)
10186             throws FileNotFoundException {
10187         int modeBits = ParcelFileDescriptor.parseMode(mode);
10188         boolean forWrite = (modeBits & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0;
10189         final Uri redactedUri = opts.getParcelable(QUERY_ARG_REDACTED_URI);
10190         if (forWrite) {
10191             if (redactedUri != null) {
10192                 throw new UnsupportedOperationException(
10193                         "Write is not supported on " + redactedUri.toString());
10194             }
10195             // Upgrade 'w' only to 'rw'. This allows us acquire a WR_LOCK when calling
10196             // #shouldOpenWithFuse
10197             modeBits |= ParcelFileDescriptor.MODE_READ_WRITE;
10198         }
10199 
10200         final boolean hasOwnerPackageName = hasOwnerPackageName(uri);
10201         final String[] projection = new String[] {
10202                 MediaColumns.DATA,
10203                 hasOwnerPackageName ? MediaColumns.OWNER_PACKAGE_NAME : "NULL",
10204                 hasOwnerPackageName ? MediaColumns.IS_PENDING : "0",
10205         };
10206 
10207         final File file;
10208         final String ownerPackageName;
10209         final boolean isPending;
10210         final LocalCallingIdentity token = clearLocalCallingIdentity();
10211         try (Cursor c = queryForSingleItem(uri, projection, null, null, signal)) {
10212             final String data = c.getString(0);
10213             if (TextUtils.isEmpty(data)) {
10214                 throw new FileNotFoundException("Missing path for " + uri);
10215             } else {
10216                 file = new File(data).getCanonicalFile();
10217             }
10218             ownerPackageName = c.getString(1);
10219             isPending = c.getInt(2) != 0;
10220         } catch (IOException e) {
10221             throw new FileNotFoundException(e.toString());
10222         } finally {
10223             restoreLocalCallingIdentity(token);
10224         }
10225 
10226         if (redactedUri == null) {
10227             checkAccess(uri, Bundle.EMPTY, file, forWrite);
10228         } else {
10229             checkAccess(redactedUri, Bundle.EMPTY, file, false);
10230         }
10231 
10232         // We don't check ownership for files with IS_PENDING set by FUSE
10233         if (isPending && !isPendingFromFuse(file)) {
10234             requireOwnershipForItem(ownerPackageName, uri);
10235         }
10236 
10237         // Figure out if we need to redact contents
10238         final boolean redactionNeeded = isRedactionNeededForOpenViaContentResolver(redactedUri,
10239                 ownerPackageName, file);
10240         long[] redactionRanges;
10241         try {
10242             redactionRanges = redactionNeeded ? RedactionUtils.getRedactionRanges(file)
10243                     : new long[0];
10244         } catch (IOException e) {
10245             throw new IllegalStateException(e);
10246         }
10247 
10248         // Yell if caller requires original, since we can't give it to them
10249         // unless they have access granted above
10250         if (redactionNeeded && MediaStore.getRequireOriginal(uri)) {
10251             throw new UnsupportedOperationException(
10252                     "Caller must hold ACCESS_MEDIA_LOCATION permission to access original");
10253         }
10254 
10255         // Kick off metadata update when writing is finished
10256         final OnCloseListener listener = (e) -> {
10257             // We always update metadata to reflect the state on disk, even when
10258             // the remote writer tried claiming an exception
10259             invalidateThumbnails(uri);
10260 
10261             // Invalidate so subsequent stat(2) on the upper fs is eventually consistent
10262             invalidateFuseDentry(file);
10263             try {
10264                 switch (match) {
10265                     case IMAGES_THUMBNAILS_ID:
10266                     case VIDEO_THUMBNAILS_ID:
10267                         final ContentValues values = new ContentValues();
10268                         updateImageMetadata(values, file);
10269                         update(uri, values, null, null);
10270                         break;
10271                     default:
10272                         scanFileAsMediaProvider(file);
10273                         break;
10274                 }
10275             } catch (Exception e2) {
10276                 Log.w(TAG, "Failed to update metadata for " + uri, e2);
10277             }
10278         };
10279 
10280         try {
10281             // First, handle any redaction that is needed for caller
10282             final ParcelFileDescriptor pfd;
10283             final String filePath = file.getPath();
10284             final int uid = Binder.getCallingUid();
10285             final int transcodeReason = mTranscodeHelper.shouldTranscode(filePath, uid, opts);
10286             final boolean shouldTranscode = transcodeReason > 0;
10287             int mediaCapabilitiesUid = opts.getInt(MediaStore.EXTRA_MEDIA_CAPABILITIES_UID);
10288             if (!shouldTranscode || mediaCapabilitiesUid < Process.FIRST_APPLICATION_UID) {
10289                 // Although 0 is a valid UID, it's not a valid app uid.
10290                 // So, we use it to signify that mediaCapabilitiesUid is not set.
10291                 mediaCapabilitiesUid = 0;
10292             }
10293             if (redactionRanges.length > 0) {
10294                 // If fuse is enabled, we can provide an fd that points to the fuse
10295                 // file system and handle redaction in the fuse handler when the caller reads.
10296                 pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits,
10297                         true /* shouldRedact */, shouldTranscode, transcodeReason);
10298             } else if (shouldTranscode) {
10299                 pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits,
10300                         false /* shouldRedact */, shouldTranscode, transcodeReason);
10301             } else {
10302                 FuseDaemon daemon = null;
10303                 try {
10304                     daemon = getFuseDaemonForFile(file, mVolumeCache);
10305                 } catch (FileNotFoundException ignored) {
10306                 }
10307                 ParcelFileDescriptor lowerFsFd = FileUtils.openSafely(file, modeBits);
10308                 // Always acquire a readLock. This allows us make multiple opens via lower
10309                 // filesystem
10310                 boolean shouldOpenWithFuse = daemon != null
10311                         && daemon.shouldOpenWithFuse(filePath, true /* forRead */,
10312                         lowerFsFd.getFd());
10313 
10314                 if (shouldOpenWithFuse) {
10315                     // If the file is already opened on the FUSE mount with VFS caching enabled
10316                     // we return an upper filesystem fd (via FUSE) to avoid file corruption
10317                     // resulting from cache inconsistencies between the upper and lower
10318                     // filesystem caches
10319                     pfd = openWithFuse(filePath, uid, mediaCapabilitiesUid, modeBits,
10320                             false /* shouldRedact */, shouldTranscode, transcodeReason);
10321                     try {
10322                         lowerFsFd.close();
10323                     } catch (IOException e) {
10324                         Log.w(TAG, "Failed to close lower filesystem fd " + file.getPath(), e);
10325                     }
10326                 } else {
10327                     Log.i(TAG, "Open with lower FS for " + filePath + ". Uid: " + uid);
10328                     if (forWrite) {
10329                         // When opening for write on the lower filesystem, invalidate the VFS dentry
10330                         // so subsequent open/getattr calls will return correctly.
10331                         //
10332                         // A 'dirty' dentry with write back cache enabled can cause the kernel to
10333                         // ignore file attributes or even see stale page cache data when the lower
10334                         // filesystem has been modified outside of the FUSE driver
10335                         invalidateFuseDentry(file);
10336                     }
10337 
10338                     pfd = lowerFsFd;
10339                 }
10340             }
10341 
10342             // Second, wrap in any listener that we've requested
10343             if (!isPending && forWrite) {
10344                 return ParcelFileDescriptor.wrap(pfd, BackgroundThread.getHandler(), listener);
10345             } else {
10346                 return pfd;
10347             }
10348         } catch (IOException e) {
10349             if (e instanceof FileNotFoundException) {
10350                 throw (FileNotFoundException) e;
10351             } else {
10352                 throw new IllegalStateException(e);
10353             }
10354         }
10355     }
10356 
10357     private boolean isRedactionNeededForOpenViaContentResolver(Uri redactedUri,
10358             String ownerPackageName, File file) {
10359         // Redacted Uris should always redact information
10360         if (redactedUri != null) {
10361             return true;
10362         }
10363 
10364         final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName);
10365         if (callerIsOwner) {
10366             return false;
10367         }
10368 
10369         // To be consistent with FUSE redaction checks we allow similar access for File Manager
10370         // and System Gallery apps.
10371         if (isCallingPackageManager() || canSystemGalleryAccessTheFile(file.getPath())) {
10372             return false;
10373         }
10374 
10375         return isRedactionNeeded();
10376     }
10377 
10378     private void deleteAndInvalidate(@NonNull Path path) {
10379         if (path == null) {
10380             return;
10381         }
10382 
10383         String fileName = path.getFileName().toString();
10384         // Delete and invalidate all files except .nomedia and .database_uuid
10385         if (!fileName.equalsIgnoreCase(MEDIA_IGNORE_FILENAME)
10386                 && !fileName.equalsIgnoreCase(FILE_DATABASE_UUID)) {
10387             deleteAndInvalidate(path.toFile());
10388         }
10389     }
10390 
10391     private void deleteAndInvalidate(@NonNull File file) {
10392         file.delete();
10393         invalidateFuseDentry(file);
10394     }
10395 
10396     private void deleteIfAllowed(Uri uri, Bundle extras, String path) {
10397         try {
10398             final File file = new File(path).getCanonicalFile();
10399             checkAccess(uri, extras, file, true);
10400             deleteAndInvalidate(file);
10401         } catch (Exception e) {
10402             Log.e(TAG, "Couldn't delete " + path, e);
10403         }
10404     }
10405 
10406     @Deprecated
10407     private boolean isPending(Uri uri) {
10408         final int match = matchUri(uri, true);
10409         switch (match) {
10410             case AUDIO_MEDIA_ID:
10411             case VIDEO_MEDIA_ID:
10412             case IMAGES_MEDIA_ID:
10413                 try (Cursor c = queryForSingleItem(uri,
10414                         new String[] { MediaColumns.IS_PENDING }, null, null, null)) {
10415                     return (c.getInt(0) != 0);
10416                 } catch (FileNotFoundException e) {
10417                     throw new IllegalStateException(e);
10418                 }
10419             default:
10420                 return false;
10421         }
10422     }
10423 
10424     @Deprecated
10425     private boolean isRedactionNeeded(Uri uri) {
10426         return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED);
10427     }
10428 
10429     private boolean isRedactionNeeded() {
10430         return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED);
10431     }
10432 
10433     private boolean isCallingPackageRequestingLegacy() {
10434         return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_GRANTED);
10435     }
10436 
10437     private boolean shouldBypassDatabase(int uid) {
10438         if (uid != android.os.Process.SHELL_UID && isCallingPackageManager()) {
10439             return mCallingIdentity.get().shouldBypassDatabase(false /*isSystemGallery*/);
10440         } else if (isCallingPackageSystemGallery()) {
10441             if (isCallingPackageLegacyWrite()) {
10442                 // We bypass db operations for legacy system galleries with W_E_S (see b/167307393).
10443                 // Tracking a longer term solution in b/168784136.
10444                 return true;
10445             } else if (!SdkLevel.isAtLeastS()) {
10446                 // We don't parse manifest flags for SdkLevel<=R yet. Hence, we don't bypass
10447                 // database updates for SystemGallery targeting R or above on R OS.
10448                 return false;
10449             }
10450             return mCallingIdentity.get().shouldBypassDatabase(true /*isSystemGallery*/);
10451         }
10452         return false;
10453     }
10454 
10455     private static int getFileMediaType(String path) {
10456         final File file = new File(path);
10457         final String mimeType = MimeUtils.resolveMimeType(file);
10458         return MimeUtils.resolveMediaType(mimeType);
10459     }
10460 
10461     private boolean canSystemGalleryAccessTheFile(String filePath) {
10462 
10463         if (!isCallingPackageSystemGallery()) {
10464             return false;
10465         }
10466 
10467         final int mediaType = getFileMediaType(filePath);
10468 
10469         return mediaType ==  FileColumns.MEDIA_TYPE_IMAGE ||
10470             mediaType ==  FileColumns.MEDIA_TYPE_VIDEO;
10471     }
10472 
10473     /**
10474      * Returns true if:
10475      * <ul>
10476      * <li>the calling identity is an app targeting Q or older versions AND is requesting legacy
10477      * storage and has the corresponding legacy access (read/write) permissions
10478      * <li>the calling identity holds {@code MANAGE_EXTERNAL_STORAGE}
10479      * <li>the calling identity owns or has access to the filePath (eg /Android/data/com.foo)
10480      * <li>the calling identity has permission to write images and the given file is an image file
10481      * <li>the calling identity has permission to write video and the given file is an video file
10482      * </ul>
10483      */
10484     private boolean shouldBypassFuseRestrictions(boolean forWrite, String filePath) {
10485         boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite()
10486                 : isCallingPackageLegacyRead();
10487         if (isRequestingLegacyStorage) {
10488             return true;
10489         }
10490 
10491         if (isCallingPackageManager()) {
10492             return true;
10493         }
10494 
10495         // Check if the caller has access to private app directories. Checks for Android/data,
10496         // Android/media and Android/obb
10497         boolean isUidAllowedAccessToDataOrObbPath =
10498                 isUidAllowedAccessToDataOrObbPathForFuse(mCallingIdentity.get().uid, filePath);
10499 
10500         /*
10501          * If owned photos is enabled, then image or video stored in app's private directory may
10502          * not have access to it (i.e, have owner_package_name as null). So, only checking path is
10503          * not enough to bypass fuse restrictions. We will have to additionally check if calling
10504          * app has read permission.
10505          */
10506         if (isUidAllowedAccessToDataOrObbPath) {
10507             if (!isExternalMediaDirectory(filePath)) {
10508                 return true;
10509             }
10510 
10511             if (!isOwnedPhotosEnabled(mCallingIdentity.get().uid)) {
10512                 return true;
10513             }
10514 
10515             int mediaType = getFileMediaType(filePath);
10516 
10517             if (MEDIA_TYPE_IMAGE == mediaType) {
10518                 boolean hasReadImagesPermission =
10519                         mCallingIdentity.get().hasPermission(PERMISSION_READ_IMAGES);
10520                 Log.v(TAG, "calling app has PERMISSION_READ_IMAGES? "
10521                         + hasReadImagesPermission);
10522                 return hasReadImagesPermission;
10523             }
10524 
10525             if (MEDIA_TYPE_VIDEO == mediaType) {
10526                 boolean hasReadVideoPermission =
10527                         mCallingIdentity.get().hasPermission(PERMISSION_READ_VIDEO);
10528                 Log.v(TAG, "calling app has PERMISSION_READ_VIDEO? "
10529                         + hasReadVideoPermission);
10530                 return hasReadVideoPermission;
10531             }
10532 
10533             return true;
10534         }
10535 
10536         // Apps with write access to images and/or videos can bypass our restrictions if all of the
10537         // the files they're accessing are of the compatible media type.
10538         return canSystemGalleryAccessTheFile(filePath);
10539     }
10540 
10541     /**
10542      * Returns true if the passed in path is an application-private data directory
10543      * (such as Android/data/com.foo or Android/obb/com.foo) that does not belong to the caller and
10544      * the caller does not have special access.
10545      */
10546     private boolean isPrivatePackagePathNotAccessibleByCaller(String path) {
10547         // Files under the apps own private directory
10548         final String appSpecificDir = extractPathOwnerPackageName(path);
10549 
10550         if (appSpecificDir == null) {
10551             return false;
10552         }
10553 
10554         // Android/media is not considered private, because it contains media that is explicitly
10555         // scanned and shared by other apps
10556         if (isExternalMediaDirectory(path)) {
10557             return false;
10558         }
10559         return !isUidAllowedAccessToDataOrObbPathForFuse(mCallingIdentity.get().uid, path);
10560     }
10561 
10562     private boolean shouldBypassDatabaseAndSetDirtyForFuse(int uid, String path) {
10563         if (shouldBypassDatabase(uid)) {
10564             synchronized (mNonHiddenPaths) {
10565                 File file = new File(path);
10566                 String key = file.getParent();
10567                 boolean maybeHidden = !mNonHiddenPaths.containsKey(key);
10568 
10569                 if (maybeHidden) {
10570                     File topNoMediaDir = FileUtils.getTopLevelNoMedia(new File(path));
10571                     if (topNoMediaDir == null) {
10572                         mNonHiddenPaths.put(key, 0);
10573                     } else {
10574                         mMediaScanner.onDirectoryDirty(topNoMediaDir);
10575                         invalidateFuseDentry(topNoMediaDir);
10576                     }
10577                 }
10578             }
10579             return true;
10580         }
10581         return false;
10582     }
10583 
10584     private static class LRUCache<K, V> extends LinkedHashMap<K, V> {
10585         private final int mMaxSize;
10586 
10587         public LRUCache(int maxSize) {
10588             this.mMaxSize = maxSize;
10589         }
10590 
10591         @Override
10592         protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
10593             return size() > mMaxSize;
10594         }
10595     }
10596 
10597     private static final class PendingOpenInfo {
10598         public final int uid;
10599         public final int mediaCapabilitiesUid;
10600         public final boolean shouldRedact;
10601         public final int transcodeReason;
10602 
10603         public PendingOpenInfo(int uid, int mediaCapabilitiesUid, boolean shouldRedact,
10604                 int transcodeReason) {
10605             this.uid = uid;
10606             this.mediaCapabilitiesUid = mediaCapabilitiesUid;
10607             this.shouldRedact = shouldRedact;
10608             this.transcodeReason = transcodeReason;
10609         }
10610     }
10611 
10612     /**
10613      * Calculates the ranges that need to be redacted for the given file and user that wants to
10614      * access the file.
10615      * Note: This method assumes that the caller of this function has already done permission checks
10616      * for the uid to access this path.
10617      *
10618      * @param uid UID of the package wanting to access the file
10619      * @param path File path
10620      * @param tid thread id making IO on the FUSE filesystem
10621      * @return Ranges that should be redacted.
10622      *
10623      * @throws IOException if an error occurs while calculating the redaction ranges
10624      */
10625     @NonNull
10626     private long[] getRedactionRangesForFuse(String path, String ioPath, int original_uid, int uid,
10627             int tid, boolean forceRedaction) throws IOException {
10628         // |ioPath| might refer to a transcoded file path (which is not indexed in the db)
10629         // |path| will always refer to a valid _data column
10630         // We use |ioPath| for the filesystem access because in the case of transcoding,
10631         // we want to get redaction ranges from the transcoded file and *not* the original file
10632         final File file = new File(ioPath);
10633 
10634         if (forceRedaction) {
10635             return RedactionUtils.getRedactionRanges(file);
10636         }
10637 
10638         // When calculating redaction ranges initiated from MediaProvider, the redaction policy
10639         // is slightly different from the FUSE initiated opens redaction policy. targetSdk=29 from
10640         // MediaProvider requires redaction, but targetSdk=29 apps from FUSE don't require redaction
10641         // Hence, we check the mPendingOpenInfo object (populated when opens are initiated from
10642         // MediaProvider) if there's a pending open from MediaProvider with matching tid and uid and
10643         // use the shouldRedact decision there if there's one.
10644         PendingOpenInfo info;
10645         synchronized (mPendingOpenInfo) {
10646             info = mPendingOpenInfo.get(tid);
10647         }
10648 
10649         if (info != null && info.uid == original_uid) {
10650             boolean shouldRedact = info.shouldRedact;
10651             if (shouldRedact) {
10652                 return RedactionUtils.getRedactionRanges(file);
10653             } else {
10654                 return new long[0];
10655             }
10656         }
10657 
10658         final LocalCallingIdentity token =
10659                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
10660         try {
10661             if (!isRedactionNeeded()
10662                     || shouldBypassFuseRestrictions(/* forWrite */ false, path)) {
10663                 return new long[0];
10664             }
10665 
10666             final Uri contentUri = FileUtils.getContentUriForPath(path);
10667             final String[] projection = new String[]{
10668                     MediaColumns.OWNER_PACKAGE_NAME, MediaColumns._ID , FileColumns.MEDIA_TYPE};
10669             final String selection = MediaColumns.DATA + "=?";
10670             final String[] selectionArgs = new String[]{path};
10671             final String ownerPackageName;
10672             final int id;
10673             final int mediaType;
10674             // Query as MediaProvider as non-RES apps will result in FileNotFoundException.
10675             // Note: The caller uid already has passed permission checks to access this file.
10676             try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection,
10677                     selection, selectionArgs, null)) {
10678                 c.moveToFirst();
10679                 ownerPackageName = c.getString(0);
10680                 id = c.getInt(1);
10681                 mediaType = c.getInt(2);
10682             } catch (FileNotFoundException e) {
10683                 // Ideally, this shouldn't happen unless the file was deleted after we checked its
10684                 // existence and before we get to the redaction logic here. In this case we throw
10685                 // and fail the operation and FuseDaemon should handle this and fail the whole open
10686                 // operation gracefully.
10687                 throw new FileNotFoundException(
10688                         path + " not found while calculating redaction ranges: " + e.getMessage());
10689             }
10690 
10691             final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(),
10692                     ownerPackageName);
10693 
10694             // Do not redact if the caller is the owner
10695             if (callerIsOwner) {
10696                 return new long[0];
10697             }
10698 
10699             // Do not redact if the caller has write uri permission granted on the file.
10700             final Uri fileUri = ContentUris.withAppendedId(contentUri, id);
10701             boolean callerHasWriteUriPermission = getContext().checkUriPermission(
10702                     fileUri, mCallingIdentity.get().pid, mCallingIdentity.get().uid,
10703                     Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PERMISSION_GRANTED;
10704             if (callerHasWriteUriPermission) {
10705                 return new long[0];
10706             }
10707             // Check if the caller has write access to other uri formats for the same file.
10708             callerHasWriteUriPermission = getOtherUriGrantsForPath(path, mediaType,
10709                     Long.toString(id), /* forWrite */ true) != null;
10710             if (callerHasWriteUriPermission) {
10711                 return new long[0];
10712             }
10713 
10714             return RedactionUtils.getRedactionRanges(file);
10715         } finally {
10716             restoreLocalCallingIdentity(token);
10717         }
10718     }
10719 
10720     /**
10721      * @return {@code true} if {@code file} is pending from FUSE, {@code false} otherwise.
10722      * Files pending from FUSE will not have pending file pattern.
10723      */
10724     private static boolean isPendingFromFuse(@NonNull File file) {
10725         final Matcher matcher =
10726                 FileUtils.PATTERN_EXPIRES_FILE.matcher(extractDisplayName(file.getName()));
10727         return !matcher.matches();
10728     }
10729 
10730     private FileAccessAttributes queryForFileAttributes(final String path)
10731             throws FileNotFoundException {
10732         Trace.beginSection("MP.queryFileAttr");
10733         final Uri contentUri = FileUtils.getContentUriForPath(path);
10734         final String[] projection = new String[]{
10735                 MediaColumns._ID,
10736                 MediaColumns.OWNER_PACKAGE_NAME,
10737                 MediaColumns.IS_PENDING,
10738                 FileColumns.MEDIA_TYPE,
10739                 MediaColumns.IS_TRASHED
10740         };
10741         final String selection = MediaColumns.DATA + "=?";
10742         final String[] selectionArgs = new String[]{path};
10743         FileAccessAttributes fileAccessAttributes;
10744         try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection,
10745                 selection,
10746                 selectionArgs, null)) {
10747             fileAccessAttributes = FileAccessAttributes.fromCursor(c);
10748         }
10749         Trace.endSection();
10750         return fileAccessAttributes;
10751     }
10752 
10753     private void checkIfFileOpenIsPermitted(String path,
10754             FileAccessAttributes fileAccessAttributes, String redactedUriId,
10755             boolean forWrite) throws FileNotFoundException {
10756         final File file = new File(path);
10757         Uri fileUri = MediaStore.Files.getContentUri(extractVolumeName(path),
10758                 fileAccessAttributes.getId());
10759         // We don't check ownership for files with IS_PENDING set by FUSE
10760         // Please note that even if ownerPackageName is null, the check below will throw an
10761         // IllegalStateException
10762         if (fileAccessAttributes.isTrashed() || (fileAccessAttributes.isPending()
10763                 && !isPendingFromFuse(new File(path)))) {
10764             requireOwnershipForItem(fileAccessAttributes.getOwnerPackageName(), fileUri);
10765         }
10766 
10767         // Check that path looks consistent before uri checks
10768         if (!FileUtils.contains(Environment.getStorageDirectory(), file)) {
10769             checkWorldReadAccess(file.getAbsolutePath());
10770         }
10771 
10772         try {
10773             // checkAccess throws FileNotFoundException only from checkWorldReadAccess(),
10774             // which we already check above. Hence, handling only SecurityException.
10775             if (redactedUriId != null) {
10776                 fileUri = ContentUris.removeId(fileUri).buildUpon().appendPath(
10777                         redactedUriId).build();
10778             }
10779             checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
10780         } catch (SecurityException e) {
10781             // Check for other Uri formats only when the single uri check flow fails.
10782             // Throw the previous exception if the multi-uri checks failed.
10783             final String uriId = redactedUriId == null
10784                     ? Long.toString(fileAccessAttributes.getId()) : redactedUriId;
10785             if (getOtherUriGrantsForPath(path, fileAccessAttributes.getMediaType(),
10786                     uriId, forWrite) == null) {
10787                 throw e;
10788             }
10789         }
10790     }
10791 
10792 
10793     /**
10794      * Checks if the app identified by the given UID is allowed to open the given file for the given
10795      * access mode.
10796      *
10797      * @param path the path of the file to be opened
10798      * @param uid UID of the app requesting to open the file
10799      * @param forWrite specifies if the file is to be opened for write
10800      * @return {@link FileOpenResult} with {@code status} {@code 0} upon success and
10801      * {@link FileOpenResult} with {@code status} {@link OsConstants#EACCES} if the operation is
10802      * illegal or not permitted for the given {@code uid} or if the calling package is a legacy app
10803      * that doesn't have right storage permission.
10804      *
10805      * Called from JNI in jni/MediaProviderWrapper.cpp
10806      */
10807     @Keep
10808     public FileOpenResult onFileOpenForFuse(String path, String ioPath, int uid, int tid,
10809             int transformsReason, boolean forWrite, boolean redact, boolean logTransformsMetrics) {
10810         final LocalCallingIdentity token =
10811                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
10812 
10813         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
10814 
10815         boolean isSuccess = false;
10816 
10817         final int originalUid = getBinderUidForFuse(uid, tid);
10818         // Use MediaProvider's own ID here since the caller may be cross profile.
10819         final int userId = UserHandle.myUserId();
10820         int mediaCapabilitiesUid = 0;
10821         final PendingOpenInfo pendingOpenInfo;
10822         synchronized (mPendingOpenInfo) {
10823             pendingOpenInfo = mPendingOpenInfo.get(tid);
10824         }
10825 
10826         if (pendingOpenInfo != null && pendingOpenInfo.uid == originalUid) {
10827             mediaCapabilitiesUid = pendingOpenInfo.mediaCapabilitiesUid;
10828         }
10829 
10830         try {
10831             boolean forceRedaction = false;
10832             String redactedUriId = null;
10833             if (isSyntheticPath(path, userId)) {
10834                 if (forWrite) {
10835                     // Synthetic URIs are not allowed to update EXIF headers.
10836                     return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
10837                             mediaCapabilitiesUid, new long[0]);
10838                 }
10839 
10840                 if (isRedactedPath(path, userId)) {
10841                     redactedUriId = extractFileName(path);
10842 
10843                     // If path is redacted Uris' path, ioPath must be the real path, ioPath must
10844                     // haven been updated to the real path during onFileLookupForFuse.
10845                     path = ioPath;
10846 
10847                     // Irrespective of the permissions we want to redact in this case.
10848                     redact = true;
10849                     forceRedaction = true;
10850                 } else if (isPickerPath(path, userId)) {
10851                     return handlePickerFileOpen(path, originalUid);
10852                 } else {
10853                     // we don't support any other transformations under .transforms/synthetic dir
10854                     return new FileOpenResult(OsConstants.ENOENT /* status */, originalUid,
10855                             mediaCapabilitiesUid, new long[0]);
10856                 }
10857             }
10858 
10859             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
10860                 Log.e(TAG, "Can't open a file in another app's external directory!");
10861                 return new FileOpenResult(OsConstants.ENOENT, originalUid, mediaCapabilitiesUid,
10862                         new long[0]);
10863             }
10864 
10865             if (shouldBypassFuseRestrictions(forWrite, path)) {
10866                 isSuccess = true;
10867                 return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid,
10868                         redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid,
10869                                 forceRedaction) : new long[0]);
10870             }
10871             // Legacy apps that made is this far don't have the right storage permission and hence
10872             // are not allowed to access anything other than their external app directory
10873             if (isCallingPackageRequestingLegacy()) {
10874                 return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
10875                         mediaCapabilitiesUid, new long[0]);
10876             }
10877 
10878             checkIfFileOpenIsPermitted(path, queryForFileAttributes(path), redactedUriId, forWrite);
10879             isSuccess = true;
10880             return new FileOpenResult(0 /* status */, originalUid, mediaCapabilitiesUid,
10881                     redact ? getRedactionRangesForFuse(path, ioPath, originalUid, uid, tid,
10882                             forceRedaction) : new long[0]);
10883         } catch (IOException e) {
10884             // We are here because
10885             // * There is no db row corresponding to the requested path, which is more unlikely.
10886             // * getRedactionRangesForFuse couldn't fetch the redaction info correctly
10887             // In all of these cases, it means that app doesn't have access permission to the file.
10888             Log.e(TAG, "Couldn't find file: " + path, e);
10889             return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
10890                     mediaCapabilitiesUid, new long[0]);
10891         } catch (IllegalStateException | SecurityException e) {
10892             Log.e(TAG, "Permission to access file: " + path + " is denied");
10893             return new FileOpenResult(OsConstants.EACCES /* status */, originalUid,
10894                     mediaCapabilitiesUid, new long[0]);
10895         } finally {
10896             if (isSuccess && logTransformsMetrics) {
10897                 notifyTranscodeHelperOnFileOpen(path, ioPath, originalUid, transformsReason);
10898             }
10899             restoreLocalCallingIdentity(token);
10900         }
10901     }
10902 
10903     @Nullable
10904     private Uri getOtherUriGrantsForPath(String path, boolean forWrite) {
10905         final Uri contentUri = FileUtils.getContentUriForPath(path);
10906         final String[] projection = new String[]{
10907                 MediaColumns._ID,
10908                 FileColumns.MEDIA_TYPE};
10909         final String selection = MediaColumns.DATA + "=?";
10910         final String[] selectionArgs = new String[]{path};
10911         final String id;
10912         final int mediaType;
10913         try (final Cursor c = queryForSingleItemAsMediaProvider(contentUri, projection, selection,
10914                 selectionArgs, null)) {
10915             id = c.getString(0);
10916             mediaType = c.getInt(1);
10917             return getOtherUriGrantsForPath(path, mediaType, id, forWrite);
10918         } catch (FileNotFoundException ignored) {
10919         }
10920         return null;
10921     }
10922 
10923     @Nullable
10924     private Uri getOtherUriGrantsForPath(String path, int mediaType, String id, boolean forWrite) {
10925         List<Uri> otherUris = new ArrayList<>();
10926         final Uri mediaUri = getMediaUriForFuse(extractVolumeName(path), mediaType, id);
10927         otherUris.add(mediaUri);
10928         final Uri externalMediaUri = getMediaUriForFuse(MediaStore.VOLUME_EXTERNAL, mediaType, id);
10929         otherUris.add(externalMediaUri);
10930         return getPermissionGrantedUri(otherUris, forWrite);
10931     }
10932 
10933     @NonNull
10934     private Uri getMediaUriForFuse(@NonNull String volumeName, int mediaType, String id) {
10935         Uri uri = MediaStore.Files.getContentUri(volumeName);
10936         switch (mediaType) {
10937             case FileColumns.MEDIA_TYPE_IMAGE:
10938                 uri = MediaStore.Images.Media.getContentUri(volumeName);
10939                 break;
10940             case FileColumns.MEDIA_TYPE_VIDEO:
10941                 uri = MediaStore.Video.Media.getContentUri(volumeName);
10942                 break;
10943             case FileColumns.MEDIA_TYPE_AUDIO:
10944                 uri = MediaStore.Audio.Media.getContentUri(volumeName);
10945                 break;
10946             case FileColumns.MEDIA_TYPE_PLAYLIST:
10947                 uri = MediaStore.Audio.Playlists.getContentUri(volumeName);
10948                 break;
10949         }
10950 
10951         return uri.buildUpon().appendPath(id).build();
10952     }
10953 
10954     /**
10955      * Returns {@code true} if {@link #mCallingIdentity#getSharedPackageNamesList(String)} contains
10956      * the given package name, {@code false} otherwise.
10957      * <p> Assumes that {@code mCallingIdentity} has been properly set to reflect the calling
10958      * package.
10959      */
10960     private boolean isCallingIdentitySharedPackageName(@NonNull String packageName) {
10961         for (String sharedPkgName : mCallingIdentity.get().getSharedPackageNamesArray()) {
10962             if (packageName.toLowerCase(Locale.ROOT)
10963                     .equals(sharedPkgName.toLowerCase(Locale.ROOT))) {
10964                 return true;
10965             }
10966         }
10967         return false;
10968     }
10969 
10970     /**
10971      * @throws IllegalStateException if path is invalid or doesn't match a volume.
10972      */
10973     @NonNull
10974     private Uri getContentUriForFile(@NonNull String filePath, @NonNull String mimeType) {
10975         final String volName;
10976         try {
10977             volName = FileUtils.getVolumeName(getContext(), new File(filePath));
10978         } catch (FileNotFoundException e) {
10979             throw new IllegalStateException("Couldn't get volume name for " + filePath);
10980         }
10981         Uri uri = Files.getContentUri(volName);
10982         String topLevelDir = extractTopLevelDir(filePath);
10983         if (topLevelDir == null) {
10984             // If the file path doesn't match the external storage directory, we use the files URI
10985             // as default and let #insert enforce the restrictions
10986             return uri;
10987         }
10988         topLevelDir = topLevelDir.toLowerCase(Locale.ROOT);
10989 
10990         switch (topLevelDir) {
10991             case DIRECTORY_PODCASTS_LOWER_CASE:
10992             case DIRECTORY_RINGTONES_LOWER_CASE:
10993             case DIRECTORY_ALARMS_LOWER_CASE:
10994             case DIRECTORY_NOTIFICATIONS_LOWER_CASE:
10995             case DIRECTORY_AUDIOBOOKS_LOWER_CASE:
10996             case DIRECTORY_RECORDINGS_LOWER_CASE:
10997                 uri = Audio.Media.getContentUri(volName);
10998                 break;
10999             case DIRECTORY_MUSIC_LOWER_CASE:
11000                 if (MimeUtils.isPlaylistMimeType(mimeType)) {
11001                     uri = Audio.Playlists.getContentUri(volName);
11002                 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) {
11003                     // Send Files uri for media type subtitle
11004                     uri = Audio.Media.getContentUri(volName);
11005                 }
11006                 break;
11007             case DIRECTORY_MOVIES_LOWER_CASE:
11008                 if (MimeUtils.isPlaylistMimeType(mimeType)) {
11009                     uri = Audio.Playlists.getContentUri(volName);
11010                 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) {
11011                     // Send Files uri for media type subtitle
11012                     uri = Video.Media.getContentUri(volName);
11013                 }
11014                 break;
11015             case DIRECTORY_DCIM_LOWER_CASE:
11016             case DIRECTORY_PICTURES_LOWER_CASE:
11017                 if (MimeUtils.isImageMimeType(mimeType)) {
11018                     uri = Images.Media.getContentUri(volName);
11019                 } else {
11020                     uri = Video.Media.getContentUri(volName);
11021                 }
11022                 break;
11023             case DIRECTORY_DOWNLOADS_LOWER_CASE:
11024             case DIRECTORY_DOCUMENTS_LOWER_CASE:
11025                 break;
11026             default:
11027                 Log.w(TAG, "Forgot to handle a top level directory in getContentUriForFile?");
11028         }
11029         return uri;
11030     }
11031 
11032     private boolean containsIgnoreCase(@Nullable List<String> stringsList, @Nullable String item) {
11033         if (item == null || stringsList == null) return false;
11034 
11035         for (String current : stringsList) {
11036             if (item.equalsIgnoreCase(current)) return true;
11037         }
11038         return false;
11039     }
11040 
11041     private boolean fileExists(@NonNull String absolutePath) {
11042         // We don't care about specific columns in the match,
11043         // we just want to check IF there's a match
11044         final String[] projection = {};
11045         final String selection = FileColumns.DATA + " = ?";
11046         final String[] selectionArgs = {absolutePath};
11047         final Uri uri = FileUtils.getContentUriForPath(absolutePath);
11048 
11049         final LocalCallingIdentity token = clearLocalCallingIdentity();
11050         try {
11051             try (final Cursor c = query(uri, projection, selection, selectionArgs, null)) {
11052                 // Shouldn't return null
11053                 return c.getCount() > 0;
11054             }
11055         } finally {
11056             clearLocalCallingIdentity(token);
11057         }
11058     }
11059 
11060     private Uri insertFileForFuse(@NonNull String path, @NonNull Uri uri, @NonNull String mimeType,
11061             boolean useData) {
11062         ContentValues values = new ContentValues();
11063         values.put(FileColumns.OWNER_PACKAGE_NAME, getCallingPackageOrSelf());
11064         values.put(MediaColumns.MIME_TYPE, mimeType);
11065         values.put(FileColumns.IS_PENDING, 1);
11066 
11067         int userIdFromPath = FileUtils.extractUserId(path);
11068 
11069         if (useData) {
11070             values.put(FileColumns.DATA, path);
11071         } else {
11072             values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
11073             values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
11074             values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
11075             // In some cases when clone profile is active, this userId can be used to determine
11076             // the path to be saved in MP database.
11077             // We do this only if the path contains a valid user-id and any such value set is
11078             // only a hint, the actual userId set will be determined later.
11079             if (userIdFromPath != -1) {
11080                 values.put(FileColumns._USER_ID, userIdFromPath);
11081             }
11082         }
11083         return insert(uri, values, Bundle.EMPTY);
11084     }
11085 
11086     /**
11087      * Enforces file creation restrictions (see return values) for the given file on behalf of the
11088      * app with the given {@code uid}. If the file is added to the shared storage, creates a
11089      * database entry for it.
11090      * <p> Does NOT create file.
11091      *
11092      * @param path the path of the file
11093      * @param uid UID of the app requesting to create the file
11094      * @return In case of success, 0. If the operation is illegal or not permitted, returns the
11095      * appropriate {@code errno} value:
11096      * <ul>
11097      * <li>{@link OsConstants#ENOENT} if the app tries to create file in other app's external dir
11098      * <li>{@link OsConstants#EEXIST} if the file already exists
11099      * <li>{@link OsConstants#EPERM} if the file type doesn't match the relative path, or if the
11100      * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission.
11101      * <li>{@link OsConstants#EIO} in case of any other I/O exception
11102      * </ul>
11103      *
11104      * @throws IllegalStateException if given path is invalid.
11105      *
11106      * Called from JNI in jni/MediaProviderWrapper.cpp
11107      */
11108     @Keep
11109     public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) {
11110         final LocalCallingIdentity token =
11111                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
11112         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
11113 
11114         try {
11115             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
11116                 Log.e(TAG, "Can't create a file in another app's external directory");
11117                 return OsConstants.ENOENT;
11118             }
11119 
11120             if (!path.equals(getAbsoluteSanitizedPath(path))) {
11121                 Log.e(TAG, "File name contains invalid characters");
11122                 return OsConstants.EPERM;
11123             }
11124 
11125             if (shouldBypassDatabaseAndSetDirtyForFuse(uid, path)) {
11126                 if (path.endsWith("/.nomedia")) {
11127                     File parent = new File(path).getParentFile();
11128                     synchronized (mNonHiddenPaths) {
11129                         mNonHiddenPaths.keySet().removeIf(
11130                                 k -> FileUtils.contains(parent, new File(k)));
11131                     }
11132                 }
11133                 return 0;
11134             }
11135 
11136             final String mimeType = MimeUtils.resolveMimeType(new File(path));
11137 
11138             if (shouldBypassFuseRestrictions(/* forWrite */ true, path)) {
11139                 final boolean callerRequestingLegacy = isCallingPackageRequestingLegacy();
11140                 if (!fileExists(path)) {
11141                     // If app has already inserted the db row, inserting the row again might set
11142                     // IS_PENDING=1. We shouldn't overwrite existing entry as part of FUSE
11143                     // operation, hence, insert the db row only when it doesn't exist.
11144                     try {
11145                         insertFileForFuse(path, FileUtils.getContentUriForPath(path),
11146                                 mimeType, /* useData */ callerRequestingLegacy);
11147                     } catch (Exception ignored) {
11148                     }
11149                 } else {
11150                     // Upon creating a file via FUSE, if a row matching the path already exists
11151                     // but a file doesn't exist on the filesystem, we transfer ownership to the
11152                     // app attempting to create the file. If we don't update ownership, then the
11153                     // app that inserted the original row may be able to observe the contents of
11154                     // written file even though they don't hold the right permissions to do so.
11155                     if (callerRequestingLegacy) {
11156                         final String owner = getCallingPackageOrSelf();
11157                         if (owner != null && !updateOwnerForPath(path, owner)) {
11158                             return OsConstants.EPERM;
11159                         }
11160                     }
11161                 }
11162 
11163                 return 0;
11164             }
11165 
11166             // Legacy apps that made is this far don't have the right storage permission and hence
11167             // are not allowed to access anything other than their external app directory
11168             if (isCallingPackageRequestingLegacy()) {
11169                 return OsConstants.EPERM;
11170             }
11171 
11172             if (fileExists(path)) {
11173                 // If the file already exists in the db, we shouldn't allow the file creation.
11174                 return OsConstants.EEXIST;
11175             }
11176 
11177             final Uri contentUri = getContentUriForFile(path, mimeType);
11178             final Uri item = insertFileForFuse(path, contentUri, mimeType, /* useData */ false);
11179             if (item == null) {
11180                 return OsConstants.EPERM;
11181             }
11182             return 0;
11183         } catch (IllegalArgumentException e) {
11184             Log.e(TAG, "insertFileIfNecessary failed", e);
11185             return OsConstants.EPERM;
11186         } finally {
11187             restoreLocalCallingIdentity(token);
11188         }
11189     }
11190 
11191     private boolean updateOwnerForPath(@NonNull String path, @NonNull String newOwner) {
11192         final DatabaseHelper helper;
11193         try {
11194             helper = getDatabaseForUri(FileUtils.getContentUriForPath(path));
11195         } catch (VolumeNotFoundException e) {
11196             // Cannot happen, as this is a path that we already resolved.
11197             throw new AssertionError("Path must already be resolved", e);
11198         }
11199 
11200         ContentValues values = new ContentValues(1);
11201         values.put(FileColumns.OWNER_PACKAGE_NAME, newOwner);
11202 
11203         return helper.runWithoutTransaction(
11204                 (db) -> db.update("files", values, "_data=?", new String[] { path })) == 1;
11205     }
11206 
11207     private int deleteFileUnchecked(@NonNull String path,
11208             LocalCallingIdentity localCallingIdentity) {
11209         final File toDelete = new File(path);
11210         if (toDelete.delete()) {
11211             final int mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(toDelete));
11212             localCallingIdentity.incrementDeletedFileCountBypassingDatabase(mediaType);
11213             return 0;
11214         } else {
11215             return OsConstants.ENOENT;
11216         }
11217     }
11218 
11219     /**
11220      * Deletes file with the given {@code path} on behalf of the app with the given {@code uid}.
11221      * <p>Before deleting, checks if app has permissions to delete this file.
11222      *
11223      * @param path the path of the file
11224      * @param uid UID of the app requesting to delete the file
11225      * @return 0 upon success.
11226      * In case of error, return the appropriate negated {@code errno} value:
11227      * <ul>
11228      * <li>{@link OsConstants#ENOENT} if the file does not exist or if the app tries to delete file
11229      * in another app's external dir
11230      * <li>{@link OsConstants#EPERM} a security exception was thrown by {@link #delete}, or if the
11231      * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission.
11232      * </ul>
11233      *
11234      * Called from JNI in jni/MediaProviderWrapper.cpp
11235      */
11236     @Keep
11237     public int deleteFileForFuse(@NonNull String path, int uid) throws IOException {
11238         final LocalCallingIdentity localCallingIdentity = getCachedCallingIdentityForFuse(uid);
11239         final LocalCallingIdentity token = clearLocalCallingIdentity(localCallingIdentity);
11240         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
11241 
11242         try {
11243             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
11244                 Log.e(TAG, "Can't delete a file in another app's external directory!");
11245                 return OsConstants.ENOENT;
11246             }
11247 
11248             if (shouldBypassDatabaseAndSetDirtyForFuse(uid, path)) {
11249                 return deleteFileUnchecked(path, localCallingIdentity);
11250             }
11251 
11252             final boolean shouldBypass = shouldBypassFuseRestrictions(/*forWrite*/ true, path);
11253 
11254             // Legacy apps that made is this far don't have the right storage permission and hence
11255             // are not allowed to access anything other than their external app directory
11256             if (!shouldBypass && isCallingPackageRequestingLegacy()) {
11257                 return OsConstants.EPERM;
11258             }
11259 
11260             final Uri contentUri = FileUtils.getContentUriForPath(path);
11261             final String where = FileColumns.DATA + " = ?";
11262             final String[] whereArgs = {path};
11263 
11264             if (delete(contentUri, where, whereArgs) == 0) {
11265                 if (shouldBypass) {
11266                     return deleteFileUnchecked(path, localCallingIdentity);
11267                 }
11268                 return OsConstants.ENOENT;
11269             } else {
11270                 // success - 1 file was deleted
11271                 return 0;
11272             }
11273 
11274         } catch (SecurityException e) {
11275             Log.e(TAG, "File deletion not allowed", e);
11276             return OsConstants.EPERM;
11277         } finally {
11278             restoreLocalCallingIdentity(token);
11279         }
11280     }
11281 
11282     // These need to stay in sync with MediaProviderWrapper.cpp's DirectoryAccessRequestType enum
11283     @IntDef(flag = true, prefix = { "DIRECTORY_ACCESS_FOR_" }, value = {
11284             DIRECTORY_ACCESS_FOR_READ,
11285             DIRECTORY_ACCESS_FOR_WRITE,
11286             DIRECTORY_ACCESS_FOR_CREATE,
11287             DIRECTORY_ACCESS_FOR_DELETE,
11288     })
11289     @Retention(RetentionPolicy.SOURCE)
11290     @VisibleForTesting
11291     @interface DirectoryAccessType {}
11292 
11293     @VisibleForTesting
11294     static final int DIRECTORY_ACCESS_FOR_READ = 1;
11295 
11296     @VisibleForTesting
11297     static final int DIRECTORY_ACCESS_FOR_WRITE = 2;
11298 
11299     @VisibleForTesting
11300     static final int DIRECTORY_ACCESS_FOR_CREATE = 3;
11301 
11302     @VisibleForTesting
11303     static final int DIRECTORY_ACCESS_FOR_DELETE = 4;
11304 
11305     /**
11306      * Checks whether the app with the given UID is allowed to access the directory denoted by the
11307      * given path.
11308      *
11309      * @param path directory's path
11310      * @param uid UID of the requesting app
11311      * @param accessType type of access being requested - eg {@link
11312      * MediaProvider#DIRECTORY_ACCESS_FOR_READ}
11313      * @return 0 if it's allowed to access the directory, {@link OsConstants#ENOENT} for attempts
11314      * to access a private package path in Android/data or Android/obb the caller doesn't have
11315      * access to, and otherwise {@link OsConstants#EACCES} if the calling package is a legacy app
11316      * that doesn't have READ_EXTERNAL_STORAGE permission or for other invalid attempts to access
11317      * Android/data or Android/obb dirs.
11318      *
11319      * Called from JNI in jni/MediaProviderWrapper.cpp
11320      */
11321     @Keep
11322     public int isDirAccessAllowedForFuse(@NonNull String path, int uid,
11323             @DirectoryAccessType int accessType) {
11324         Preconditions.checkArgumentInRange(accessType, 1, DIRECTORY_ACCESS_FOR_DELETE,
11325                 "accessType");
11326 
11327         final boolean forRead = accessType == DIRECTORY_ACCESS_FOR_READ;
11328         final LocalCallingIdentity token =
11329                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
11330         PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
11331         try {
11332             if ("/storage/emulated".equals(path)) {
11333                 return OsConstants.EPERM;
11334             }
11335             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
11336                 Log.e(TAG, "Can't access another app's external directory!");
11337                 return OsConstants.ENOENT;
11338             }
11339 
11340             if (shouldBypassFuseRestrictions(/* forWrite= */ !forRead, path)) {
11341                 return 0;
11342             }
11343 
11344             // Do not allow apps that reach this point to access Android/data or Android/obb dirs.
11345             // Creation should be via getContext().getExternalFilesDir() etc methods.
11346             // Reads and writes on primary volumes should be via mount views of lowerfs for apps
11347             // that get special access to these directories.
11348             // Reads and writes on secondary volumes would be provided via an early return from
11349             // shouldBypassFuseRestrictions above (again just for apps with special access).
11350             if (isDataOrObbPath(path)) {
11351                 return OsConstants.EACCES;
11352             }
11353 
11354             // Legacy apps that made is this far don't have the right storage permission and hence
11355             // are not allowed to access anything other than their external app directory
11356             if (isCallingPackageRequestingLegacy()) {
11357                 return OsConstants.EACCES;
11358             }
11359             // This is a non-legacy app. Rest of the directories are generally writable
11360             // except for non-default top-level directories.
11361             if (!forRead) {
11362                 final String[] relativePath = sanitizePath(extractRelativePath(path));
11363                 if (relativePath.length == 0) {
11364                     Log.e(TAG,
11365                             "Directory update not allowed on invalid relative path for " + path);
11366                     return OsConstants.EPERM;
11367                 }
11368                 final boolean isTopLevelDir =
11369                         relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
11370                 if (isTopLevelDir) {
11371                     // We don't allow deletion of any top-level folders
11372                     if (accessType == DIRECTORY_ACCESS_FOR_DELETE) {
11373                         Log.e(TAG, "Deleting top level directories are not allowed!");
11374                         return OsConstants.EACCES;
11375                     }
11376 
11377                     // We allow creating or writing to default top-level folders, but we don't
11378                     // allow creation or writing to non-default top-level folders.
11379                     if ((accessType == DIRECTORY_ACCESS_FOR_CREATE
11380                             || accessType == DIRECTORY_ACCESS_FOR_WRITE)
11381                             && FileUtils.isDefaultDirectoryName(extractDisplayName(path))) {
11382                         return 0;
11383                     }
11384 
11385                     Log.e(TAG,
11386                             "Creating or writing to a non-default top level directory is not "
11387                                     + "allowed!");
11388                     return OsConstants.EACCES;
11389                 }
11390             }
11391 
11392             return 0;
11393         } finally {
11394             restoreLocalCallingIdentity(token);
11395         }
11396     }
11397 
11398     @Keep
11399     public boolean isUidAllowedAccessToDataOrObbPathForFuse(int uid, String path) {
11400         final LocalCallingIdentity token =
11401                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
11402         try {
11403             return isCallingIdentityAllowedAccessToDataOrObbPath(
11404                     extractRelativePathWithDisplayName(path));
11405         } finally {
11406             restoreLocalCallingIdentity(token);
11407         }
11408     }
11409 
11410     private boolean isCallingIdentityAllowedAccessToDataOrObbPath(String relativePath) {
11411         // Files under the apps own private directory
11412         final String appSpecificDir = extractOwnerPackageNameFromRelativePath(relativePath);
11413 
11414         if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) {
11415             return true;
11416         }
11417         // This is a private-package relativePath; return true if accessible by the caller
11418         return isCallingIdentityAllowedSpecialPrivatePathAccess(relativePath);
11419     }
11420 
11421     /**
11422      * @return true iff the caller has installer privileges which gives write access to obb dirs.
11423      *
11424      * @deprecated This method should only be called for Android R. For Android S+, please use
11425      * {@link StorageManager#getExternalStorageMountMode} to check if the caller has
11426      * {@link StorageManager#MOUNT_MODE_EXTERNAL_INSTALLER} access.
11427      *
11428      * Note: WRITE_EXTERNAL_STORAGE permission should ideally not be requested by non-legacy apps.
11429      * But to be consistent with {@link StorageManager} check for Installer apps access for primary
11430      * volumes in Android R, we do not add non-legacy apps check here as well.
11431      */
11432     @Deprecated
11433     private boolean isCallingIdentityAllowedInstallerAccess() {
11434         final boolean hasWrite = mCallingIdentity.get().
11435                 hasPermission(PERMISSION_WRITE_EXTERNAL_STORAGE);
11436 
11437         if (!hasWrite) {
11438             return false;
11439         }
11440 
11441         // We're only willing to give out installer access if they also hold
11442         // runtime permission; this is a firm CDD requirement
11443         final boolean hasInstall = mCallingIdentity.get().
11444                 hasPermission(PERMISSION_INSTALL_PACKAGES);
11445 
11446         if (hasInstall) {
11447             return true;
11448         }
11449         // OPSTR_REQUEST_INSTALL_PACKAGES is granted/denied per package but vold can't
11450         // update mountpoints of a specific package. So, check the appop for all packages
11451         // sharing the uid and allow same level of storage access for all packages even if
11452         // one of the packages has the appop granted.
11453         // To maintain consistency of access in primary volume and secondary volumes use the same
11454         // logic as we do for Zygote.MOUNT_EXTERNAL_INSTALLER view.
11455         return mCallingIdentity.get().hasPermission(APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID);
11456     }
11457 
11458     private String getExternalStorageProviderAuthority() {
11459         if (SdkLevel.isAtLeastS()) {
11460             return getExternalStorageProviderAuthorityFromDocumentsContract();
11461         }
11462         return MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
11463     }
11464 
11465     @RequiresApi(Build.VERSION_CODES.S)
11466     private String getExternalStorageProviderAuthorityFromDocumentsContract() {
11467         return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
11468     }
11469 
11470     private String getDownloadsProviderAuthority() {
11471         if (SdkLevel.isAtLeastS()) {
11472             return getDownloadsProviderAuthorityFromDocumentsContract();
11473         }
11474         return DOWNLOADS_PROVIDER_AUTHORITY;
11475     }
11476 
11477     @RequiresApi(Build.VERSION_CODES.S)
11478     private String getDownloadsProviderAuthorityFromDocumentsContract() {
11479         return DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
11480     }
11481 
11482     private boolean isCallingIdentityDownloadProvider() {
11483         return UserHandle.getAppId(getCallingUidOrSelf()) == mDownloadsAuthorityAppId;
11484     }
11485 
11486     private boolean isCallingIdentityExternalStorageProvider() {
11487         return UserHandle.getAppId(getCallingUidOrSelf()) == mExternalStorageAuthorityAppId;
11488     }
11489 
11490     private boolean isCallingIdentityMtp() {
11491         return mCallingIdentity.get().hasPermission(PERMISSION_ACCESS_MTP);
11492     }
11493 
11494     /**
11495      * The following apps have access to all private-app directories on secondary volumes:
11496      *    * ExternalStorageProvider
11497      *    * DownloadProvider
11498      *    * Signature apps with ACCESS_MTP permission granted
11499      *      (Note: For Android R we also allow privileged apps with ACCESS_MTP to access all
11500      *      private-app directories, this additional access is removed for Android S+).
11501      *
11502      * Installer apps can only access private-app directories on Android/obb.
11503      *
11504      * @param relativePath the relative path of the file to access
11505      */
11506     private boolean isCallingIdentityAllowedSpecialPrivatePathAccess(String relativePath) {
11507         if (SdkLevel.isAtLeastS()) {
11508             return isMountModeAllowedPrivatePathAccess(getCallingUidOrSelf(), getCallingPackage(),
11509                     relativePath);
11510         } else {
11511             if (isCallingIdentityDownloadProvider() ||
11512                     isCallingIdentityExternalStorageProvider() || isCallingIdentityMtp()) {
11513                 return true;
11514             }
11515             return (isObbOrChildRelativePath(relativePath) &&
11516                     isCallingIdentityAllowedInstallerAccess());
11517         }
11518     }
11519 
11520     @RequiresApi(Build.VERSION_CODES.S)
11521     private boolean isMountModeAllowedPrivatePathAccess(int uid, String packageName,
11522             String relativePath) {
11523         // This is required as only MediaProvider (package with WRITE_MEDIA_STORAGE) can access
11524         // mount modes.
11525         final CallingIdentity token = clearCallingIdentity();
11526         try {
11527             final int mountMode = mStorageManager.getExternalStorageMountMode(uid, packageName);
11528             switch (mountMode) {
11529                 case StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE:
11530                 case StorageManager.MOUNT_MODE_EXTERNAL_PASS_THROUGH:
11531                     return true;
11532                 case StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER:
11533                     return isObbOrChildRelativePath(relativePath);
11534             }
11535         } catch (Exception e) {
11536             Log.w(TAG, "Caller does not have the permissions to access mount modes: ", e);
11537         } finally {
11538             restoreCallingIdentity(token);
11539         }
11540         return false;
11541     }
11542 
11543     private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
11544         // System internals can work with all media
11545         if (isCallingPackageSelf() || isCallingPackageShell()) {
11546             return true;
11547         }
11548 
11549         // Apps that have permission to manage external storage can work with all files
11550         if (isCallingPackageManager()) {
11551             return true;
11552         }
11553 
11554         // Check if caller is known to be owner of this item, to speed up
11555         // performance of our permission checks
11556         final int table = matchUri(uri, true);
11557         switch (table) {
11558             case AUDIO_MEDIA_ID:
11559             case VIDEO_MEDIA_ID:
11560             case IMAGES_MEDIA_ID:
11561             case FILES_ID:
11562             case DOWNLOADS_ID:
11563                 final long id = ContentUris.parseId(uri);
11564                 if (mCallingIdentity.get().isOwned(id)) {
11565                     return true;
11566                 }
11567                 break;
11568             default:
11569                 // continue below
11570         }
11571 
11572         // Check whether the uri is a specific table or not. Don't allow the global access to these
11573         // table uris
11574         switch (table) {
11575             case AUDIO_MEDIA:
11576             case IMAGES_MEDIA:
11577             case VIDEO_MEDIA:
11578             case DOWNLOADS:
11579             case FILES:
11580             case AUDIO_ALBUMS:
11581             case AUDIO_ARTISTS:
11582             case AUDIO_GENRES:
11583             case AUDIO_PLAYLISTS:
11584                 return false;
11585             default:
11586                 // continue below
11587         }
11588 
11589         // Outstanding grant means they get access
11590         return isUriPermissionGranted(uri, forWrite);
11591     }
11592 
11593     /**
11594      * Returns any uri that is granted from the set of Uris passed.
11595      */
11596     @Nullable
11597     private Uri getPermissionGrantedUri(@NonNull List<Uri> uris, boolean forWrite) {
11598         for (Uri uri : uris) {
11599             if (isUriPermissionGranted(uri, forWrite)) {
11600                 return uri;
11601             }
11602         }
11603         return null;
11604     }
11605 
11606     private boolean isUriPermissionGranted(Uri uri, boolean forWrite) {
11607         final int modeFlags = forWrite
11608                 ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION
11609                 : Intent.FLAG_GRANT_READ_URI_PERMISSION;
11610         int uriPermission = getContext().checkUriPermission(uri, mCallingIdentity.get().pid,
11611                 mCallingIdentity.get().uid, modeFlags);
11612         return uriPermission == PERMISSION_GRANTED;
11613     }
11614 
11615     @VisibleForTesting
11616     public boolean isFuseThread() {
11617         return FuseDaemon.native_is_fuse_thread();
11618     }
11619 
11620 
11621     /**
11622      * Enforce that caller has access to the given {@link Uri}.
11623      *
11624      * @throws SecurityException if access isn't allowed.
11625      */
11626     @VisibleForTesting
11627     protected void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras,
11628             boolean forWrite) {
11629         Trace.beginSection("MP.enforceCallingPermission");
11630         try {
11631             enforceCallingPermissionInternal(uri, extras, forWrite);
11632         } finally {
11633             Trace.endSection();
11634         }
11635     }
11636 
11637     private void enforceCallingPermission(@NonNull Collection<Uri> uris, boolean forWrite) {
11638         for (Uri uri : uris) {
11639             enforceCallingPermission(uri, Bundle.EMPTY, forWrite);
11640         }
11641     }
11642 
11643     private void enforceCallingPermissionInternal(@NonNull Uri uri, @NonNull Bundle extras,
11644             boolean forWrite) {
11645         Objects.requireNonNull(uri);
11646         Objects.requireNonNull(extras);
11647 
11648         // Try a simple global check first before falling back to performing a
11649         // simple query to probe for access.
11650         if (checkCallingPermissionGlobal(uri, forWrite)) {
11651             // Access allowed, yay!
11652             return;
11653         }
11654 
11655         // For redacted URI proceed with its corresponding URI as query builder doesn't support
11656         // redacted URIs for fetching a database row
11657         // NOTE: The grants (if any) must have been on redacted URI hence global check requires
11658         // redacted URI
11659         Uri redactedUri = null;
11660         if (isRedactedUri(uri)) {
11661             redactedUri = uri;
11662             uri = getUriForRedactedUri(uri);
11663         }
11664 
11665         final DatabaseHelper helper;
11666         try {
11667             helper = getDatabaseForUri(uri);
11668         } catch (VolumeNotFoundException e) {
11669             throw e.rethrowAsIllegalArgumentException();
11670         }
11671 
11672         final boolean allowHidden = isCallingPackageAllowedHidden();
11673         final int table = matchUri(uri, allowHidden);
11674 
11675         final String selection = extras.getString(QUERY_ARG_SQL_SELECTION);
11676         final String[] selectionArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);
11677 
11678         // First, check to see if caller has direct write access
11679         if (forWrite) {
11680             final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, null);
11681             qb.allowColumn(SQLiteQueryBuilder.ROWID_COLUMN);
11682             try (Cursor c = qb.query(helper, new String[] { SQLiteQueryBuilder.ROWID_COLUMN },
11683                     selection, selectionArgs, null, null, null, null, null)) {
11684                 if (c.moveToFirst()) {
11685                     // Direct write access granted, yay!
11686                     return;
11687                 }
11688             }
11689         }
11690 
11691         // We only allow the user to grant access to specific media items in
11692         // strongly typed collections; never to broad collections
11693         boolean allowUserGrant = false;
11694         final int matchUri = matchUri(uri, true);
11695         switch (matchUri) {
11696             case IMAGES_MEDIA_ID:
11697             case AUDIO_MEDIA_ID:
11698             case VIDEO_MEDIA_ID:
11699                 allowUserGrant = true;
11700                 break;
11701         }
11702 
11703         // Second, check to see if caller has direct read access
11704         final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null);
11705         qb.allowColumn(SQLiteQueryBuilder.ROWID_COLUMN);
11706         try (Cursor c = qb.query(helper, new String[] { SQLiteQueryBuilder.ROWID_COLUMN },
11707                 selection, selectionArgs, null, null, null, null, null)) {
11708             if (c.moveToFirst()) {
11709                 if (!forWrite) {
11710                     // Direct read access granted, yay!
11711                     return;
11712                 } else if (allowUserGrant) {
11713                     // Caller has read access, but they wanted to write, and
11714                     // they'll need to get the user to grant that access
11715                     final Context context = getContext();
11716                     final Collection<Uri> uris = Collections.singletonList(uri);
11717                     final PendingIntent intent = MediaStore
11718                             .createWriteRequest(ContentResolver.wrap(this), uris);
11719 
11720                     final Icon icon = getCollectionIcon(uri);
11721                     final RemoteAction action = new RemoteAction(icon,
11722                             context.getText(R.string.permission_required_action),
11723                             context.getText(R.string.permission_required_action),
11724                             intent);
11725 
11726                     throw new RecoverableSecurityException(new SecurityException(
11727                             getCallingPackageOrSelf() + " has no access to " + uri),
11728                             context.getText(R.string.permission_required), action);
11729                 }
11730             }
11731         }
11732 
11733         if (redactedUri != null) uri = redactedUri;
11734         throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri);
11735     }
11736 
11737     private Icon getCollectionIcon(Uri uri) {
11738         final PackageManager pm = getContext().getPackageManager();
11739         final String type = uri.getPathSegments().get(1);
11740         final String groupName;
11741         switch (type) {
11742             default: groupName = android.Manifest.permission_group.STORAGE; break;
11743         }
11744         try {
11745             final PermissionGroupInfo perm = pm.getPermissionGroupInfo(groupName, 0);
11746             return Icon.createWithResource(perm.packageName, perm.icon);
11747         } catch (NameNotFoundException e) {
11748             throw new RuntimeException(e);
11749         }
11750     }
11751 
11752     private void checkAccess(@NonNull Uri uri, @NonNull Bundle extras, @NonNull File file,
11753             boolean isWrite) throws FileNotFoundException {
11754         // First, does caller have the needed row-level access?
11755         enforceCallingPermission(uri, extras, isWrite);
11756 
11757         // Second, does the path look consistent?
11758         if (!FileUtils.contains(Environment.getStorageDirectory(), file)) {
11759             checkWorldReadAccess(file.getAbsolutePath());
11760         }
11761     }
11762 
11763     /**
11764      * Check whether the path is a world-readable file
11765      */
11766     @VisibleForTesting
11767     public static void checkWorldReadAccess(String path) throws FileNotFoundException {
11768         // Path has already been canonicalized, and we relax the check to look
11769         // at groups to support runtime storage permissions.
11770         final int accessBits = path.startsWith("/storage/") ? OsConstants.S_IRGRP
11771                 : OsConstants.S_IROTH;
11772         try {
11773             StructStat stat = Os.stat(path);
11774             if (OsConstants.S_ISREG(stat.st_mode) &&
11775                 ((stat.st_mode & accessBits) == accessBits)) {
11776                 checkLeadingPathComponentsWorldExecutable(path);
11777                 return;
11778             }
11779         } catch (ErrnoException e) {
11780             // couldn't stat the file, either it doesn't exist or isn't
11781             // accessible to us
11782         }
11783 
11784         throw new FileNotFoundException("Can't access " + path);
11785     }
11786 
11787     private static void checkLeadingPathComponentsWorldExecutable(String filePath)
11788             throws FileNotFoundException {
11789         File parent = new File(filePath).getParentFile();
11790 
11791         // Path has already been canonicalized, and we relax the check to look
11792         // at groups to support runtime storage permissions.
11793         final int accessBits = filePath.startsWith("/storage/") ? OsConstants.S_IXGRP
11794                 : OsConstants.S_IXOTH;
11795 
11796         while (parent != null) {
11797             if (! parent.exists()) {
11798                 // parent dir doesn't exist, give up
11799                 throw new FileNotFoundException("access denied");
11800             }
11801             try {
11802                 StructStat stat = Os.stat(parent.getPath());
11803                 if ((stat.st_mode & accessBits) != accessBits) {
11804                     // the parent dir doesn't have the appropriate access
11805                     throw new FileNotFoundException("Can't access " + filePath);
11806                 }
11807             } catch (ErrnoException e1) {
11808                 // couldn't stat() parent
11809                 throw new FileNotFoundException("Can't access " + filePath);
11810             }
11811             parent = parent.getParentFile();
11812         }
11813     }
11814 
11815     @VisibleForTesting
11816     static class FallbackException extends Exception {
11817         private final int mThrowSdkVersion;
11818 
11819         public FallbackException(String message, int throwSdkVersion) {
11820             super(message);
11821             mThrowSdkVersion = throwSdkVersion;
11822         }
11823 
11824         public FallbackException(String message, Throwable cause, int throwSdkVersion) {
11825             super(message, cause);
11826             mThrowSdkVersion = throwSdkVersion;
11827         }
11828 
11829         @Override
11830         public String getMessage() {
11831             if (getCause() != null) {
11832                 return super.getMessage() + ": " + getCause().getMessage();
11833             } else {
11834                 return super.getMessage();
11835             }
11836         }
11837 
11838         public IllegalArgumentException rethrowAsIllegalArgumentException() {
11839             throw new IllegalArgumentException(getMessage());
11840         }
11841 
11842         public Cursor translateForQuery(int targetSdkVersion) {
11843             if (targetSdkVersion >= mThrowSdkVersion) {
11844                 throw new IllegalArgumentException(getMessage());
11845             } else {
11846                 Log.w(TAG, getMessage());
11847                 return null;
11848             }
11849         }
11850 
11851         public Uri translateForInsert(int targetSdkVersion) {
11852             if (targetSdkVersion >= mThrowSdkVersion) {
11853                 throw new IllegalArgumentException(getMessage());
11854             } else {
11855                 Log.w(TAG, getMessage());
11856                 return null;
11857             }
11858         }
11859 
11860         public int translateForBulkInsert(int targetSdkVersion) {
11861             if (targetSdkVersion >= mThrowSdkVersion) {
11862                 throw new IllegalArgumentException(getMessage());
11863             } else {
11864                 Log.w(TAG, getMessage());
11865                 return 0;
11866             }
11867         }
11868 
11869         public int translateForUpdateDelete(int targetSdkVersion) {
11870             if (targetSdkVersion >= mThrowSdkVersion) {
11871                 throw new IllegalArgumentException(getMessage());
11872             } else {
11873                 Log.w(TAG, getMessage());
11874                 return 0;
11875             }
11876         }
11877     }
11878 
11879     @VisibleForTesting
11880     static class VolumeNotFoundException extends FallbackException {
11881         public VolumeNotFoundException(String volumeName) {
11882             super("Volume " + volumeName + " not found", Build.VERSION_CODES.Q);
11883         }
11884     }
11885 
11886     @VisibleForTesting
11887     static class VolumeArgumentException extends FallbackException {
11888         public VolumeArgumentException(File actual, Collection<File> allowed) {
11889             super("Requested path " + actual + " doesn't appear under " + allowed,
11890                     Build.VERSION_CODES.Q);
11891         }
11892     }
11893 
11894     public List<String> getSupportedTranscodingRelativePaths() {
11895         return mTranscodeHelper.getSupportedRelativePaths();
11896     }
11897 
11898     public List<String> getSupportedUncachedRelativePaths() {
11899         return StringUtils.verifySupportedUncachedRelativePaths(
11900                        StringUtils.getStringArrayConfig(getContext(),
11901                                R.array.config_supported_uncached_relative_paths));
11902     }
11903 
11904     /**
11905      * Creating a new method for Transcoding to avoid any merge conflicts.
11906      * TODO(b/170465810): Remove this when the code is refactored.
11907      */
11908     @NonNull DatabaseHelper getDatabaseForUriForTranscoding(Uri uri)
11909             throws VolumeNotFoundException {
11910         return getDatabaseForUri(uri);
11911     }
11912 
11913     @NonNull
11914     private DatabaseHelper getDatabaseForUri(Uri uri) throws VolumeNotFoundException {
11915         final String volumeName = resolveVolumeName(uri);
11916         synchronized (mAttachedVolumes) {
11917             boolean volumeAttached = false;
11918             UserHandle user = mCallingIdentity.get().getUser();
11919             for (MediaVolume vol : mAttachedVolumes) {
11920                 if (vol.getName().equals(volumeName)
11921                         && (vol.isVisibleToUser(user) || vol.isPublicVolume()) ) {
11922                     volumeAttached = true;
11923                     break;
11924                 }
11925             }
11926             if (!volumeAttached) {
11927                 // Dump some more debug info
11928                 Log.e(TAG, "Volume " + volumeName + " not found, calling identity: "
11929                         + user + ", attached volumes: " + mAttachedVolumes);
11930                 throw new VolumeNotFoundException(volumeName);
11931             }
11932         }
11933         if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
11934             return mInternalDatabase;
11935         } else {
11936             return mExternalDatabase;
11937         }
11938     }
11939 
11940     static boolean isMediaDatabaseName(String name) {
11941         if (INTERNAL_DATABASE_NAME.equals(name)) {
11942             return true;
11943         }
11944         if (EXTERNAL_DATABASE_NAME.equals(name)) {
11945             return true;
11946         }
11947         return name.startsWith("external-") && name.endsWith(".db");
11948     }
11949 
11950     @NonNull
11951     private Uri getBaseContentUri(@NonNull String volumeName) {
11952         return MediaStore.AUTHORITY_URI.buildUpon().appendPath(volumeName).build();
11953     }
11954 
11955     public Uri attachVolume(MediaVolume volume, boolean validate, String volumeState) {
11956         Log.v(TAG, "attachVolume() called for " + volume.getName() + " with state:" + volumeState);
11957         if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
11958             throw new SecurityException(
11959                     "Opening and closing databases not allowed.");
11960         }
11961 
11962         final String volumeName = volume.getName();
11963 
11964         // Quick check for shady volume names
11965         MediaStore.checkArgumentVolumeName(volumeName);
11966 
11967         // Quick check that volume actually exists
11968         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName) && validate) {
11969             try {
11970                 getVolumePath(volumeName);
11971             } catch (IOException e) {
11972                 throw new IllegalArgumentException(
11973                         "Volume " + volume + " currently unavailable", e);
11974             }
11975         }
11976 
11977         synchronized (mAttachedVolumes) {
11978             mAttachedVolumes.add(volume);
11979         }
11980 
11981         final ContentResolver resolver = getContext().getContentResolver();
11982         final Uri uri = getBaseContentUri(volumeName);
11983         // TODO(b/182396009) we probably also want to notify clone profile (and vice versa)
11984         ForegroundThread.getExecutor().execute(() -> {
11985             resolver.notifyChange(getBaseContentUri(volumeName), null);
11986         });
11987 
11988         if (LOGV) Log.v(TAG, "Attached volume: " + volume);
11989         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
11990             ForegroundThread.getExecutor().execute(() -> {
11991                 // Also notify on synthetic view of all devices
11992                 resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
11993                 mExternalDatabase.runWithTransaction((db) -> {
11994                     ensureNecessaryFolders(volume, db);
11995                     return null;
11996                 });
11997 
11998                 // We just finished the database operation above, we know that
11999                 // it's ready to answer queries, so notify our DocumentProvider
12000                 // so it can answer queries without risking ANR
12001                 MediaDocumentsProvider.onMediaStoreReady(getContext());
12002             });
12003         }
12004 
12005         if (Environment.MEDIA_MOUNTED.equalsIgnoreCase(volumeState)) {
12006             mDatabaseBackupAndRecovery.setupVolumeDbBackupAndRecovery(volume.getName());
12007         }
12008 
12009         return uri;
12010     }
12011 
12012     private void detachVolume(Uri uri) {
12013         final String volumeName = MediaStore.getVolumeName(uri);
12014         try {
12015             detachVolume(getVolume(volumeName));
12016         } catch (FileNotFoundException e) {
12017             Log.e(TAG, "Couldn't find volume for URI " + uri, e) ;
12018         }
12019     }
12020 
12021     public boolean isVolumeAttached(MediaVolume volume) {
12022         synchronized (mAttachedVolumes) {
12023             return mAttachedVolumes.contains(volume);
12024         }
12025     }
12026 
12027     public void detachVolume(MediaVolume volume) {
12028         Log.v(TAG, "detachVolume() received for " + volume.getName());
12029         if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
12030             throw new SecurityException(
12031                     "Opening and closing databases not allowed.");
12032         }
12033 
12034         final String volumeName = volume.getName();
12035 
12036         // Quick check for shady volume names
12037         MediaStore.checkArgumentVolumeName(volumeName);
12038 
12039         if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
12040             throw new UnsupportedOperationException(
12041                     "Deleting the internal volume is not allowed");
12042         }
12043 
12044         mDatabaseBackupAndRecovery.onDetachVolume(volumeName);
12045         // Signal any scanning to shut down
12046         mMediaScanner.onDetachVolume(volume);
12047 
12048         synchronized (mAttachedVolumes) {
12049             mAttachedVolumes.remove(volume);
12050         }
12051 
12052         final ContentResolver resolver = getContext().getContentResolver();
12053         ForegroundThread.getExecutor().execute(() -> {
12054             resolver.notifyChange(getBaseContentUri(volumeName), null);
12055         });
12056 
12057         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
12058             ForegroundThread.getExecutor().execute(() -> {
12059                 // Also notify on synthetic view of all devices
12060                 resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
12061             });
12062         }
12063 
12064         if (LOGV) Log.v(TAG, "Detached volume: " + volumeName);
12065     }
12066 
12067     private void ensureNecessaryFolders(MediaVolume volume, SQLiteDatabase db) {
12068         ensureDefaultFolders(volume, db);
12069         ensureThumbnailsValid(volume, db);
12070 
12071         // Create redacted directories
12072         if (MediaStore.VOLUME_EXTERNAL_PRIMARY.equalsIgnoreCase(volume.getName())) {
12073             // Create dir for redacted and picker URI paths.
12074             File redactedRelativePath = buildPrimaryVolumeFile(uidToUserId(MY_UID),
12075                     getRedactedRelativePath());
12076             if (!redactedRelativePath.exists() && !redactedRelativePath.mkdirs()) {
12077                 // We should always be able to create these directories from MediaProvider
12078                 Log.wtf(TAG, "Couldn't create redacted path for " + UserHandle.myUserId());
12079             }
12080         }
12081     }
12082 
12083     @GuardedBy("mAttachedVolumes")
12084     private final ArraySet<MediaVolume> mAttachedVolumes = new ArraySet<>();
12085     @GuardedBy("mCustomCollators")
12086     private final ArraySet<String> mCustomCollators = new ArraySet<>();
12087 
12088     private MediaScanner mMediaScanner;
12089 
12090     private ProjectionHelper mProjectionHelper;
12091     private DatabaseHelper mInternalDatabase;
12092     private DatabaseHelper mExternalDatabase;
12093     private PickerDbFacade mPickerDbFacade;
12094     private ExternalDbFacade mExternalDbFacade;
12095     private PickerDataLayer mPickerDataLayer;
12096     private ConfigStore mConfigStore;
12097     private PickerSyncController mPickerSyncController;
12098     private TranscodeHelper mTranscodeHelper;
12099     private PhotoPickerTranscodeHelper mPhotoPickerTranscodeHelper;
12100     private MediaGrants mMediaGrants;
12101     private FilesOwnershipUtils mFilesOwnershipUtils;
12102     private DatabaseBackupAndRecovery mDatabaseBackupAndRecovery;
12103 
12104     private BackupExecutor mExternalPrimaryBackupExecutor;
12105 
12106     // name of the volume currently being scanned by the media scanner (or null)
12107     private String mMediaScannerVolume;
12108 
12109 
12110     private static final HashSet<Integer> REDACTED_URI_SUPPORTED_TYPES = new HashSet<>(
12111             Arrays.asList(AUDIO_MEDIA_ID, IMAGES_MEDIA_ID, VIDEO_MEDIA_ID, FILES_ID, DOWNLOADS_ID));
12112 
12113     private LocalUriMatcher mUriMatcher;
12114 
12115     private int matchUri(Uri uri, boolean allowHidden) {
12116         return mUriMatcher.matchUri(uri, allowHidden);
12117     }
12118 
12119 
12120 
12121     /**
12122      * Set of columns that can be safely mutated by external callers; all other
12123      * columns are treated as read-only, since they reflect what the media
12124      * scanner found on disk, and any mutations would be overwritten the next
12125      * time the media was scanned.
12126      */
12127     private static final ArraySet<String> sMutableColumns = new ArraySet<>();
12128 
12129     static {
12130         sMutableColumns.add(MediaStore.MediaColumns.DATA);
12131         sMutableColumns.add(MediaStore.MediaColumns.RELATIVE_PATH);
12132         sMutableColumns.add(MediaStore.MediaColumns.DISPLAY_NAME);
12133         sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING);
12134         sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED);
12135         sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE);
12136         sMutableColumns.add(MediaStore.MediaColumns.OWNER_PACKAGE_NAME);
12137 
12138         sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK);
12139 
12140         sMutableColumns.add(MediaStore.Video.VideoColumns.TAGS);
12141         sMutableColumns.add(MediaStore.Video.VideoColumns.CATEGORY);
12142         sMutableColumns.add(MediaStore.Video.VideoColumns.BOOKMARK);
12143 
12144         sMutableColumns.add(MediaStore.Audio.Playlists.NAME);
12145         sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID);
12146         sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
12147 
12148         sMutableColumns.add(MediaStore.DownloadColumns.DOWNLOAD_URI);
12149         sMutableColumns.add(MediaStore.DownloadColumns.REFERER_URI);
12150 
12151         sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE);
12152         sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE);
12153 
12154         sMutableColumns.add(MediaStore.Files.FileColumns.OEM_METADATA);
12155     }
12156 
12157     /**
12158      * Set of columns that affect placement of files on disk.
12159      */
12160     private static final ArraySet<String> sPlacementColumns = new ArraySet<>();
12161 
12162     static {
12163         sPlacementColumns.add(MediaStore.MediaColumns.DATA);
12164         sPlacementColumns.add(MediaStore.MediaColumns.RELATIVE_PATH);
12165         sPlacementColumns.add(MediaStore.MediaColumns.DISPLAY_NAME);
12166         sPlacementColumns.add(MediaStore.MediaColumns.MIME_TYPE);
12167         sPlacementColumns.add(MediaStore.MediaColumns.IS_PENDING);
12168         sPlacementColumns.add(MediaStore.MediaColumns.IS_TRASHED);
12169         sPlacementColumns.add(MediaStore.MediaColumns.DATE_EXPIRES);
12170     }
12171 
12172     /**
12173      * List of abusive custom columns that we're willing to allow via
12174      * {@link SQLiteQueryBuilder#setProjectionAllowlist(Collection)}.
12175      */
12176     static final ArrayList<Pattern> sAllowlist = new ArrayList<>();
12177 
12178     private static void addAllowlistPattern(String pattern) {
12179         sAllowlist.add(Pattern.compile(" *" + pattern + " *"));
12180     }
12181 
12182     static {
12183         final String maybeAs = "( (as )?[_a-z0-9]+)?";
12184         addAllowlistPattern("(?i)[_a-z0-9]+" + maybeAs);
12185         addAllowlistPattern("audio\\._id AS _id");
12186         addAllowlistPattern(
12187                 "(?i)(min|max|sum|avg|total|count|cast)\\(([_a-z0-9]+"
12188                         + maybeAs
12189                         + "|\\*)\\)"
12190                         + maybeAs);
12191         addAllowlistPattern(
12192                 "case when case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added"
12193                     + " \\* \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then"
12194                     + " date_added when \\(date_added >= \\d+ and date_added < \\d+\\) then"
12195                     + " date_added / \\d+ else \\d+ end > case when \\(date_modified >= \\d+ and"
12196                     + " date_modified < \\d+\\) then date_modified \\* \\d+ when \\(date_modified"
12197                     + " >= \\d+ and date_modified < \\d+\\) then date_modified when"
12198                     + " \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified /"
12199                     + " \\d+ else \\d+ end then case when \\(date_added >= \\d+ and date_added <"
12200                     + " \\d+\\) then date_added \\* \\d+ when \\(date_added >= \\d+ and date_added"
12201                     + " < \\d+\\) then date_added when \\(date_added >= \\d+ and date_added <"
12202                     + " \\d+\\) then date_added / \\d+ else \\d+ end else case when"
12203                     + " \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified \\*"
12204                     + " \\d+ when \\(date_modified >= \\d+ and date_modified < \\d+\\) then"
12205                     + " date_modified when \\(date_modified >= \\d+ and date_modified < \\d+\\)"
12206                     + " then date_modified / \\d+ else \\d+ end end as corrected_added_modified");
12207         addAllowlistPattern(
12208                 "MAX\\(case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\*"
12209                     + " \\d+ when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when"
12210                     + " \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else"
12211                     + " \\d+ end\\)");
12212         addAllowlistPattern(
12213                 "MAX\\(case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added \\*"
12214                     + " \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added"
12215                     + " when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added / \\d+"
12216                     + " else \\d+ end\\)");
12217         addAllowlistPattern(
12218                 "MAX\\(case when \\(date_modified >= \\d+ and date_modified < \\d+\\) then"
12219                     + " date_modified \\* \\d+ when \\(date_modified >= \\d+ and date_modified <"
12220                     + " \\d+\\) then date_modified when \\(date_modified >= \\d+ and date_modified"
12221                     + " < \\d+\\) then date_modified / \\d+ else \\d+ end\\)");
12222         addAllowlistPattern("\"content://media/[a-z]+/audio/media\"");
12223         addAllowlistPattern(
12224                 "substr\\(_data, length\\(_data\\)-length\\(_display_name\\), 1\\) as"
12225                     + " filename_prevchar");
12226         addAllowlistPattern("\\*" + maybeAs);
12227         addAllowlistPattern(
12228                 "case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\* \\d+"
12229                     + " when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when"
12230                     + " \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else"
12231                     + " \\d+ end");
12232     }
12233 
12234     public ArrayMap<String, String> getProjectionMap(Class<?>... clazzes) {
12235         return mProjectionHelper.getProjectionMap(clazzes);
12236     }
12237 
12238     static <T> boolean containsAny(Set<T> a, Set<T> b) {
12239         for (T i : b) {
12240             if (a.contains(i)) {
12241                 return true;
12242             }
12243         }
12244         return false;
12245     }
12246 
12247     @VisibleForTesting
12248     @Nullable
12249     static Uri computeCommonPrefix(@NonNull List<Uri> uris) {
12250         if (uris.isEmpty()) return null;
12251 
12252         final Uri base = uris.get(0);
12253         final List<String> basePath = new ArrayList<>(base.getPathSegments());
12254         for (int i = 1; i < uris.size(); i++) {
12255             final List<String> probePath = uris.get(i).getPathSegments();
12256             for (int j = 0; j < basePath.size() && j < probePath.size(); j++) {
12257                 if (!Objects.equals(basePath.get(j), probePath.get(j))) {
12258                     // Trim away all remaining common elements
12259                     while (basePath.size() > j) {
12260                         basePath.remove(j);
12261                     }
12262                 }
12263             }
12264 
12265             final int probeSize = probePath.size();
12266             while (basePath.size() > probeSize) {
12267                 basePath.remove(probeSize);
12268             }
12269         }
12270 
12271         final Uri.Builder builder = base.buildUpon().path(null);
12272         for (String s : basePath) {
12273             builder.appendPath(s);
12274         }
12275         return builder.build();
12276     }
12277 
12278     public ExternalDbFacade getExternalDbFacade() {
12279         return mExternalDbFacade;
12280     }
12281 
12282     public PickerSyncController getPickerSyncController() {
12283         return mPickerSyncController;
12284     }
12285 
12286     private boolean isCallingPackageSystemGallery() {
12287         if (mCallingIdentity.get().hasPermission(PERMISSION_IS_SYSTEM_GALLERY)) {
12288             if (isCallingPackageRequestingLegacy()) {
12289                 return isCallingPackageLegacyWrite();
12290             }
12291             return true;
12292         }
12293         return false;
12294     }
12295 
12296     private int getCallingUidOrSelf() {
12297         return mCallingIdentity.get().uid;
12298     }
12299 
12300     private boolean isCallerPhotoPicker() {
12301         try {
12302             return PermissionUtils.checkManageCloudMediaProvidersPermission(
12303                     getContext(),
12304                     mCallingIdentity.get().pid,
12305                     mCallingIdentity.get().uid
12306             );
12307         } catch (RuntimeException e) {
12308             Log.e(TAG, "Could not check MANAGE_CLOUD_MEDIA_PROVIDERS_PERMISSION permission", e);
12309             return false;
12310         }
12311     }
12312 
12313     @Deprecated
12314     private String getCallingPackageOrSelf() {
12315         return mCallingIdentity.get().getPackageName();
12316     }
12317 
12318     @Deprecated
12319     @VisibleForTesting
12320     public int getCallingPackageTargetSdkVersion() {
12321         return mCallingIdentity.get().getTargetSdkVersion();
12322     }
12323 
12324     @Deprecated
12325     private boolean isCallingPackageAllowedHidden() {
12326         return isCallingPackageSelf();
12327     }
12328 
12329     @Deprecated
12330     private boolean isCallingPackageSelf() {
12331         return mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF);
12332     }
12333 
12334     @Deprecated
12335     private boolean isCallingPackageShell() {
12336         return mCallingIdentity.get().hasPermission(PERMISSION_IS_SHELL);
12337     }
12338 
12339     @Deprecated
12340     private boolean isCallingPackageManager() {
12341         return mCallingIdentity.get().hasPermission(PERMISSION_IS_MANAGER);
12342     }
12343 
12344     @Deprecated
12345     private boolean isCallingPackageDelegator() {
12346         return mCallingIdentity.get().hasPermission(PERMISSION_IS_DELEGATOR);
12347     }
12348 
12349     @Deprecated
12350     private boolean isCallingPackageLegacyRead() {
12351         return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_READ);
12352     }
12353 
12354     @Deprecated
12355     private boolean isCallingPackageLegacyWrite() {
12356         return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE);
12357     }
12358 
12359     @Override
12360     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
12361         writer.println("mThumbSize=" + mThumbSize);
12362         synchronized (mAttachedVolumes) {
12363             writer.println("mAttachedVolumes=" + mAttachedVolumes);
12364         }
12365         writer.println();
12366 
12367         mVolumeCache.dump(writer);
12368         writer.println();
12369 
12370         mUserCache.dump(writer);
12371         writer.println();
12372 
12373         mTranscodeHelper.dump(writer);
12374         writer.println();
12375 
12376         mConfigStore.dump(writer);
12377         writer.println();
12378 
12379         mPickerDbFacade.dump(writer);
12380         writer.println();
12381 
12382         mPickerSyncController.dump(writer);
12383         writer.println();
12384 
12385         dumpAccessLogs(writer);
12386         writer.println();
12387 
12388         Logging.dumpPersistent(writer);
12389     }
12390 
12391     private void dumpAccessLogs(PrintWriter writer) {
12392         synchronized (mCachedCallingIdentityForFuse) {
12393             for (int i = 0; i < mCachedCallingIdentityForFuse.size(); i++) {
12394                 mCachedCallingIdentityForFuse.valueAt(i).dump(writer);
12395             }
12396         }
12397     }
12398 
12399     /**
12400      * Replaces "external" in the URI path with the specified volumeName.
12401      * Example:
12402      * Input: content://media/external/images/media/1232
12403      * Output: content://media/{volumeName}/images/media/1232
12404      */
12405     private Uri replaceExternalUriWithVolumeName(Uri uri, String volumeName) {
12406         List<String> pathSegments = uri.getPathSegments();
12407         if (!pathSegments.isEmpty() && pathSegments.get(0).equalsIgnoreCase(
12408                 MediaStore.VOLUME_EXTERNAL)) {
12409             List<String> updatedSegments = new ArrayList<>(pathSegments);
12410             updatedSegments.set(0, volumeName);
12411             return uri.buildUpon()
12412                     .path(TextUtils.join("/", updatedSegments))
12413                     .build();
12414         }
12415         return uri;
12416     }
12417 
12418     /**
12419      * Called once - from {@link #onCreate()}.
12420      */
12421     @NonNull
12422     private ConfigStore createConfigStore() {
12423         // Tests may want override provideConfigStore() in order to inject a mock object.
12424         ConfigStore configStore = provideConfigStore();
12425         if (configStore == null) {
12426             // Tests did not provide an alternative implementation: create our regular "production"
12427             // ConfigStore.
12428             configStore = MediaApplication.getConfigStore();
12429         }
12430         return configStore;
12431     }
12432 
12433     /**
12434      * Initializes the MimeTypeFixHandler for Android 15, running only for Android 15
12435      * This method loads the mime types from the res/raw directory
12436      * This is necessary to ensure that MediaProvider can handle mime types correctly on Android 15
12437      */
12438     private void initializeMimeTypeFixHandlerForAndroid15(Context context) {
12439         if (Build.VERSION.SDK_INT != Build.VERSION_CODES.VANILLA_ICE_CREAM) {
12440             return;
12441         }
12442 
12443         if (!Flags.enableMimeTypeFixForAndroid15()) {
12444             return;
12445         }
12446 
12447         // Load all the MIME types from various files in the background to reduce the latency
12448         // caused when this method is called from onCreate
12449         BackgroundThread.getExecutor().execute(() -> {
12450             try {
12451                 MimeTypeFixHandler.loadMimeTypes(context);
12452             } catch (Exception e) {
12453                 Log.e(TAG, "Failed to initialize MimeTypeFixHandler: ", e);
12454             }
12455         });
12456     }
12457 
12458     /**
12459      * <b>FOT TESTING PURPOSES ONLY</b>
12460      * <p>
12461      * Allows injecting alternative {@link ConfigStore} implementation.
12462      */
12463     @VisibleForTesting
12464     @Nullable
12465     protected ConfigStore provideConfigStore() {
12466         return null;
12467     }
12468 
12469     protected VolumeCache getVolumeCache() {
12470         return mVolumeCache;
12471     }
12472 
12473     protected DatabaseBackupAndRecovery createDatabaseBackupAndRecovery() {
12474         return new DatabaseBackupAndRecovery(mConfigStore, mVolumeCache);
12475     }
12476 
12477     protected MaliciousAppDetector createMaliciousAppDetector() {
12478         return new MaliciousAppDetector(getContext());
12479     }
12480 
12481     protected boolean shouldCheckForMaliciousActivity() {
12482         // Check for malicious activity if not a system gallery app, not the media provider itself,
12483         // and the malicious app detector flag is enabled
12484         if (!SdkLevel.isAtLeastS()) {
12485             return false;
12486         }
12487         return Flags.enableMaliciousAppDetector() && !isCallingPackageSystemGallery()
12488                 && !isCallingPackageSelf();
12489     }
12490 }
12491