blob: 5bc77d04b2a47016b92a04d9df6da4d336c19269 [file] [log] [blame]
The Android Open Source Project70215272009-03-03 19:32:43 -08001/*
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
17package com.android.providers.media;
18
Zim696dea42020-03-07 11:41:42 +000019import static android.Manifest.permission.ACCESS_MEDIA_LOCATION;
20import static android.app.AppOpsManager.permissionToOp;
Jeff Sharkeyc3088d82018-12-11 17:32:51 -070021import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
22import static android.app.PendingIntent.FLAG_IMMUTABLE;
23import static android.app.PendingIntent.FLAG_ONE_SHOT;
Jeff Sharkey9c1bb3d2019-11-14 13:59:41 -070024import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION;
25import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS;
Jeff Sharkey5a8bb562018-08-10 18:04:10 -060026import static android.content.pm.PackageManager.PERMISSION_GRANTED;
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -070027import static android.provider.MediaStore.MATCH_DEFAULT;
28import static android.provider.MediaStore.MATCH_EXCLUDE;
29import static android.provider.MediaStore.MATCH_INCLUDE;
30import static android.provider.MediaStore.MATCH_ONLY;
31import static android.provider.MediaStore.QUERY_ARG_MATCH_FAVORITE;
32import static android.provider.MediaStore.QUERY_ARG_MATCH_PENDING;
33import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED;
Jeff Sharkey61378cb2019-11-23 16:11:09 -070034import static android.provider.MediaStore.QUERY_ARG_RELATED_URI;
Jeff Sharkeyb6781bc2019-07-18 18:45:52 -060035import static android.provider.MediaStore.getVolumeName;
Jeff Sharkey007645e2012-03-08 17:45:12 -080036
Jeff Sharkeyc55994b2019-12-20 19:43:59 -070037import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME;
38import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME;
Abhijeet Kaur3bc15772021-11-17 08:40:34 +000039import static com.android.providers.media.LocalCallingIdentity.APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID;
40import static com.android.providers.media.LocalCallingIdentity.PERMISSION_ACCESS_MTP;
41import static com.android.providers.media.LocalCallingIdentity.PERMISSION_INSTALL_PACKAGES;
Jeff Sharkey8411c402020-04-29 22:12:36 -060042import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_DELEGATOR;
shafik575d0742019-11-25 17:02:57 +000043import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED;
shafik575d0742019-11-25 17:02:57 +000044import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_READ;
Jeff Sharkeyc5c39142019-12-15 22:46:03 -070045import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE;
Jeff Sharkey8411c402020-04-29 22:12:36 -060046import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_MANAGER;
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -060047import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED;
Jeff Sharkey8411c402020-04-29 22:12:36 -060048import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SELF;
49import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SHELL;
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -060050import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_AUDIO;
51import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES;
52import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO;
53import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_AUDIO;
Abhijeet Kaur3bc15772021-11-17 08:40:34 +000054import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_EXTERNAL_STORAGE;
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -060055import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_IMAGES;
56import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_VIDEO;
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -070057import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND;
58import static com.android.providers.media.scan.MediaScanner.REASON_IDLE;
Jeff Sharkeye59ed6b2020-01-11 16:20:00 -070059import static com.android.providers.media.util.DatabaseUtils.bindList;
Sahana Raoea587fc2020-06-03 15:56:23 +010060import static com.android.providers.media.util.FileUtils.PATTERN_PENDING_FILEPATH_FOR_SQL;
Jeff Sharkeye152d5762019-10-11 17:14:51 -060061import static com.android.providers.media.util.FileUtils.extractDisplayName;
62import static com.android.providers.media.util.FileUtils.extractFileName;
Abhijeet Kaur3bc15772021-11-17 08:40:34 +000063import static com.android.providers.media.util.FileUtils.extractOwnerPackageNameFromRelativePath;
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -070064import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName;
65import static com.android.providers.media.util.FileUtils.extractRelativePath;
Abhijeet Kaur3bc15772021-11-17 08:40:34 +000066import static com.android.providers.media.util.FileUtils.extractRelativePathWithDisplayName;
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -070067import static com.android.providers.media.util.FileUtils.extractTopLevelDir;
68import static com.android.providers.media.util.FileUtils.extractVolumeName;
Sahana Raof21671d2020-03-09 16:49:26 +000069import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath;
Ricky Waifeb9d9b2020-04-06 19:14:46 +010070import static com.android.providers.media.util.FileUtils.isDataOrObbPath;
Abhijeet Kaur3bc15772021-11-17 08:40:34 +000071import static com.android.providers.media.util.FileUtils.isDataOrObbRelativePath;
Jeff Sharkey5ea5c282019-12-18 14:06:28 -070072import static com.android.providers.media.util.FileUtils.isDownload;
Abhijeet Kaur3bc15772021-11-17 08:40:34 +000073import static com.android.providers.media.util.FileUtils.isObbOrChildRelativePath;
Sahana Raof21671d2020-03-09 16:49:26 +000074import static com.android.providers.media.util.FileUtils.sanitizePath;
Jeff Sharkeyc55994b2019-12-20 19:43:59 -070075import static com.android.providers.media.util.Logging.LOGV;
76import static com.android.providers.media.util.Logging.TAG;
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -060077
Jeff Sharkey55f76902015-07-24 15:22:08 -070078import android.app.AppOpsManager;
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -060079import android.app.AppOpsManager.OnOpActiveChangedListener;
Zim696dea42020-03-07 11:41:42 +000080import android.app.AppOpsManager.OnOpChangedListener;
Jeff Sharkeye2750322020-01-07 22:06:24 -070081import android.app.DownloadManager;
Jeff Sharkeyc3088d82018-12-11 17:32:51 -070082import android.app.PendingIntent;
83import android.app.RecoverableSecurityException;
84import android.app.RemoteAction;
Abhijeet Kaur3bc15772021-11-17 08:40:34 +000085import android.app.compat.CompatChanges;
86import android.compat.annotation.ChangeId;
87import android.compat.annotation.EnabledAfter;
Owen Linbdd3b832010-07-13 17:53:41 +080088import android.content.BroadcastReceiver;
Jeff Sharkeyeea49d32019-12-11 17:45:38 -070089import android.content.ClipData;
Jeff Sharkey7ea24f22019-08-22 10:14:18 -060090import android.content.ClipDescription;
Owen Linbdd3b832010-07-13 17:53:41 +080091import android.content.ContentProvider;
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -060092import android.content.ContentProviderClient;
Owen Linbdd3b832010-07-13 17:53:41 +080093import android.content.ContentProviderOperation;
94import android.content.ContentProviderResult;
95import android.content.ContentResolver;
96import android.content.ContentUris;
97import android.content.ContentValues;
98import android.content.Context;
99import android.content.Intent;
100import android.content.IntentFilter;
101import android.content.OperationApplicationException;
Mike Lockwooded9bbc42011-01-12 19:32:44 -0500102import android.content.SharedPreferences;
Owen Linbdd3b832010-07-13 17:53:41 +0800103import android.content.UriMatcher;
shafik77ed67b2020-02-06 18:27:35 +0000104import android.content.pm.ApplicationInfo;
Jeff Sharkeybd262742019-12-17 16:40:29 -0700105import android.content.pm.PackageInstaller.SessionInfo;
Marco Nelissen3e6a4282013-08-27 13:49:24 -0700106import android.content.pm.PackageManager;
Marco Nelissen90c7da02012-02-17 09:25:39 -0800107import android.content.pm.PackageManager.NameNotFoundException;
Jeff Sharkeyc3088d82018-12-11 17:32:51 -0700108import android.content.pm.PermissionGroupInfo;
Jeff Sharkey74f73732019-11-12 15:36:32 -0700109import android.content.pm.ProviderInfo;
Jeff Sharkey29421112018-07-27 20:56:44 -0600110import android.content.res.AssetFileDescriptor;
Sean Stout247d9182018-01-23 11:00:37 -0800111import android.content.res.Configuration;
Daniel Lehmann70676502011-01-12 14:45:49 -0800112import android.content.res.Resources;
The Android Open Source Project70215272009-03-03 19:32:43 -0800113import android.database.Cursor;
Marco Nelissen00270192010-01-08 08:35:20 -0800114import android.database.MatrixCursor;
Sahana Rao5b0b9652019-12-31 17:49:25 +0000115import android.database.sqlite.SQLiteConstraintException;
The Android Open Source Project70215272009-03-03 19:32:43 -0800116import android.database.sqlite.SQLiteDatabase;
The Android Open Source Project70215272009-03-03 19:32:43 -0800117import android.graphics.Bitmap;
118import android.graphics.BitmapFactory;
Jeff Sharkeyc3088d82018-12-11 17:32:51 -0700119import android.graphics.drawable.Icon;
Jeff Sharkey66881302019-10-05 10:50:06 -0600120import android.icu.util.ULocale;
Jeff Sharkey55d5bd92018-12-01 18:26:52 -0700121import android.media.ExifInterface;
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -0700122import android.media.ThumbnailUtils;
Mike Lockwood90345782010-12-30 11:52:26 -0500123import android.mtp.MtpConstants;
The Android Open Source Project70215272009-03-03 19:32:43 -0800124import android.net.Uri;
125import android.os.Binder;
Jeff Sharkey43913322019-12-16 16:28:02 -0700126import android.os.Binder.ProxyTransactListener;
Jean-Michel Trivi880dce92016-09-14 12:58:11 -0700127import android.os.Build;
Marco Nelissen38b43642012-01-27 09:40:07 -0800128import android.os.Bundle;
Jeff Sharkey29421112018-07-27 20:56:44 -0600129import android.os.CancellationSignal;
The Android Open Source Project70215272009-03-03 19:32:43 -0800130import android.os.Environment;
Jeff Sharkeyf63882a2018-01-04 15:08:09 -0700131import android.os.IBinder;
The Android Open Source Project70215272009-03-03 19:32:43 -0800132import android.os.ParcelFileDescriptor;
Jeff Sharkey29421112018-07-27 20:56:44 -0600133import android.os.ParcelFileDescriptor.OnCloseListener;
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -0600134import android.os.RemoteException;
Marco Nelissen10af34f2011-12-16 17:59:52 -0800135import android.os.SystemClock;
Nandana Dutt4f5e15a2019-11-29 10:45:58 +0000136import android.os.SystemProperties;
Jeff Sharkey031af8d2019-04-28 11:11:30 -0600137import android.os.Trace;
Jeff Sharkeyf63882a2018-01-04 15:08:09 -0700138import android.os.UserHandle;
Mike Lockwoodc47e4f22011-05-09 19:08:21 -0700139import android.os.storage.StorageManager;
Jeff Sharkey8411c402020-04-29 22:12:36 -0600140import android.os.storage.StorageManager.StorageVolumeCallback;
Mike Lockwood1f3014a2011-05-23 19:42:01 -0400141import android.os.storage.StorageVolume;
Mike Lockwooded9bbc42011-01-12 19:32:44 -0500142import android.preference.PreferenceManager;
Jeff Sharkeya57867a2019-02-14 13:27:35 -0700143import android.provider.BaseColumns;
144import android.provider.Column;
The Android Open Source Project70215272009-03-03 19:32:43 -0800145import android.provider.MediaStore;
146import android.provider.MediaStore.Audio;
Jeff Sharkey6378ccb2019-03-20 13:47:36 -0600147import android.provider.MediaStore.Audio.AudioColumns;
Marco Nelissen4eff7fe2012-04-06 12:41:41 -0700148import android.provider.MediaStore.Audio.Playlists;
Sudheer Shanka56cba322018-12-07 10:55:58 -0800149import android.provider.MediaStore.Downloads;
Owen Lina2466a72012-04-16 17:59:00 +0800150import android.provider.MediaStore.Files;
Daniel Lehmann70676502011-01-12 14:45:49 -0800151import android.provider.MediaStore.Files.FileColumns;
The Android Open Source Project70215272009-03-03 19:32:43 -0800152import android.provider.MediaStore.Images;
Daniel Lehmann70676502011-01-12 14:45:49 -0800153import android.provider.MediaStore.Images.ImageColumns;
The Android Open Source Project70215272009-03-03 19:32:43 -0800154import android.provider.MediaStore.MediaColumns;
155import android.provider.MediaStore.Video;
Elliott Hughesf3b67d52014-04-28 11:42:08 -0700156import android.system.ErrnoException;
157import android.system.Os;
158import android.system.OsConstants;
159import android.system.StructStat;
The Android Open Source Project70215272009-03-03 19:32:43 -0800160import android.text.TextUtils;
Marco Nelissen10af34f2011-12-16 17:59:52 -0800161import android.text.format.DateUtils;
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -0600162import android.util.ArrayMap;
Jeff Sharkey0a0caad2018-08-10 14:07:57 -0600163import android.util.ArraySet;
Jeff Sharkey5f9c0792019-01-26 13:52:03 -0700164import android.util.DisplayMetrics;
The Android Open Source Project70215272009-03-03 19:32:43 -0800165import android.util.Log;
Sudheer Shanka1a79ac02019-01-17 13:14:52 -0800166import android.util.LongSparseArray;
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -0700167import android.util.Size;
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600168import android.util.SparseArray;
Sahana Raob105c222020-06-17 20:18:48 +0100169import android.webkit.MimeTypeMap;
Jeff Sharkey556d2d92018-07-12 19:51:27 -0600170
Jeff Sharkeyb6781bc2019-07-18 18:45:52 -0600171import androidx.annotation.GuardedBy;
shafika51f3ce2019-10-10 17:06:41 +0100172import androidx.annotation.Keep;
Jeff Sharkeyb6781bc2019-07-18 18:45:52 -0600173import androidx.annotation.NonNull;
174import androidx.annotation.Nullable;
175import androidx.annotation.VisibleForTesting;
176
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700177import com.android.providers.media.DatabaseHelper.OnFilesChangeListener;
Jeff Sharkeyd5a42922020-03-06 14:42:12 -0700178import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener;
Zimedbe69e2019-12-13 18:49:36 +0000179import com.android.providers.media.fuse.ExternalStorageServiceImpl;
180import com.android.providers.media.fuse.FuseDaemon;
Jeff Sharkeyf52c8122020-03-28 10:27:41 -0600181import com.android.providers.media.playlist.Playlist;
Jeff Sharkey10b4d8d2019-02-04 21:53:22 -0700182import com.android.providers.media.scan.MediaScanner;
Jeff Sharkey99a48282019-03-22 15:11:53 -0600183import com.android.providers.media.scan.ModernMediaScanner;
Jeff Sharkeyf05c4e72019-08-20 10:35:50 -0600184import com.android.providers.media.util.BackgroundThread;
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600185import com.android.providers.media.util.CachedSupplier;
Jeff Sharkey7ea24f22019-08-22 10:14:18 -0600186import com.android.providers.media.util.DatabaseUtils;
187import com.android.providers.media.util.FileUtils;
Jeff Sharkeye04e2c62020-03-05 10:53:33 -0700188import com.android.providers.media.util.ForegroundThread;
Jeff Sharkey60ca2982019-05-11 13:44:09 -0600189import com.android.providers.media.util.IsoInterface;
Jeff Sharkey5278ead2020-01-07 16:40:18 -0700190import com.android.providers.media.util.Logging;
Jeff Sharkeyf05c4e72019-08-20 10:35:50 -0600191import com.android.providers.media.util.LongArray;
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -0700192import com.android.providers.media.util.Metrics;
Jeff Sharkeye152d5762019-10-11 17:14:51 -0600193import com.android.providers.media.util.MimeUtils;
shafikd84da092020-04-29 17:53:30 +0100194import com.android.providers.media.util.PermissionUtils;
Jeff Sharkeye2750322020-01-07 22:06:24 -0700195import com.android.providers.media.util.RedactingFileDescriptor;
Jeff Sharkeyfe66ae32020-01-11 14:36:53 -0700196import com.android.providers.media.util.SQLiteQueryBuilder;
Anton Hansson7efb82f2019-05-20 10:37:27 +0100197import com.android.providers.media.util.XmpInterface;
Jeff Sharkey29421112018-07-27 20:56:44 -0600198
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -0600199import com.google.common.hash.Hashing;
200
The Android Open Source Project70215272009-03-03 19:32:43 -0800201import java.io.File;
Marco Nelissen10af34f2011-12-16 17:59:52 -0800202import java.io.FileDescriptor;
Jeff Sharkey60ca2982019-05-11 13:44:09 -0600203import java.io.FileInputStream;
The Android Open Source Project70215272009-03-03 19:32:43 -0800204import java.io.FileNotFoundException;
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -0700205import java.io.FileOutputStream;
The Android Open Source Project70215272009-03-03 19:32:43 -0800206import java.io.IOException;
Jeff Sharkeyd6697822020-03-22 20:59:47 -0600207import java.io.OutputStream;
Marco Nelissen10af34f2011-12-16 17:59:52 -0800208import java.io.PrintWriter;
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -0600209import java.nio.charset.StandardCharsets;
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -0700210import java.nio.file.Path;
Marco Nelissencb0c5a62009-12-08 13:44:19 -0800211import java.util.ArrayList;
Jeff Sharkey31d03d12018-08-07 11:26:19 -0600212import java.util.Arrays;
Marco Nelissen10af34f2011-12-16 17:59:52 -0800213import java.util.Collection;
Marco Nelissenf5f9eca2009-12-09 09:26:15 -0800214import java.util.List;
Marco Nelissen38b43642012-01-27 09:40:07 -0800215import java.util.Locale;
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600216import java.util.Map;
Jeff Sharkeyfac48212018-10-18 09:44:45 -0600217import java.util.Objects;
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -0700218import java.util.Optional;
Jeff Sharkey0e880712019-02-11 11:01:31 -0700219import java.util.Set;
Jeff Sharkeye9876152018-12-08 11:14:13 -0700220import java.util.function.Consumer;
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600221import java.util.function.Supplier;
Sahana Rao71403c02020-03-25 17:13:40 +0000222import java.util.function.UnaryOperator;
Jeff Sharkey58f533a2018-08-06 18:31:51 -0600223import java.util.regex.Matcher;
224import java.util.regex.Pattern;
The Android Open Source Project70215272009-03-03 19:32:43 -0800225
226/**
227 * Media content provider. See {@link android.provider.MediaStore} for details.
228 * Separate databases are kept for each external storage card we see (using the
229 * card's ID as an index). The content visible at content://media/external/...
230 * changes with the card.
231 */
232public class MediaProvider extends ContentProvider {
Jeff Sharkey58f533a2018-08-06 18:31:51 -0600233 /**
Abhijeet Kaur3bc15772021-11-17 08:40:34 +0000234 * Enables checks to stop apps from inserting and updating to private files via media provider.
235 */
236 @ChangeId
237 @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.R)
238 static final long ENABLE_CHECKS_FOR_PRIVATE_FILES = 172100307L;
239
240 /**
Jeff Sharkeyd2568872019-02-09 13:49:05 -0700241 * Regex of a selection string that matches a specific ID.
242 */
Jeff Sharkeyc9ae8592019-10-07 11:41:04 -0600243 static final Pattern PATTERN_SELECTION_ID = Pattern.compile(
Jeff Sharkeyd2568872019-02-09 13:49:05 -0700244 "(?:image_id|video_id)\\s*=\\s*(\\d+)");
245
246 /**
Nandana Dutt4f5e15a2019-11-29 10:45:58 +0000247 * Property that indicates whether fuse is enabled.
248 */
249 private static final String PROP_FUSE = "persist.sys.fuse";
250
251 /**
shafika32e93c2019-11-01 12:17:34 +0000252 * These directory names aren't declared in Environment as final variables, and so we need to
253 * have the same values in separate final variables in order to have them considered constant
254 * expressions.
255 */
256 private static final String DIRECTORY_MUSIC = "Music";
257 private static final String DIRECTORY_PODCASTS = "Podcasts";
258 private static final String DIRECTORY_RINGTONES = "Ringtones";
259 private static final String DIRECTORY_ALARMS = "Alarms";
260 private static final String DIRECTORY_NOTIFICATIONS = "Notifications";
261 private static final String DIRECTORY_PICTURES = "Pictures";
262 private static final String DIRECTORY_MOVIES = "Movies";
263 private static final String DIRECTORY_DOWNLOADS = "Download";
264 private static final String DIRECTORY_DCIM = "DCIM";
265 private static final String DIRECTORY_DOCUMENTS = "Documents";
266 private static final String DIRECTORY_AUDIOBOOKS = "Audiobooks";
Sahana Rao71693442019-11-13 13:48:07 +0000267 private static final String DIRECTORY_ANDROID = "Android";
shafikf0fea692020-02-14 15:49:17 +0000268
Sahana Rao71693442019-11-13 13:48:07 +0000269 private static final String DIRECTORY_MEDIA = "media";
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -0700270 private static final String DIRECTORY_THUMBNAILS = ".thumbnails";
271
272 /**
273 * Hard-coded filename where the current value of
274 * {@link DatabaseHelper#getOrCreateUuid} is persisted on a physical SD card
275 * to help identify stale thumbnail collections.
276 */
277 private static final String FILE_DATABASE_UUID = ".database_uuid";
shafika32e93c2019-11-01 12:17:34 +0000278
279 /**
shafikac34fe92020-02-25 15:28:55 +0000280 * Specify what default directories the caller gets full access to. By default, the caller
281 * shouldn't get full access to any default dirs.
282 * But for example, we do an exception for System Gallery apps and allow them full access to:
283 * DCIM, Pictures, Movies.
284 */
285 private static final String INCLUDED_DEFAULT_DIRECTORIES =
286 "android:included-default-directories";
287
288 /**
Sahana Rao02fb8f42020-05-14 16:54:35 +0100289 * Value indicating that operations should include database rows matching the criteria defined
Sahana Raoea587fc2020-06-03 15:56:23 +0100290 * by this key only when calling package has write permission to the database row or column is
291 * {@column MediaColumns#IS_PENDING} and is set by FUSE.
Sahana Rao02fb8f42020-05-14 16:54:35 +0100292 * <p>
293 * Note that items <em>not</em> matching the criteria will also be included, and as part of this
294 * match no additional write permission checks are carried out for those items.
295 */
Sahana Raoea587fc2020-06-03 15:56:23 +0100296 private static final int MATCH_VISIBLE_FOR_FILEPATH = 32;
Sahana Rao02fb8f42020-05-14 16:54:35 +0100297
298 /**
Sahana Raob02e7152020-06-12 17:07:31 +0100299 * Where clause to match pending files from FUSE. Pending files from FUSE will not have
300 * PATTERN_PENDING_FILEPATH_FOR_SQL pattern.
301 */
302 private static final String MATCH_PENDING_FROM_FUSE = String.format("lower(%s) NOT REGEXP '%s'",
303 MediaColumns.DATA, PATTERN_PENDING_FILEPATH_FOR_SQL);
304
305 /**
Jeff Sharkey0218c142018-10-19 15:37:00 -0600306 * Set of {@link Cursor} columns that refer to raw filesystem paths.
Jeff Sharkey16fb8052018-10-18 15:22:53 -0600307 */
Jeff Sharkey0218c142018-10-19 15:37:00 -0600308 private static final ArrayMap<String, Object> sDataColumns = new ArrayMap<>();
Jeff Sharkey16fb8052018-10-18 15:22:53 -0600309
310 {
Jeff Sharkey0218c142018-10-19 15:37:00 -0600311 sDataColumns.put(MediaStore.MediaColumns.DATA, null);
312 sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null);
313 sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null);
314 sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null);
315 sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null);
Jeff Sharkey16fb8052018-10-18 15:22:53 -0600316 }
317
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600318 private static final Object sCacheLock = new Object();
Jeff Sharkey007645e2012-03-08 17:45:12 -0800319
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600320 @GuardedBy("sCacheLock")
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600321 private static final Set<String> sCachedExternalVolumeNames = new ArraySet<>();
322 @GuardedBy("sCacheLock")
Jeff Sharkey22988642020-03-05 17:09:39 -0700323 private static final Map<String, File> sCachedVolumePaths = new ArrayMap<>();
324 @GuardedBy("sCacheLock")
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600325 private static final Map<String, Collection<File>> sCachedVolumeScanPaths = new ArrayMap<>();
Jeff Sharkey564929d2020-04-06 16:51:58 -0600326 @GuardedBy("sCacheLock")
327 private static final ArrayMap<File, String> sCachedVolumePathToId = new ArrayMap<>();
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600328
Abhijeet Kaur3bc15772021-11-17 08:40:34 +0000329 /**
330 * Please use {@link getDownloadsProviderAuthority()} instead of using this directly.
331 */
332 private static final String DOWNLOADS_PROVIDER_AUTHORITY = "downloads";
333
Zim7d249ef2020-05-26 13:55:56 +0100334 @GuardedBy("mShouldRedactThreadIds")
335 private final LongArray mShouldRedactThreadIds = new LongArray();
336
Abhijeet Kaur2598beb2020-03-23 14:31:58 +0000337 public void updateVolumes() {
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600338 synchronized (sCacheLock) {
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600339 sCachedExternalVolumeNames.clear();
340 sCachedExternalVolumeNames.addAll(MediaStore.getExternalVolumeNames(getContext()));
Jeff Sharkey74472302020-06-04 17:35:44 -0600341 Log.v(TAG, "Updated external volumes to: " + sCachedExternalVolumeNames.toString());
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600342
Jeff Sharkey22988642020-03-05 17:09:39 -0700343 sCachedVolumePaths.clear();
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600344 sCachedVolumeScanPaths.clear();
Jeff Sharkey564929d2020-04-06 16:51:58 -0600345 sCachedVolumePathToId.clear();
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600346 try {
347 sCachedVolumeScanPaths.put(MediaStore.VOLUME_INTERNAL,
Jeff Sharkeyc5c39142019-12-15 22:46:03 -0700348 FileUtils.getVolumeScanPaths(getContext(), MediaStore.VOLUME_INTERNAL));
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -0600349 } catch (FileNotFoundException e) {
350 Log.wtf(TAG, "Failed to update volume " + MediaStore.VOLUME_INTERNAL, e);
351 }
352
353 for (String volumeName : sCachedExternalVolumeNames) {
354 try {
Jeff Sharkey564929d2020-04-06 16:51:58 -0600355 final Uri uri = MediaStore.Files.getContentUri(volumeName);
356 final StorageVolume volume = mStorageManager.getStorageVolume(uri);
Jeff Sharkey564929d2020-04-06 16:51:58 -0600357 sCachedVolumePaths.put(volumeName, volume.getDirectory());
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600358 sCachedVolumeScanPaths.put(volumeName,
Jeff Sharkeyc5c39142019-12-15 22:46:03 -0700359 FileUtils.getVolumeScanPaths(getContext(), volumeName));
Jeff Sharkey564929d2020-04-06 16:51:58 -0600360 sCachedVolumePathToId.put(volume.getDirectory(), volume.getId());
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -0600361 } catch (IllegalStateException | FileNotFoundException e) {
362 Log.wtf(TAG, "Failed to update volume " + volumeName, e);
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600363 }
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600364 }
365 }
Jeff Sharkeye59ed6b2020-01-11 16:20:00 -0700366
367 // Update filters to reflect mounted volumes so users don't get
368 // confused by metadata from ejected volumes
Jeff Sharkey22988642020-03-05 17:09:39 -0700369 ForegroundThread.getExecutor().execute(() -> {
Jeff Sharkeye59ed6b2020-01-11 16:20:00 -0700370 mExternalDatabase.setFilterVolumeNames(getExternalVolumeNames());
371 });
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600372 }
373
Jeff Sharkey564929d2020-04-06 16:51:58 -0600374 public @NonNull File getVolumePath(@NonNull String volumeName) throws FileNotFoundException {
Jeff Sharkeyc5c39142019-12-15 22:46:03 -0700375 // Ugly hack to keep unit tests passing, where we don't always have a
376 // Context to discover volumes with
377 if (getContext() == null) {
378 return Environment.getExternalStorageDirectory();
379 }
380
Jeff Sharkey22988642020-03-05 17:09:39 -0700381 synchronized (sCacheLock) {
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -0600382 if (sCachedVolumePaths.containsKey(volumeName)) {
383 return sCachedVolumePaths.get(volumeName);
Jeff Sharkey22988642020-03-05 17:09:39 -0700384 }
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -0600385
386 // Nothing found above; let's ask directly and cache the answer
387 final File res = FileUtils.getVolumePath(getContext(), volumeName);
388 sCachedVolumePaths.put(volumeName, res);
Jeff Sharkey22988642020-03-05 17:09:39 -0700389 return res;
390 }
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600391 }
392
Jeff Sharkey564929d2020-04-06 16:51:58 -0600393 public @NonNull String getVolumeId(@NonNull File file) throws FileNotFoundException {
394 synchronized (sCacheLock) {
395 for (int i = 0; i < sCachedVolumePathToId.size(); i++) {
396 if (FileUtils.contains(sCachedVolumePathToId.keyAt(i), file)) {
397 return sCachedVolumePathToId.valueAt(i);
398 }
399 }
400
401 // Nothing found above; let's ask directly and cache the answer
402 final StorageVolume volume = mStorageManager.getStorageVolume(file);
Jeff Sharkeyf06febd2020-04-07 13:03:30 -0600403 if (volume == null) {
404 throw new FileNotFoundException("Missing volume for " + file);
405 }
Jeff Sharkey564929d2020-04-06 16:51:58 -0600406 sCachedVolumePathToId.put(volume.getDirectory(), volume.getId());
407 return volume.getId();
408 }
409 }
410
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -0600411 public @NonNull Set<String> getExternalVolumeNames() {
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600412 synchronized (sCacheLock) {
413 return new ArraySet<>(sCachedExternalVolumeNames);
414 }
415 }
416
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -0600417 public @NonNull Collection<File> getVolumeScanPaths(String volumeName)
418 throws FileNotFoundException {
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600419 synchronized (sCacheLock) {
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -0600420 if (sCachedVolumeScanPaths.containsKey(volumeName)) {
421 return new ArrayList<>(sCachedVolumeScanPaths.get(volumeName));
422 }
423
424 // Nothing found above; let's ask directly and cache the answer
425 final Collection<File> res = FileUtils.getVolumeScanPaths(getContext(), volumeName);
426 sCachedVolumeScanPaths.put(volumeName, res);
427 return res;
Jeff Sharkey007645e2012-03-08 17:45:12 -0800428 }
429 }
430
Jeff Sharkey5d36def2013-10-16 16:35:29 -0700431 private StorageManager mStorageManager;
Jeff Sharkey55f76902015-07-24 15:22:08 -0700432 private AppOpsManager mAppOpsManager;
Sean Stoutcceb5e42017-09-08 11:16:00 -0700433 private PackageManager mPackageManager;
Abhijeet Kaur3bc15772021-11-17 08:40:34 +0000434 private int mExternalStorageAuthorityAppId;
435 private int mDownloadsAuthorityAppId;
Jeff Sharkey5d36def2013-10-16 16:35:29 -0700436
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -0700437 private Size mThumbSize;
438
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -0600439 /**
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600440 * Map from UID to cached {@link LocalCallingIdentity}. Values are only
Zimuzo Ezeozueee10fb72020-03-19 20:26:37 +0000441 * maintained in this map while the UID is actively working with a
442 * performance-critical component, such as camera.
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600443 */
444 @GuardedBy("mCachedCallingIdentity")
445 private final SparseArray<LocalCallingIdentity> mCachedCallingIdentity = new SparseArray<>();
446
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600447 private final OnOpActiveChangedListener mActiveListener = (code, uid, packageName, active) -> {
448 synchronized (mCachedCallingIdentity) {
449 if (active) {
Philip P. Moltmann05b68ea2019-09-27 13:33:11 -0700450 // TODO moltmann: Set correct featureId
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600451 mCachedCallingIdentity.put(uid,
Philip P. Moltmann05b68ea2019-09-27 13:33:11 -0700452 LocalCallingIdentity.fromExternal(getContext(), uid, packageName,
453 null));
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600454 } else {
455 mCachedCallingIdentity.remove(uid);
456 }
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600457 }
458 };
459
Zimuzo Ezeozueee10fb72020-03-19 20:26:37 +0000460 /**
461 * Map from UID to cached {@link LocalCallingIdentity}. Values are only
462 * maintained in this map until there's any change in the appops needed or packages
463 * used in the {@link LocalCallingIdentity}.
464 */
465 @GuardedBy("mCachedCallingIdentityForFuse")
466 private final SparseArray<LocalCallingIdentity> mCachedCallingIdentityForFuse =
467 new SparseArray<>();
468
Zim696dea42020-03-07 11:41:42 +0000469 private OnOpChangedListener mModeListener =
Zimuzo Ezeozueee10fb72020-03-19 20:26:37 +0000470 (op, packageName) -> invalidateLocalCallingIdentityCache(packageName, "op " + op);
Zim696dea42020-03-07 11:41:42 +0000471
shafik60ed7012020-04-24 20:10:33 +0100472 /**
473 * Retrieves a cached calling identity or creates a new one. Also, always sets the app-op
474 * description for the calling identity.
475 */
shafikd84da092020-04-29 17:53:30 +0100476 private LocalCallingIdentity getCachedCallingIdentityForFuse(int uid) {
Zimuzo Ezeozueee10fb72020-03-19 20:26:37 +0000477 synchronized (mCachedCallingIdentityForFuse) {
shafikd84da092020-04-29 17:53:30 +0100478 PermissionUtils.setOpDescription("via FUSE");
Zimuzo Ezeozueee10fb72020-03-19 20:26:37 +0000479 LocalCallingIdentity ident = mCachedCallingIdentityForFuse.get(uid);
Zim696dea42020-03-07 11:41:42 +0000480 if (ident == null) {
Zimuzo Ezeozueee10fb72020-03-19 20:26:37 +0000481 ident = LocalCallingIdentity.fromExternal(getContext(), uid);
482 mCachedCallingIdentityForFuse.put(uid, ident);
Zim696dea42020-03-07 11:41:42 +0000483 }
484 return ident;
485 }
486 }
487
488 /**
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600489 * Calling identity state about on the current thread. Populated on demand,
490 * and invalidated by {@link #onCallingPackageChanged()} when each remote
491 * call is finished.
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -0600492 */
493 private final ThreadLocal<LocalCallingIdentity> mCallingIdentity = ThreadLocal
Zimuzo Ezeozueee10fb72020-03-19 20:26:37 +0000494 .withInitial(() -> {
shafikd84da092020-04-29 17:53:30 +0100495 PermissionUtils.setOpDescription("via MediaProvider");
Zimuzo Ezeozueee10fb72020-03-19 20:26:37 +0000496 synchronized (mCachedCallingIdentity) {
497 final LocalCallingIdentity cached = mCachedCallingIdentity
498 .get(Binder.getCallingUid());
499 return (cached != null) ? cached
500 : LocalCallingIdentity.fromBinder(getContext(), this);
501 }
502 });
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -0600503
Jeff Sharkey43913322019-12-16 16:28:02 -0700504 /**
505 * We simply propagate the UID that is being tracked by
506 * {@link LocalCallingIdentity}, which means we accurately blame both
507 * incoming Binder calls and FUSE calls.
508 */
509 private final ProxyTransactListener mTransactListener = new ProxyTransactListener() {
510 @Override
511 public Object onTransactStarted(IBinder binder, int transactionCode) {
Jeff Sharkey564929d2020-04-06 16:51:58 -0600512 if (LOGV) Trace.beginSection(Thread.currentThread().getStackTrace()[5].getMethodName());
513 return Binder.setCallingWorkSourceUid(mCallingIdentity.get().uid);
Jeff Sharkey43913322019-12-16 16:28:02 -0700514 }
515
516 @Override
517 public void onTransactEnded(Object session) {
518 final long token = (long) session;
519 Binder.restoreCallingWorkSource(token);
Jeff Sharkey564929d2020-04-06 16:51:58 -0600520 if (LOGV) Trace.endSection();
Jeff Sharkey43913322019-12-16 16:28:02 -0700521 }
522 };
523
Marco Nelissen7f364942011-12-12 14:32:49 -0800524 // In memory cache of path<->id mappings, to speed up inserts during media scan
Jeff Sharkey6cf27b92019-03-24 13:03:02 -0600525 @GuardedBy("mDirectoryCache")
526 private final ArrayMap<String, Long> mDirectoryCache = new ArrayMap<>();
Marco Nelissen7f364942011-12-12 14:32:49 -0800527
Marco Nelissen16620452012-02-03 12:45:44 -0800528 private static final String[] sDataOnlyColumn = new String[] {
529 FileColumns.DATA
530 };
531
Chong Zhangeb5f7a62016-08-31 21:25:15 -0700532 private static final String ID_NOT_PARENT_CLAUSE =
Jeff Sharkeyacc3b582019-12-07 11:52:32 -0700533 "_id NOT IN (SELECT parent FROM files WHERE parent IS NOT NULL)";
Chong Zhangeb5f7a62016-08-31 21:25:15 -0700534
Marco Nelissen01e706a2013-09-12 15:38:42 -0700535 private static final String CANONICAL = "canonical";
536
Zim696dea42020-03-07 11:41:42 +0000537 private BroadcastReceiver mPackageReceiver = new BroadcastReceiver() {
538 @Override
539 public void onReceive(Context context, Intent intent) {
540 switch (intent.getAction()) {
541 case Intent.ACTION_PACKAGE_REMOVED:
542 case Intent.ACTION_PACKAGE_ADDED:
543 Uri uri = intent.getData();
544 String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
545 if (pkg != null) {
Zimuzo Ezeozueee10fb72020-03-19 20:26:37 +0000546 invalidateLocalCallingIdentityCache(pkg, "package " + intent.getAction());
Zim696dea42020-03-07 11:41:42 +0000547 } else {
548 Log.w(TAG, "Failed to retrieve package from intent: " + intent.getAction());
549 }
550 break;
551 }
552 }
553 };
554
Zimuzo Ezeozueee10fb72020-03-19 20:26:37 +0000555 private void invalidateLocalCallingIdentityCache(String packageName, String reason) {
556 synchronized (mCachedCallingIdentityForFuse) {
557 try {
558 Log.i(TAG, "Invalidating LocalCallingIdentity cache for package " + packageName
559 + ". Reason: " + reason);
560 mCachedCallingIdentityForFuse.remove(
561 getContext().getPackageManager().getPackageUid(packageName, 0));
562 } catch (NameNotFoundException ignored) {
Zim696dea42020-03-07 11:41:42 +0000563 }
564 }
565 }
566
Martijn Coenen49971b12020-02-18 08:59:29 +0100567 private final void updateQuotaTypeForUri(@NonNull Uri uri, int mediaType) {
Jeff Sharkey22988642020-03-05 17:09:39 -0700568 Trace.beginSection("updateQuotaTypeForUri");
Martijn Coenen49971b12020-02-18 08:59:29 +0100569 File file;
570 try {
571 file = queryForDataFile(uri, null);
Martijn Coenenc55e1072020-06-10 14:48:00 +0200572 if (!file.exists()) {
573 // This can happen if an item is inserted in MediaStore before it is created
574 return;
575 }
Martijn Coenenaf2d34d2020-06-19 12:52:20 +0200576
577 if (mediaType == FileColumns.MEDIA_TYPE_NONE) {
578 // This might be because the file is hidden; but we still want to
579 // attribute its quota to the correct type, so get the type from
580 // the extension instead.
581 mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
582 }
583
584 updateQuotaTypeForFileInternal(file, mediaType);
Martijn Coenen49971b12020-02-18 08:59:29 +0100585 } catch (FileNotFoundException e) {
586 // Ignore
587 return;
Martijn Coenenaf2d34d2020-06-19 12:52:20 +0200588 } finally {
589 Trace.endSection();
Martijn Coenen49971b12020-02-18 08:59:29 +0100590 }
Martijn Coenenaf2d34d2020-06-19 12:52:20 +0200591 }
592
593 private final void updateQuotaTypeForFileInternal(File file, int mediaType) {
Martijn Coenen49971b12020-02-18 08:59:29 +0100594 try {
595 switch (mediaType) {
596 case FileColumns.MEDIA_TYPE_AUDIO:
597 mStorageManager.updateExternalStorageFileQuotaType(file,
598 StorageManager.QUOTA_TYPE_MEDIA_AUDIO);
599 break;
600 case FileColumns.MEDIA_TYPE_VIDEO:
601 mStorageManager.updateExternalStorageFileQuotaType(file,
602 StorageManager.QUOTA_TYPE_MEDIA_VIDEO);
603 break;
604 case FileColumns.MEDIA_TYPE_IMAGE:
605 mStorageManager.updateExternalStorageFileQuotaType(file,
606 StorageManager.QUOTA_TYPE_MEDIA_IMAGE);
607 break;
608 default:
609 mStorageManager.updateExternalStorageFileQuotaType(file,
610 StorageManager.QUOTA_TYPE_MEDIA_NONE);
611 break;
612 }
613 } catch (IOException e) {
614 Log.w(TAG, "Failed to update quota type for " + file.getPath(), e);
615 }
616 }
617
Jeff Sharkey22988642020-03-05 17:09:39 -0700618 /**
619 * Since these operations are in the critical path of apps working with
620 * media, we only collect the {@link Uri} that need to be notified, and all
621 * other side-effect operations are delegated to {@link BackgroundThread} so
622 * that we return as quickly as possible.
623 */
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700624 private final OnFilesChangeListener mFilesListener = new OnFilesChangeListener() {
625 @Override
626 public void onInsert(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
627 int mediaType, boolean isDownload) {
Sahana Raoa211c4e2020-03-23 02:59:33 +0000628 handleInsertedRowForFuse(id);
Jeff Sharkeye04e2c62020-03-05 10:53:33 -0700629 acceptWithExpansion(helper::notifyInsert, volumeName, id, mediaType, isDownload);
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700630
Jeff Sharkey22988642020-03-05 17:09:39 -0700631 helper.postBackground(() -> {
632 if (helper.isExternal()) {
633 // Update the quota type on the filesystem
634 Uri fileUri = MediaStore.Files.getContentUri(volumeName, id);
635 updateQuotaTypeForUri(fileUri, mediaType);
636 }
Martijn Coenen49971b12020-02-18 08:59:29 +0100637
Jeff Sharkey22988642020-03-05 17:09:39 -0700638 // Tell our SAF provider so it knows when views are no longer empty
639 MediaDocumentsProvider.onMediaStoreInsert(getContext(), volumeName, mediaType, id);
640 });
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700641 }
642
643 @Override
Sahana Raoa211c4e2020-03-23 02:59:33 +0000644 public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName,
645 long oldId, int oldMediaType, boolean oldIsDownload,
646 long newId, int newMediaType, boolean newIsDownload,
Sahana Rao1e8271b2020-04-03 14:01:08 +0100647 String oldOwnerPackage, String newOwnerPackage, String oldPath) {
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700648 final boolean isDownload = oldIsDownload || newIsDownload;
Martijn Coenenc55e1072020-06-10 14:48:00 +0200649 final Uri fileUri = MediaStore.Files.getContentUri(volumeName, oldId);
Sahana Rao1e8271b2020-04-03 14:01:08 +0100650 handleUpdatedRowForFuse(oldPath, oldOwnerPackage, oldId, newId);
651 handleOwnerPackageNameChange(oldPath, oldOwnerPackage, newOwnerPackage);
Sahana Raoa211c4e2020-03-23 02:59:33 +0000652 acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, oldMediaType, isDownload);
653
Martijn Coenenc55e1072020-06-10 14:48:00 +0200654 helper.postBackground(() -> {
655 if (helper.isExternal()) {
656 // Update the quota type on the filesystem
657 updateQuotaTypeForUri(fileUri, newMediaType);
658 }
659 });
660
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700661 if (newMediaType != oldMediaType) {
Sahana Raoa211c4e2020-03-23 02:59:33 +0000662 acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, newMediaType,
663 isDownload);
Jeff Sharkey22988642020-03-05 17:09:39 -0700664
665 helper.postBackground(() -> {
Jeff Sharkey22988642020-03-05 17:09:39 -0700666 // Invalidate any thumbnails when the media type changes
667 invalidateThumbnails(fileUri);
668 });
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700669 }
670 }
671
672 @Override
673 public void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id,
Sahana Raoa211c4e2020-03-23 02:59:33 +0000674 int mediaType, boolean isDownload, String ownerPackageName, String path) {
675 handleDeletedRowForFuse(path, ownerPackageName, id);
Jeff Sharkey22988642020-03-05 17:09:39 -0700676 acceptWithExpansion(helper::notifyDelete, volumeName, id, mediaType, isDownload);
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700677
Jeff Sharkey22988642020-03-05 17:09:39 -0700678 helper.postBackground(() -> {
679 // Item no longer exists, so revoke all access to it
680 Trace.beginSection("revokeUriPermission");
681 try {
682 acceptWithExpansion((uri) -> {
683 getContext().revokeUriPermission(uri, ~0);
684 }, volumeName, id, mediaType, isDownload);
685 } finally {
686 Trace.endSection();
687 }
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700688
Jeff Sharkey22988642020-03-05 17:09:39 -0700689 // Invalidate any thumbnails now that media is gone
690 invalidateThumbnails(MediaStore.Files.getContentUri(volumeName, id));
691
692 // Tell our SAF provider so it can revoke too
693 MediaDocumentsProvider.onMediaStoreDelete(getContext(), volumeName, mediaType, id);
694 });
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700695 }
696 };
697
Sahana Rao71403c02020-03-25 17:13:40 +0000698 private final UnaryOperator<String> mIdGenerator = path -> {
699 final long rowId = mCallingIdentity.get().getDeletedRowId(path);
700 if (rowId != -1 && isFuseThread()) {
701 return String.valueOf(rowId);
702 }
703 return null;
704 };
705
Jeff Sharkeyd5a42922020-03-06 14:42:12 -0700706 private final OnLegacyMigrationListener mMigrationListener = new OnLegacyMigrationListener() {
707 @Override
708 public void onStarted(ContentProviderClient client, String volumeName) {
709 MediaStore.startLegacyMigration(ContentResolver.wrap(client), volumeName);
710 }
711
712 @Override
Jeff Sharkey9aca51f2020-04-29 11:28:08 -0600713 public void onProgress(ContentProviderClient client, String volumeName,
714 long progress, long total) {
715 // TODO: notify blocked threads of progress once we can change APIs
716 }
717
718 @Override
Jeff Sharkeyd5a42922020-03-06 14:42:12 -0700719 public void onFinished(ContentProviderClient client, String volumeName) {
720 MediaStore.finishLegacyMigration(ContentResolver.wrap(client), volumeName);
721 }
722 };
723
The Android Open Source Project70215272009-03-03 19:32:43 -0800724 /**
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700725 * Apply {@link Consumer#accept} to the given item.
Jeff Sharkeyd6302642019-04-28 13:30:23 -0600726 * <p>
727 * Since media items can be exposed through multiple collections or views,
728 * this method expands the single item being accepted to also accept all
729 * relevant views.
730 */
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700731 private void acceptWithExpansion(@NonNull Consumer<Uri> consumer, @NonNull String volumeName,
732 long id, int mediaType, boolean isDownload) {
733 switch (mediaType) {
734 case FileColumns.MEDIA_TYPE_AUDIO:
735 consumer.accept(MediaStore.Audio.Media.getContentUri(volumeName, id));
Jeff Sharkeyd6302642019-04-28 13:30:23 -0600736
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700737 // Any changing audio items mean we probably need to invalidate all
738 // indexed views built from that media
Jeff Sharkeyd6302642019-04-28 13:30:23 -0600739 consumer.accept(Audio.Genres.getContentUri(volumeName));
740 consumer.accept(Audio.Playlists.getContentUri(volumeName));
741 consumer.accept(Audio.Artists.getContentUri(volumeName));
742 consumer.accept(Audio.Albums.getContentUri(volumeName));
Jeff Sharkey11685352019-05-14 10:48:07 -0600743 break;
Jeff Sharkey5cff16b2020-01-17 19:00:50 -0700744
745 case FileColumns.MEDIA_TYPE_VIDEO:
746 consumer.accept(MediaStore.Video.Media.getContentUri(volumeName, id));
747 break;
748
749 case FileColumns.MEDIA_TYPE_IMAGE:
750 consumer.accept(MediaStore.Images.Media.getContentUri(volumeName, id));
751 break;
752 }
753
754 // Also notify through any generic views
755 consumer.accept(MediaStore.Files.getContentUri(volumeName, id));
756 if (isDownload) {
757 consumer.accept(MediaStore.Downloads.getContentUri(volumeName, id));
758 }
759
760 // Rinse and repeat through any synthetic views
761 switch (volumeName) {
762 case MediaStore.VOLUME_INTERNAL:
763 case MediaStore.VOLUME_EXTERNAL:
764 // Already a top-level view, no need to expand
765 break;
766 default:
767 acceptWithExpansion(consumer, MediaStore.VOLUME_EXTERNAL,
768 id, mediaType, isDownload);
769 break;
Jeff Sharkeyd6302642019-04-28 13:30:23 -0600770 }
771 }
772
Mike Lockwooded9bbc42011-01-12 19:32:44 -0500773 private static final String[] sDefaultFolderNames = {
shafikf0fea692020-02-14 15:49:17 +0000774 Environment.DIRECTORY_MUSIC,
775 Environment.DIRECTORY_PODCASTS,
776 Environment.DIRECTORY_RINGTONES,
777 Environment.DIRECTORY_ALARMS,
778 Environment.DIRECTORY_NOTIFICATIONS,
779 Environment.DIRECTORY_PICTURES,
780 Environment.DIRECTORY_MOVIES,
781 Environment.DIRECTORY_DOWNLOADS,
782 Environment.DIRECTORY_DCIM,
shafik0382d042020-03-03 20:16:24 +0000783 Environment.DIRECTORY_AUDIOBOOKS,
784 Environment.DIRECTORY_DOCUMENTS,
Mike Lockwooded9bbc42011-01-12 19:32:44 -0500785 };
786
shafikf0fea692020-02-14 15:49:17 +0000787 private static boolean isDefaultDirectoryName(@Nullable String dirName) {
788 for (String defaultDirName : sDefaultFolderNames) {
789 if (defaultDirName.equals(dirName)) {
790 return true;
791 }
792 }
793 return false;
794 }
795
Jeff Sharkey72613f72015-08-19 14:18:19 -0700796 /**
797 * Ensure that default folders are created on mounted primary storage
798 * devices. We only do this once per volume so we don't annoy the user if
799 * deleted manually.
800 */
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -0600801 private void ensureDefaultFolders(@NonNull String volumeName, @NonNull SQLiteDatabase db) {
Jeff Sharkey7eed0eb2019-01-19 17:27:46 -0700802 try {
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600803 final File path = getVolumePath(volumeName);
Jeff Sharkey7eed0eb2019-01-19 17:27:46 -0700804 final StorageVolume vol = mStorageManager.getStorageVolume(path);
805 final String key;
Zim2697d472020-02-19 14:27:11 +0000806 if (vol == null) {
807 Log.w(TAG, "Failed to ensure default folders for " + volumeName);
808 return;
809 }
810
811 if (vol.isPrimary()) {
Jeff Sharkey7eed0eb2019-01-19 17:27:46 -0700812 key = "created_default_folders";
813 } else {
Jeff Sharkeyc5c39142019-12-15 22:46:03 -0700814 key = "created_default_folders_" + vol.getMediaStoreVolumeName();
Mike Lockwooded9bbc42011-01-12 19:32:44 -0500815 }
816
Jeff Sharkey7eed0eb2019-01-19 17:27:46 -0700817 final SharedPreferences prefs = PreferenceManager
818 .getDefaultSharedPreferences(getContext());
819 if (prefs.getInt(key, 0) == 0) {
820 for (String folderName : sDefaultFolderNames) {
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -0700821 final File folder = new File(vol.getDirectory(), folderName);
Jeff Sharkey7eed0eb2019-01-19 17:27:46 -0700822 if (!folder.exists()) {
823 folder.mkdirs();
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -0600824 insertDirectory(db, folder.getAbsolutePath());
Jeff Sharkey7eed0eb2019-01-19 17:27:46 -0700825 }
826 }
827
828 SharedPreferences.Editor editor = prefs.edit();
829 editor.putInt(key, 1);
830 editor.commit();
831 }
832 } catch (IOException e) {
833 Log.w(TAG, "Failed to ensure default folders for " + volumeName, e);
Mike Lockwooded9bbc42011-01-12 19:32:44 -0500834 }
835 }
836
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -0700837 /**
838 * Ensure that any thumbnail collections on the given storage volume can be
839 * used with the given {@link DatabaseHelper}. If the
840 * {@link DatabaseHelper#getOrCreateUuid} doesn't match the UUID found on
841 * disk, then all thumbnails will be considered stable and will be deleted.
842 */
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -0600843 private void ensureThumbnailsValid(@NonNull String volumeName, @NonNull SQLiteDatabase db) {
844 final String uuidFromDatabase = DatabaseHelper.getOrCreateUuid(db);
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -0700845 try {
846 for (File dir : getThumbnailDirectories(volumeName)) {
847 if (!dir.exists()) {
848 dir.mkdirs();
849 }
850
851 final File file = new File(dir, FILE_DATABASE_UUID);
852 final Optional<String> uuidFromDisk = FileUtils.readString(file);
853
854 final boolean updateUuid;
855 if (!uuidFromDisk.isPresent()) {
856 // For newly inserted volumes or upgrading of existing volumes,
857 // assume that our current UUID is valid
858 updateUuid = true;
859 } else if (!Objects.equals(uuidFromDatabase, uuidFromDisk.get())) {
860 // The UUID of database disagrees with the one on disk,
861 // which means we can't trust any thumbnails
862 Log.d(TAG, "Invalidating all thumbnails under " + dir);
863 FileUtils.walkFileTreeContents(dir.toPath(), this::deleteAndInvalidate);
864 updateUuid = true;
865 } else {
866 updateUuid = false;
867 }
868
869 if (updateUuid) {
870 FileUtils.writeString(file, Optional.of(uuidFromDatabase));
871 }
872 }
873 } catch (IOException e) {
874 Log.w(TAG, "Failed to ensure thumbnails valid for " + volumeName, e);
875 }
876 }
877
The Android Open Source Project70215272009-03-03 19:32:43 -0800878 @Override
Jeff Sharkey74f73732019-11-12 15:36:32 -0700879 public void attachInfo(Context context, ProviderInfo info) {
Jeff Sharkey74f73732019-11-12 15:36:32 -0700880 Log.v(TAG, "Attached " + info.authority + " from " + info.applicationInfo.packageName);
881
Jeff Sharkey74f73732019-11-12 15:36:32 -0700882 mUriMatcher = new LocalUriMatcher(info.authority);
Jeff Sharkey56c34e82019-11-21 15:56:37 -0700883
884 super.attachInfo(context, info);
Jeff Sharkey74f73732019-11-12 15:36:32 -0700885 }
886
887 @Override
The Android Open Source Project70215272009-03-03 19:32:43 -0800888 public boolean onCreate() {
Mike Lockwoodd186c642010-07-14 15:37:42 -0400889 final Context context = getContext();
890
Jeff Sharkey7873f542019-05-30 12:18:38 -0600891 // Shift call statistics back to the original caller
Jeff Sharkey43913322019-12-16 16:28:02 -0700892 Binder.setProxyTransactListener(mTransactListener);
Jeff Sharkey7873f542019-05-30 12:18:38 -0600893
Jeff Sharkey55f76902015-07-24 15:22:08 -0700894 mStorageManager = context.getSystemService(StorageManager.class);
895 mAppOpsManager = context.getSystemService(AppOpsManager.class);
Sean Stoutcceb5e42017-09-08 11:16:00 -0700896 mPackageManager = context.getPackageManager();
Jeff Sharkey5d36def2013-10-16 16:35:29 -0700897
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -0700898 // Reasonable thumbnail size is half of the smallest screen edge width
Jeff Sharkey5f9c0792019-01-26 13:52:03 -0700899 final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
900 final int thumbSize = Math.min(metrics.widthPixels, metrics.heightPixels) / 2;
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -0700901 mThumbSize = new Size(thumbSize, thumbSize);
902
Jeff Sharkeye3650642020-04-03 18:50:03 -0600903 mMediaScanner = new ModernMediaScanner(context);
Jeff Sharkey85acbbe2019-10-15 17:10:30 -0600904
Jeff Sharkey74f73732019-11-12 15:36:32 -0700905 mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME,
Jeff Sharkeye3650642020-04-03 18:50:03 -0600906 true, false, false, Column.class,
Sahana Rao71403c02020-03-25 17:13:40 +0000907 Metrics::logSchemaChange, mFilesListener, mMigrationListener, mIdGenerator);
Jeff Sharkey74f73732019-11-12 15:36:32 -0700908 mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME,
Jeff Sharkeye3650642020-04-03 18:50:03 -0600909 false, false, false, Column.class,
Sahana Rao71403c02020-03-25 17:13:40 +0000910 Metrics::logSchemaChange, mFilesListener, mMigrationListener, mIdGenerator);
The Android Open Source Project70215272009-03-03 19:32:43 -0800911
Zim696dea42020-03-07 11:41:42 +0000912 final IntentFilter packageFilter = new IntentFilter();
913 packageFilter.setPriority(10);
Abhijeet Kaur2598beb2020-03-23 14:31:58 +0000914 packageFilter.addDataScheme("package");
Zim696dea42020-03-07 11:41:42 +0000915 packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
916 packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
917 context.registerReceiver(mPackageReceiver, packageFilter);
918
Zim604f4522020-06-05 15:30:09 +0100919 // Watch for invalidation of cached volumes
920 mStorageManager.registerStorageVolumeCallback(context.getMainExecutor(),
921 new StorageVolumeCallback() {
922 @Override
923 public void onStateChanged(@NonNull StorageVolume volume) {
924 updateVolumes();
925 }
926 });
927
Zim49354282019-09-09 13:52:39 +0100928 updateVolumes();
Zim604f4522020-06-05 15:30:09 +0100929 attachVolume(MediaStore.VOLUME_INTERNAL, /* validate */ false);
Zim49354282019-09-09 13:52:39 +0100930 for (String volumeName : getExternalVolumeNames()) {
Zim604f4522020-06-05 15:30:09 +0100931 attachVolume(volumeName, /* validate */ false);
The Android Open Source Project70215272009-03-03 19:32:43 -0800932 }
933
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600934 // Watch for performance-sensitive activity
Jeff Sharkey7ea24f22019-08-22 10:14:18 -0600935 mAppOpsManager.startWatchingActive(new String[] {
936 AppOpsManager.OPSTR_CAMERA
937 }, context.getMainExecutor(), mActiveListener);
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -0600938
Zim696dea42020-03-07 11:41:42 +0000939 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE,
940 null /* all packages */, mModeListener);
941 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_EXTERNAL_STORAGE,
942 null /* all packages */, mModeListener);
943 mAppOpsManager.startWatchingMode(permissionToOp(ACCESS_MEDIA_LOCATION),
944 null /* all packages */, mModeListener);
945 // Legacy apps
946 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_LEGACY_STORAGE,
947 null /* all packages */, mModeListener);
948 // File managers
949 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE,
950 null /* all packages */, mModeListener);
951 // Default gallery changes
952 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_IMAGES,
953 null /* all packages */, mModeListener);
954 mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO,
955 null /* all packages */, mModeListener);
Nikita Ioffea9551ab2020-06-22 14:22:23 +0100956 try {
957 // Here we are forced to depend on the non-public API of AppOpsManager. If
958 // OPSTR_NO_ISOLATED_STORAGE app op is not defined in AppOpsManager, then this call will
959 // throw an IllegalArgumentException during MediaProvider startup. In combination with
960 // MediaProvider's CTS tests it should give us guarantees that OPSTR_NO_ISOLATED_STORAGE
961 // is defined.
962 mAppOpsManager.startWatchingMode(PermissionUtils.OPSTR_NO_ISOLATED_STORAGE,
963 null /* all packages */, mModeListener);
964 } catch (IllegalArgumentException e) {
965 Log.w(TAG, "Failed to start watching " + PermissionUtils.OPSTR_NO_ISOLATED_STORAGE, e);
966 }
Abhijeet Kaur3bc15772021-11-17 08:40:34 +0000967
968 ProviderInfo provider = mPackageManager.resolveContentProvider(
969 getDownloadsProviderAuthority(), PackageManager.MATCH_DIRECT_BOOT_AWARE
970 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
971 if (provider != null) {
972 mDownloadsAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
973 }
974
975 provider = mPackageManager.resolveContentProvider(getExternalStorageProviderAuthority(),
976 PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
977 if (provider != null) {
978 mExternalStorageAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
979 }
The Android Open Source Project70215272009-03-03 19:32:43 -0800980 return true;
981 }
982
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -0600983 @Override
984 public void onCallingPackageChanged() {
985 // Identity of the current thread has changed, so invalidate caches
986 mCallingIdentity.remove();
987 }
988
989 public LocalCallingIdentity clearLocalCallingIdentity() {
Jeff Sharkey667618e2019-08-20 08:46:35 -0600990 return clearLocalCallingIdentity(LocalCallingIdentity.fromSelf(getContext()));
Jeff Sharkey0ee97412019-05-20 14:00:12 -0600991 }
992
993 public LocalCallingIdentity clearLocalCallingIdentity(LocalCallingIdentity replacement) {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -0600994 final LocalCallingIdentity token = mCallingIdentity.get();
Jeff Sharkey0ee97412019-05-20 14:00:12 -0600995 mCallingIdentity.set(replacement);
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -0600996 return token;
997 }
998
999 public void restoreLocalCallingIdentity(LocalCallingIdentity token) {
1000 mCallingIdentity.set(token);
1001 }
1002
Jeff Sharkeybd262742019-12-17 16:40:29 -07001003 private boolean isPackageKnown(@NonNull String packageName) {
1004 final PackageManager pm = getContext().getPackageManager();
1005
1006 // First, is the app actually installed?
1007 try {
1008 pm.getPackageInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES);
1009 return true;
1010 } catch (NameNotFoundException ignored) {
1011 }
1012
1013 // Second, is the app pending, probably from a backup/restore operation?
1014 for (SessionInfo si : pm.getPackageInstaller().getAllSessions()) {
1015 if (Objects.equals(packageName, si.getAppPackageName())) {
1016 return true;
1017 }
1018 }
1019
1020 // I've never met this package in my life
1021 return false;
1022 }
1023
Jeff Sharkey99a48282019-03-22 15:11:53 -06001024 public void onIdleMaintenance(@NonNull CancellationSignal signal) {
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07001025 final long startTime = SystemClock.elapsedRealtime();
1026
Jeff Sharkey5278ead2020-01-07 16:40:18 -07001027 // Trim any stale log files before we emit new events below
1028 Logging.trimPersistent();
1029
Jeff Sharkeya0c3c3b2019-04-15 16:25:07 -06001030 // Scan all volumes to resolve any staleness
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06001031 for (String volumeName : getExternalVolumeNames()) {
Jeff Sharkeya0c3c3b2019-04-15 16:25:07 -06001032 // Possibly bail before digging into each volume
1033 signal.throwIfCanceled();
1034
1035 try {
Jeff Sharkeyc5c39142019-12-15 22:46:03 -07001036 MediaService.onScanVolume(getContext(), volumeName, REASON_IDLE);
Jeff Sharkeya0c3c3b2019-04-15 16:25:07 -06001037 } catch (IOException e) {
1038 Log.w(TAG, e);
1039 }
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07001040
1041 // Ensure that our thumbnails are valid
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001042 mExternalDatabase.runWithTransaction((db) -> {
1043 ensureThumbnailsValid(volumeName, db);
1044 return null;
1045 });
Jeff Sharkeya0c3c3b2019-04-15 16:25:07 -06001046 }
1047
Jeff Sharkey99a48282019-03-22 15:11:53 -06001048 // Delete any stale thumbnails
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001049 final int staleThumbnails = mExternalDatabase.runWithTransaction((db) -> {
1050 return pruneThumbnails(db, signal);
1051 });
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07001052 Log.d(TAG, "Pruned " + staleThumbnails + " unknown thumbnails");
Jeff Sharkey99a48282019-03-22 15:11:53 -06001053
Jeff Sharkey7320f372018-09-12 15:14:24 -06001054 // Finished orphaning any content whose package no longer exists
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001055 final int stalePackages = mExternalDatabase.runWithTransaction((db) -> {
1056 final ArraySet<String> unknownPackages = new ArraySet<>();
1057 try (Cursor c = db.query(true, "files", new String[] { "owner_package_name" },
1058 null, null, null, null, null, null, signal)) {
1059 while (c.moveToNext()) {
1060 final String packageName = c.getString(0);
1061 if (TextUtils.isEmpty(packageName)) continue;
Jeff Sharkeybd262742019-12-17 16:40:29 -07001062
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001063 if (!isPackageKnown(packageName)) {
1064 unknownPackages.add(packageName);
1065 }
Jeff Sharkey7320f372018-09-12 15:14:24 -06001066 }
1067 }
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001068 for (String packageName : unknownPackages) {
1069 onPackageOrphaned(db, packageName);
1070 }
1071 return unknownPackages.size();
1072 });
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07001073 Log.d(TAG, "Pruned " + stalePackages + " unknown packages");
Jeff Sharkey711d10f2019-01-04 16:09:52 -07001074
Jeff Sharkeyd4babd82019-05-14 12:37:07 -06001075 // Delete any expired content; we're paranoid about wildly changing
1076 // clocks, so only delete items within the last week
Jeff Sharkey71437302019-04-09 23:46:52 -06001077 final long from = ((System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS) / 1000);
1078 final long to = (System.currentTimeMillis() / 1000);
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001079 final int expiredMedia = mExternalDatabase.runWithTransaction((db) -> {
1080 try (Cursor c = db.query(true, "files", new String[] { "volume_name", "_id" },
1081 FileColumns.DATE_EXPIRES + " BETWEEN " + from + " AND " + to, null,
1082 null, null, null, null, signal)) {
1083 while (c.moveToNext()) {
1084 final String volumeName = c.getString(0);
1085 final long id = c.getLong(1);
1086 delete(Files.getContentUri(volumeName, id), null, null);
1087 }
1088 return c.getCount();
Jeff Sharkey711d10f2019-01-04 16:09:52 -07001089 }
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001090 });
1091 Log.d(TAG, "Deleted " + expiredMedia + " expired items");
Jeff Sharkeyd4babd82019-05-14 12:37:07 -06001092
1093 // Forget any stale volumes
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001094 mExternalDatabase.runWithTransaction((db) -> {
1095 final Set<String> recentVolumeNames = MediaStore
1096 .getRecentExternalVolumeNames(getContext());
1097 final Set<String> knownVolumeNames = new ArraySet<>();
1098 try (Cursor c = db.query(true, "files", new String[] { MediaColumns.VOLUME_NAME },
1099 null, null, null, null, null, null, signal)) {
1100 while (c.moveToNext()) {
1101 knownVolumeNames.add(c.getString(0));
1102 }
Jeff Sharkeyd4babd82019-05-14 12:37:07 -06001103 }
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001104 final Set<String> staleVolumeNames = new ArraySet<>();
1105 staleVolumeNames.addAll(knownVolumeNames);
1106 staleVolumeNames.removeAll(recentVolumeNames);
1107 for (String staleVolumeName : staleVolumeNames) {
1108 final int num = db.delete("files", FileColumns.VOLUME_NAME + "=?",
1109 new String[] { staleVolumeName });
1110 Log.d(TAG, "Forgot " + num + " stale items from " + staleVolumeName);
1111 }
1112 return null;
1113 });
Jeff Sharkeyc5793122019-08-19 15:58:35 -06001114
1115 synchronized (mDirectoryCache) {
1116 mDirectoryCache.clear();
1117 }
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07001118
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001119 final long itemCount = mExternalDatabase.runWithTransaction((db) -> {
1120 return DatabaseHelper.getItemCount(db);
1121 });
1122
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07001123 final long durationMillis = (SystemClock.elapsedRealtime() - startTime);
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001124 Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount,
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07001125 durationMillis, staleThumbnails, expiredMedia);
Jeff Sharkey7320f372018-09-12 15:14:24 -06001126 }
1127
1128 public void onPackageOrphaned(String packageName) {
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001129 mExternalDatabase.runWithTransaction((db) -> {
1130 onPackageOrphaned(db, packageName);
1131 return null;
1132 });
1133 }
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06001134
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001135 public void onPackageOrphaned(@NonNull SQLiteDatabase db, @NonNull String packageName) {
Jeff Sharkey7320f372018-09-12 15:14:24 -06001136 final ContentValues values = new ContentValues();
1137 values.putNull(FileColumns.OWNER_PACKAGE_NAME);
1138
Jeff Sharkey71437302019-04-09 23:46:52 -06001139 final int count = db.update("files", values,
1140 "owner_package_name=?", new String[] { packageName });
1141 if (count > 0) {
1142 Log.d(TAG, "Orphaned " + count + " items belonging to "
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06001143 + packageName + " on " + db.getPath());
Jeff Sharkey7320f372018-09-12 15:14:24 -06001144 }
1145 }
1146
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07001147 public void scanDirectory(File file, int reason) {
1148 mMediaScanner.scanDirectory(file, reason);
Jeff Sharkey85acbbe2019-10-15 17:10:30 -06001149 }
1150
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07001151 public Uri scanFile(File file, int reason) {
1152 return mMediaScanner.scanFile(file, reason);
Jeff Sharkey85acbbe2019-10-15 17:10:30 -06001153 }
1154
Sahana Rao0ccfdd82020-02-17 10:34:42 +00001155 public Uri scanFile(File file, int reason, String ownerPackage) {
1156 return mMediaScanner.scanFile(file, reason, ownerPackage);
1157 }
1158
shafik15e2d612019-10-31 20:10:25 +00001159 /**
1160 * Makes MediaScanner scan the given file.
1161 * @param file path of the file to be scanned
shafik15e2d612019-10-31 20:10:25 +00001162 *
1163 * Called from JNI in jni/MediaProviderWrapper.cpp
1164 */
1165 @Keep
Sahana Raoee321362020-03-05 19:27:37 +00001166 public void scanFileForFuse(String file) {
1167 scanFile(new File(file), REASON_DEMAND);
shafik15e2d612019-10-31 20:10:25 +00001168 }
1169
shafik77ed67b2020-02-06 18:27:35 +00001170 /**
Martijn Coenenaf2d34d2020-06-19 12:52:20 +02001171 * Called when a new file is created through FUSE
1172 *
1173 * @param file path of the file that was created
1174 *
1175 * Called from JNI in jni/MediaProviderWrapper.cpp
1176 */
1177 @Keep
1178 public void onFileCreatedForFuse(String path) {
1179 // Make sure we update the quota type of the file
1180 BackgroundThread.getExecutor().execute(() -> {
1181 File file = new File(path);
1182 int mediaType = MimeUtils.resolveMediaType(MimeUtils.resolveMimeType(file));
1183 updateQuotaTypeForFileInternal(file, mediaType);
1184 });
1185 }
1186
1187 /**
shafik77ed67b2020-02-06 18:27:35 +00001188 * Returns true if the app denoted by the given {@code uid} and {@code packageName} is allowed
1189 * to clear other apps' cache directories.
1190 */
1191 static boolean hasPermissionToClearCaches(Context context, ApplicationInfo ai) {
shafikd84da092020-04-29 17:53:30 +01001192 PermissionUtils.setOpDescription("clear app cache");
1193 try {
Jeff Sharkey8411c402020-04-29 22:12:36 -06001194 return PermissionUtils.checkPermissionManager(context, /* pid */ -1, ai.uid,
1195 ai.packageName, /* attributionTag */ null);
shafikd84da092020-04-29 17:53:30 +01001196 } finally {
1197 PermissionUtils.clearOpDescription();
1198 }
shafik77ed67b2020-02-06 18:27:35 +00001199 }
1200
Jeff Sharkey58f533a2018-08-06 18:31:51 -06001201 @VisibleForTesting
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06001202 void computeAudioLocalizedValues(ContentValues values) {
1203 try {
1204 final String title = values.getAsString(AudioColumns.TITLE);
1205 final String titleRes = values.getAsString(AudioColumns.TITLE_RESOURCE_URI);
1206
1207 if (!TextUtils.isEmpty(titleRes)) {
1208 final String localized = getLocalizedTitle(titleRes);
1209 if (!TextUtils.isEmpty(localized)) {
1210 values.put(AudioColumns.TITLE, localized);
1211 }
1212 } else {
1213 final String localized = getLocalizedTitle(title);
1214 if (!TextUtils.isEmpty(localized)) {
1215 values.put(AudioColumns.TITLE, localized);
1216 values.put(AudioColumns.TITLE_RESOURCE_URI, title);
1217 }
1218 }
1219 } catch (Exception e) {
1220 Log.w(TAG, "Failed to localize title", e);
1221 }
1222 }
1223
1224 @VisibleForTesting
1225 static void computeAudioKeyValues(ContentValues values) {
1226 computeAudioKeyValue(values,
1227 AudioColumns.TITLE, AudioColumns.TITLE_KEY, null);
1228 computeAudioKeyValue(values,
1229 AudioColumns.ALBUM, AudioColumns.ALBUM_KEY, AudioColumns.ALBUM_ID);
1230 computeAudioKeyValue(values,
1231 AudioColumns.ARTIST, AudioColumns.ARTIST_KEY, AudioColumns.ARTIST_ID);
1232 computeAudioKeyValue(values,
1233 AudioColumns.GENRE, AudioColumns.GENRE_KEY, AudioColumns.GENRE_ID);
1234 }
1235
1236 private static void computeAudioKeyValue(@NonNull ContentValues values, @NonNull String focus,
1237 @Nullable String focusKey, @Nullable String focusId) {
1238 if (focusKey != null) values.remove(focusKey);
1239 if (focusId != null) values.remove(focusId);
1240
1241 final String value = values.getAsString(focus);
1242 if (TextUtils.isEmpty(value)) return;
1243
1244 final String key = Audio.keyFor(value);
1245 if (key == null) return;
1246
1247 if (focusKey != null) {
1248 values.put(focusKey, key);
1249 }
1250 if (focusId != null) {
1251 // Many apps break if we generate negative IDs, so trim off the
1252 // highest bit to ensure we're always unsigned
1253 final long id = Hashing.farmHashFingerprint64()
Jeff Sharkeyb3e66032020-05-03 11:34:41 -06001254 .hashString(key, StandardCharsets.UTF_8).asLong() & ~(1L << 63);
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06001255 values.put(focusId, id);
1256 }
1257 }
1258
Marco Nelissen01e706a2013-09-12 15:38:42 -07001259 @Override
1260 public Uri canonicalize(Uri uri) {
Jeff Sharkey94461582018-07-12 14:34:47 -06001261 final boolean allowHidden = isCallingPackageAllowedHidden();
1262 final int match = matchUri(uri, allowHidden);
Jeff Sharkey313eec82019-05-12 12:25:57 -06001263
1264 // Skip when we have nothing to canonicalize
1265 if ("1".equals(uri.getQueryParameter(CANONICAL))) {
1266 return uri;
1267 }
1268
Jeff Sharkey6378ccb2019-03-20 13:47:36 -06001269 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
1270 switch (match) {
1271 case AUDIO_MEDIA_ID: {
1272 final String title = getDefaultTitleFromCursor(c);
1273 if (!TextUtils.isEmpty(title)) {
1274 final Uri.Builder builder = uri.buildUpon();
1275 builder.appendQueryParameter(AudioColumns.TITLE, title);
1276 builder.appendQueryParameter(CANONICAL, "1");
1277 return builder.build();
1278 }
Jeff Sharkeyb3e66032020-05-03 11:34:41 -06001279 break;
Jeff Sharkey6378ccb2019-03-20 13:47:36 -06001280 }
1281 case VIDEO_MEDIA_ID:
1282 case IMAGES_MEDIA_ID: {
1283 final String documentId = c
1284 .getString(c.getColumnIndexOrThrow(MediaColumns.DOCUMENT_ID));
1285 if (!TextUtils.isEmpty(documentId)) {
1286 final Uri.Builder builder = uri.buildUpon();
1287 builder.appendQueryParameter(MediaColumns.DOCUMENT_ID, documentId);
1288 builder.appendQueryParameter(CANONICAL, "1");
1289 return builder.build();
1290 }
Jeff Sharkeyb3e66032020-05-03 11:34:41 -06001291 break;
Jeff Sharkey4b1921d2018-12-11 12:24:46 -07001292 }
1293 }
Jeff Sharkey6378ccb2019-03-20 13:47:36 -06001294 } catch (FileNotFoundException e) {
1295 Log.w(TAG, e.getMessage());
Mattias Nilssona79fcf12014-03-26 17:18:35 +01001296 }
Jeff Sharkey4b1921d2018-12-11 12:24:46 -07001297 return null;
Marco Nelissen01e706a2013-09-12 15:38:42 -07001298 }
1299
1300 @Override
1301 public Uri uncanonicalize(Uri uri) {
Jeff Sharkey94461582018-07-12 14:34:47 -06001302 final boolean allowHidden = isCallingPackageAllowedHidden();
1303 final int match = matchUri(uri, allowHidden);
1304
Jeff Sharkey6378ccb2019-03-20 13:47:36 -06001305 // Skip when we have nothing to uncanonicalize
1306 if (!"1".equals(uri.getQueryParameter(CANONICAL))) {
1307 return uri;
1308 }
Marco Nelissen01e706a2013-09-12 15:38:42 -07001309
Jeff Sharkey6378ccb2019-03-20 13:47:36 -06001310 // Extract values and then clear to avoid recursive lookups
1311 final String title = uri.getQueryParameter(AudioColumns.TITLE);
1312 final String documentId = uri.getQueryParameter(MediaColumns.DOCUMENT_ID);
1313 uri = uri.buildUpon().clearQuery().build();
1314
1315 switch (match) {
1316 case AUDIO_MEDIA_ID: {
1317 // First check for an exact match
1318 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
1319 if (Objects.equals(title, getDefaultTitleFromCursor(c))) {
1320 return uri;
1321 }
1322 } catch (FileNotFoundException e) {
1323 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e);
Mattias Nilssona79fcf12014-03-26 17:18:35 +01001324 }
Marco Nelissen01e706a2013-09-12 15:38:42 -07001325
Jeff Sharkey6378ccb2019-03-20 13:47:36 -06001326 // Otherwise fallback to searching
1327 final Uri baseUri = ContentUris.removeId(uri);
1328 try (Cursor c = queryForSingleItem(baseUri,
1329 new String[] { BaseColumns._ID },
1330 AudioColumns.TITLE + "=?", new String[] { title }, null)) {
1331 return ContentUris.withAppendedId(baseUri, c.getLong(0));
1332 } catch (FileNotFoundException e) {
1333 Log.w(TAG, "Failed to resolve " + uri + ": " + e);
Mattias Nilssona79fcf12014-03-26 17:18:35 +01001334 return null;
1335 }
Jeff Sharkey6378ccb2019-03-20 13:47:36 -06001336 }
1337 case VIDEO_MEDIA_ID:
1338 case IMAGES_MEDIA_ID: {
1339 // First check for an exact match
1340 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) {
1341 if (Objects.equals(title, getDefaultTitleFromCursor(c))) {
1342 return uri;
1343 }
1344 } catch (FileNotFoundException e) {
1345 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e);
1346 }
1347
1348 // Otherwise fallback to searching
1349 final Uri baseUri = ContentUris.removeId(uri);
1350 try (Cursor c = queryForSingleItem(baseUri,
1351 new String[] { BaseColumns._ID },
1352 MediaColumns.DOCUMENT_ID + "=?", new String[] { documentId }, null)) {
1353 return ContentUris.withAppendedId(baseUri, c.getLong(0));
1354 } catch (FileNotFoundException e) {
1355 Log.w(TAG, "Failed to resolve " + uri + ": " + e);
Mattias Nilssona79fcf12014-03-26 17:18:35 +01001356 return null;
1357 }
Marco Nelissen01e706a2013-09-12 15:38:42 -07001358 }
Marco Nelissen01e706a2013-09-12 15:38:42 -07001359 }
Jeff Sharkey6378ccb2019-03-20 13:47:36 -06001360
Marco Nelissen01e706a2013-09-12 15:38:42 -07001361 return uri;
1362 }
1363
1364 private Uri safeUncanonicalize(Uri uri) {
1365 Uri newUri = uncanonicalize(uri);
1366 if (newUri != null) {
1367 return newUri;
1368 }
1369 return uri;
1370 }
1371
Sahana Raoa82bd6a2019-10-10 18:10:37 +01001372 /**
Sahana Raob02e7152020-06-12 17:07:31 +01001373 * @return where clause to exclude database rows where
1374 * <ul>
1375 * <li> {@code column} is set or
1376 * <li> {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE and not owned by
1377 * calling package.
1378 * </ul>
1379 */
1380 private String getWhereClauseForMatchExclude(@NonNull String column) {
1381 if (column.equalsIgnoreCase(MediaColumns.IS_PENDING)) {
1382 final String callingPackage = getCallingPackageOrSelf();
1383 final String matchSharedPackagesClause = FileColumns.OWNER_PACKAGE_NAME + " IN "
Sahana Rao0bbd3e12020-06-06 15:56:44 +01001384 + getSharedPackages();
Sahana Raob02e7152020-06-12 17:07:31 +01001385 // Include owned pending files from Fuse
1386 return String.format("%s=0 OR (%s=1 AND %s AND %s)", column, column,
1387 MATCH_PENDING_FROM_FUSE, matchSharedPackagesClause);
1388 }
1389 return column + "=0";
1390 }
1391
1392 /**
Sahana Rao02fb8f42020-05-14 16:54:35 +01001393 * @return where clause to include database rows where
1394 * <ul>
1395 * <li> {@code column} is not set or
Sahana Raoea587fc2020-06-03 15:56:23 +01001396 * <li> {@code column} is set and calling package has write permission to corresponding db row
1397 * or {@code column} is {@link MediaColumns#IS_PENDING} and is set by FUSE.
Sahana Rao02fb8f42020-05-14 16:54:35 +01001398 * </ul>
1399 * The method is used to match db rows corresponding to writable pending and trashed files.
1400 */
1401 @Nullable
Sahana Raoea587fc2020-06-03 15:56:23 +01001402 private String getWhereClauseForMatchableVisibleFromFilePath(@NonNull Uri uri,
1403 @NonNull String column) {
Sahana Rao02fb8f42020-05-14 16:54:35 +01001404 if (isCallingPackageLegacyWrite() || checkCallingPermissionGlobal(uri, /*forWrite*/ true)) {
1405 // No special filtering needed
1406 return null;
1407 }
1408
1409 final String callingPackage = getCallingPackageOrSelf();
1410
1411 final ArrayList<String> options = new ArrayList<>();
1412 switch(matchUri(uri, isCallingPackageAllowedHidden())) {
1413 case IMAGES_MEDIA_ID:
1414 case IMAGES_MEDIA:
1415 case IMAGES_THUMBNAILS_ID:
1416 case IMAGES_THUMBNAILS:
1417 if (checkCallingPermissionImages(/*forWrite*/ true, callingPackage)) {
1418 // No special filtering needed
1419 return null;
1420 }
1421 break;
1422 case AUDIO_MEDIA_ID:
1423 case AUDIO_MEDIA:
1424 case AUDIO_PLAYLISTS_ID:
1425 case AUDIO_PLAYLISTS:
1426 if (checkCallingPermissionAudio(/*forWrite*/ true, callingPackage)) {
1427 // No special filtering needed
1428 return null;
1429 }
1430 break;
1431 case VIDEO_MEDIA_ID:
1432 case VIDEO_MEDIA:
1433 case VIDEO_THUMBNAILS_ID:
1434 case VIDEO_THUMBNAILS:
1435 if (checkCallingPermissionVideo(/*firWrite*/ true, callingPackage)) {
1436 // No special filtering needed
1437 return null;
1438 }
1439 break;
1440 case DOWNLOADS_ID:
1441 case DOWNLOADS:
1442 // No app has special permissions for downloads.
1443 break;
1444 case FILES_ID:
1445 case FILES:
1446 if (checkCallingPermissionAudio(/*forWrite*/ true, callingPackage)) {
1447 // Allow apps with audio permission to include audio* media types.
1448 options.add(DatabaseUtils.bindSelection("media_type=?",
1449 FileColumns.MEDIA_TYPE_AUDIO));
1450 options.add(DatabaseUtils.bindSelection("media_type=?",
1451 FileColumns.MEDIA_TYPE_PLAYLIST));
1452 options.add(DatabaseUtils.bindSelection("media_type=?",
1453 FileColumns.MEDIA_TYPE_SUBTITLE));
1454 }
1455 if (checkCallingPermissionVideo(/*forWrite*/ true, callingPackage)) {
1456 // Allow apps with video permission to include video* media types.
1457 options.add(DatabaseUtils.bindSelection("media_type=?",
1458 FileColumns.MEDIA_TYPE_VIDEO));
1459 options.add(DatabaseUtils.bindSelection("media_type=?",
1460 FileColumns.MEDIA_TYPE_SUBTITLE));
1461 }
1462 if (checkCallingPermissionImages(/*forWrite*/ true, callingPackage)) {
1463 // Allow apps with images permission to include images* media types.
1464 options.add(DatabaseUtils.bindSelection("media_type=?",
1465 FileColumns.MEDIA_TYPE_IMAGE));
1466 }
1467 break;
1468 default:
1469 // is_pending, is_trashed are not applicable for rest of the media tables.
1470 return null;
1471 }
1472
1473 final String matchSharedPackagesClause = FileColumns.OWNER_PACKAGE_NAME + " IN "
Sahana Rao0bbd3e12020-06-06 15:56:44 +01001474 + getSharedPackages();
Sahana Rao02fb8f42020-05-14 16:54:35 +01001475 options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause));
1476
Sahana Raoea587fc2020-06-03 15:56:23 +01001477 if (column.equalsIgnoreCase(MediaColumns.IS_PENDING)) {
1478 // Include all pending files from Fuse
Sahana Raob02e7152020-06-12 17:07:31 +01001479 options.add(MATCH_PENDING_FROM_FUSE);
Sahana Raoea587fc2020-06-03 15:56:23 +01001480 }
1481
Sahana Rao02fb8f42020-05-14 16:54:35 +01001482 final String matchWritableRowsClause = String.format("%s=0 OR (%s=1 AND %s)", column,
1483 column, TextUtils.join(" OR ", options));
1484 return matchWritableRowsClause;
1485 }
1486
1487 /**
Sahana Rao8a588e72019-12-06 11:32:56 +00001488 * Gets list of files in {@code path} from media provider database.
Sahana Raoa82bd6a2019-10-10 18:10:37 +01001489 *
Sahana Rao8a588e72019-12-06 11:32:56 +00001490 * @param path path of the directory.
1491 * @param uid UID of the calling process.
1492 * @return a list of file names in the given directory path.
1493 * An empty list is returned if no files are visible to the calling app or the given directory
1494 * does not have any files.
Sahana Rao81ceaf02019-12-23 12:37:06 +00001495 * A list with ["/"] is returned if the path is not indexed by MediaProvider database or
1496 * calling package is a legacy app and has appropriate storage permissions for the given path.
1497 * In both scenarios file names should be obtained from lower file system.
shafik63abf8b2020-03-02 15:44:37 +00001498 * A list with empty string[""] is returned if the calling package doesn't have access to the
1499 * given path.
1500 *
1501 * <p>Directory names are always obtained from lower file system.
Sahana Raoa82bd6a2019-10-10 18:10:37 +01001502 *
1503 * Called from JNI in jni/MediaProviderWrapper.cpp
1504 */
1505 @Keep
Sahana Rao8a588e72019-12-06 11:32:56 +00001506 public String[] getFilesInDirectoryForFuse(String path, int uid) {
Zim696dea42020-03-07 11:41:42 +00001507 final LocalCallingIdentity token =
shafikd84da092020-04-29 17:53:30 +01001508 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
Zim696dea42020-03-07 11:41:42 +00001509
Sahana Raoa82bd6a2019-10-10 18:10:37 +01001510 try {
Martijn Coenen9a1f6792020-03-17 07:13:47 +01001511 if (isPrivatePackagePathNotOwnedByCaller(path)) {
1512 return new String[] {""};
Sahana Rao81ceaf02019-12-23 12:37:06 +00001513 }
shafik575d0742019-11-25 17:02:57 +00001514
Ricky Waifeb9d9b2020-04-06 19:14:46 +01001515 // Do not allow apps to list Android/data or Android/obb dirs. Installer and
1516 // MOUNT_EXTERNAL_ANDROID_WRITABLE apps won't be blocked by this, as their OBB dirs
1517 // are mounted to lowerfs directly.
1518 if (isDataOrObbPath(path)) {
1519 return new String[] {""};
1520 }
1521
shafik63abf8b2020-03-02 15:44:37 +00001522 if (shouldBypassFuseRestrictions(/*forWrite*/ false, path)) {
1523 return new String[] {"/"};
1524 }
1525 // Legacy apps that made is this far don't have the right storage permission and hence
1526 // are not allowed to access anything other than their external app directory
1527 if (isCallingPackageRequestingLegacy()) {
1528 return new String[] {""};
1529 }
1530
Sahana Rao71693442019-11-13 13:48:07 +00001531 // Get relative path for the contents of given directory.
Abhijeet Kaur3bc15772021-11-17 08:40:34 +00001532 String relativePath = extractRelativePathWithDisplayName(path);
Sahana Raoa82bd6a2019-10-10 18:10:37 +01001533
Sahana Rao71693442019-11-13 13:48:07 +00001534 if (relativePath == null) {
Sahana Rao8a588e72019-12-06 11:32:56 +00001535 // Path is /storage/emulated/, if relativePath is null, MediaProvider doesn't
1536 // have any details about the given directory. Use lower file system to obtain
1537 // files and directories in the given directory.
Sahana Rao71693442019-11-13 13:48:07 +00001538 return new String[] {"/"};
Sahana Rao71693442019-11-13 13:48:07 +00001539 }
Sahana Rao8a588e72019-12-06 11:32:56 +00001540
1541 // For all other paths, get file names from media provider database.
1542 // Return media and non-media files visible to the calling package.
1543 ArrayList<String> fileNamesList = new ArrayList<>();
Sahana Rao8a588e72019-12-06 11:32:56 +00001544
Sahana Rao44c1d6f2020-05-14 18:42:00 +01001545 // Only FileColumns.DATA contains actual name of the file.
1546 String[] projection = {MediaColumns.DATA};
1547
Sahana Rao8a588e72019-12-06 11:32:56 +00001548 Bundle queryArgs = new Bundle();
1549 queryArgs.putString(QUERY_ARG_SQL_SELECTION, MediaColumns.RELATIVE_PATH +
1550 " =? and mime_type not like 'null'");
1551 queryArgs.putStringArray(QUERY_ARG_SQL_SELECTION_ARGS, new String[] {relativePath});
Sahana Rao44c1d6f2020-05-14 18:42:00 +01001552 // Get database entries for files from MediaProvider database with
1553 // MediaColumns.RELATIVE_PATH as the given path.
shafik536982a2020-05-14 17:54:05 +01001554 try (final Cursor cursor = query(FileUtils.getContentUriForPath(path), projection,
Sahana Rao8a588e72019-12-06 11:32:56 +00001555 queryArgs, null)) {
1556 while(cursor.moveToNext()) {
Sahana Rao44c1d6f2020-05-14 18:42:00 +01001557 fileNamesList.add(extractDisplayName(cursor.getString(0)));
Sahana Rao8a588e72019-12-06 11:32:56 +00001558 }
1559 }
1560 return fileNamesList.toArray(new String[fileNamesList.size()]);
Sahana Raoa82bd6a2019-10-10 18:10:37 +01001561 } finally {
1562 restoreLocalCallingIdentity(token);
1563 }
Sahana Raoa82bd6a2019-10-10 18:10:37 +01001564 }
1565
Sahana Rao2c416032019-12-31 13:41:00 +00001566 /**
Martijn Coenen070bce12020-06-08 21:18:24 +02001567 * Scan files during directory renames for the following reasons:
Sahana Raoaeded752020-04-29 17:28:15 +01001568 * <ul>
Sahana Rao8ff51bc2020-05-14 23:53:50 +01001569 * <li>Because we don't update db rows for directories, we scan the oldPath to discard stale
1570 * directory db rows. This prevents conflicts during subsequent db operations with oldPath.
Martijn Coenen070bce12020-06-08 21:18:24 +02001571 * <li>We need to scan newPath as well, because the new directory may have become hidden
1572 * or unhidden, in which case we need to update the media types of the contained files
Sahana Raoaeded752020-04-29 17:28:15 +01001573 * </ul>
1574 */
Martijn Coenen070bce12020-06-08 21:18:24 +02001575 private void scanRenamedDirectoryForFuse(@NonNull String oldPath, @NonNull String newPath) {
Sahana Raoaeded752020-04-29 17:28:15 +01001576 final LocalCallingIdentity token = clearLocalCallingIdentity();
1577 try {
Martijn Coenen070bce12020-06-08 21:18:24 +02001578 scanFile(new File(oldPath), REASON_DEMAND);
1579 scanFile(new File(newPath), REASON_DEMAND);
Sahana Raoaeded752020-04-29 17:28:15 +01001580 } finally {
1581 restoreLocalCallingIdentity(token);
1582 }
1583 }
1584
1585 /**
Sahana Rao5b0b9652019-12-31 17:49:25 +00001586 * Checks if given {@code mimeType} is supported in {@code path}.
1587 */
1588 private boolean isMimeTypeSupportedInPath(String path, String mimeType) {
1589 final String supportedPrimaryMimeType;
Jeff Sharkeyc4a5f812020-05-03 21:07:14 -06001590 final int match = matchUri(getContentUriForFile(path, mimeType), true);
1591 switch (match) {
Sahana Rao5b0b9652019-12-31 17:49:25 +00001592 case AUDIO_MEDIA:
1593 supportedPrimaryMimeType = "audio";
1594 break;
1595 case VIDEO_MEDIA:
1596 supportedPrimaryMimeType = "video";
1597 break;
1598 case IMAGES_MEDIA:
1599 supportedPrimaryMimeType = "image";
1600 break;
1601 default:
1602 supportedPrimaryMimeType = ClipDescription.MIMETYPE_UNKNOWN;
1603 }
Jeff Sharkeyc4a5f812020-05-03 21:07:14 -06001604 return (supportedPrimaryMimeType.equalsIgnoreCase(ClipDescription.MIMETYPE_UNKNOWN) ||
1605 MimeUtils.startsWithIgnoreCase(mimeType, supportedPrimaryMimeType));
Sahana Rao5b0b9652019-12-31 17:49:25 +00001606 }
1607
Sahana Rao1e8271b2020-04-03 14:01:08 +01001608 /**
1609 * Removes owner package for the renamed path if the calling package doesn't own the db row
1610 *
1611 * When oldPath is renamed to newPath, if newPath exists in the database, and caller is not the
1612 * owner of the file, owner package is set to 'null'. This prevents previous owner of newPath
1613 * from accessing renamed file.
1614 * @return {@code true} if
1615 * <ul>
1616 * <li> there is no corresponding database row for given {@code path}
1617 * <li> shared calling package is the owner of the database row
1618 * <li> owner package name is already set to 'null'
1619 * <li> updating owner package name to 'null' was successful.
1620 * </ul>
1621 * Returns {@code false} otherwise.
1622 */
1623 private boolean maybeRemoveOwnerPackageForFuseRename(@NonNull DatabaseHelper helper,
1624 @NonNull String path) {
1625
shafik536982a2020-05-14 17:54:05 +01001626 final Uri uri = FileUtils.getContentUriForPath(path);
Sahana Rao1e8271b2020-04-03 14:01:08 +01001627 final int match = matchUri(uri, isCallingPackageAllowedHidden());
1628 final String ownerPackageName;
1629 final String selection = MediaColumns.DATA + " =? AND "
1630 + MediaColumns.OWNER_PACKAGE_NAME + " != 'null'";
1631 final String[] selectionArgs = new String[] {path};
1632
1633 final SQLiteQueryBuilder qbForQuery =
1634 getQueryBuilder(TYPE_QUERY, match, uri, Bundle.EMPTY, null);
1635 try (Cursor c = qbForQuery.query(helper, new String[] {FileColumns.OWNER_PACKAGE_NAME},
1636 selection, selectionArgs, null, null, null, null, null)) {
1637 if (!c.moveToFirst()) {
1638 // We don't need to remove owner_package from db row if path doesn't exist in
1639 // database or owner_package is already set to 'null'
1640 return true;
1641 }
1642 ownerPackageName = c.getString(0);
1643 if (isCallingIdentitySharedPackageName(ownerPackageName)) {
1644 // We don't need to remove owner_package from db row if calling package is the owner
1645 // of the database row
1646 return true;
1647 }
1648 }
1649
1650 final SQLiteQueryBuilder qbForUpdate =
1651 getQueryBuilder(TYPE_UPDATE, match, uri, Bundle.EMPTY, null);
1652 ContentValues values = new ContentValues();
1653 values.put(FileColumns.OWNER_PACKAGE_NAME, "null");
1654 return qbForUpdate.update(helper, values, selection, selectionArgs) == 1;
1655 }
1656
shafikac34fe92020-02-25 15:28:55 +00001657 private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
1658 @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) {
1659 return updateDatabaseForFuseRename(helper, oldPath, newPath, values, Bundle.EMPTY);
1660 }
1661
Sahana Rao5b0b9652019-12-31 17:49:25 +00001662 /**
1663 * Updates database entry for given {@code path} with {@code values}
1664 */
Jeff Sharkey021e68f2020-01-14 18:21:50 -07001665 private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
shafikac34fe92020-02-25 15:28:55 +00001666 @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values,
1667 @NonNull Bundle qbExtras) {
shafik536982a2020-05-14 17:54:05 +01001668 final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath);
Sahana Rao5b0b9652019-12-31 17:49:25 +00001669 boolean allowHidden = isCallingPackageAllowedHidden();
1670 final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE,
shafikac34fe92020-02-25 15:28:55 +00001671 matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null);
Sahana Rao5b0b9652019-12-31 17:49:25 +00001672 final String selection = MediaColumns.DATA + " =? ";
1673 int count = 0;
1674 boolean retryUpdateWithReplace = false;
1675
1676 try {
Sahana Rao50326572020-02-25 13:07:38 +00001677 // TODO(b/146777893): System gallery apps can rename a media directory containing
1678 // non-media files. This update doesn't support updating non-media files that are not
1679 // owned by system gallery app.
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001680 count = qbForUpdate.update(helper, values, selection, new String[]{oldPath});
Sahana Rao5b0b9652019-12-31 17:49:25 +00001681 } catch (SQLiteConstraintException e) {
1682 Log.w(TAG, "Database update failed while renaming " + oldPath, e);
1683 retryUpdateWithReplace = true;
1684 }
1685
1686 if (retryUpdateWithReplace) {
1687 // We are replacing file in newPath with file in oldPath. If calling package has
1688 // write permission for newPath, delete existing database entry and retry update.
shafik536982a2020-05-14 17:54:05 +01001689 final Uri uriNewPath = FileUtils.getContentUriForPath(oldPath);
Sahana Rao5b0b9652019-12-31 17:49:25 +00001690 final SQLiteQueryBuilder qbForDelete = getQueryBuilder(TYPE_DELETE,
shafikac34fe92020-02-25 15:28:55 +00001691 matchUri(uriNewPath, allowHidden), uriNewPath, qbExtras, null);
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001692 if (qbForDelete.delete(helper, selection, new String[] {newPath}) == 1) {
Sahana Rao5b0b9652019-12-31 17:49:25 +00001693 Log.i(TAG, "Retrying database update after deleting conflicting entry");
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001694 count = qbForUpdate.update(helper, values, selection, new String[]{oldPath});
Sahana Rao5b0b9652019-12-31 17:49:25 +00001695 } else {
1696 return false;
1697 }
1698 }
1699 return count == 1;
1700 }
1701
1702 /**
1703 * Gets {@link ContentValues} for updating database entry to {@code path}.
1704 */
Martijn Coenen070bce12020-06-08 21:18:24 +02001705 private ContentValues getContentValuesForFuseRename(String path, String newMimeType,
1706 boolean checkHidden) {
Sahana Rao5b0b9652019-12-31 17:49:25 +00001707 ContentValues values = new ContentValues();
1708 values.put(MediaColumns.MIME_TYPE, newMimeType);
1709 values.put(MediaColumns.DATA, path);
1710
Martijn Coenen070bce12020-06-08 21:18:24 +02001711 if (checkHidden && shouldFileBeHidden(new File(path))) {
1712 values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
1713 } else {
Sahana Rao5b0b9652019-12-31 17:49:25 +00001714 int mediaType = MimeUtils.resolveMediaType(newMimeType);
1715 values.put(FileColumns.MEDIA_TYPE, mediaType);
1716 }
1717 final boolean allowHidden = isCallingPackageAllowedHidden();
Jeff Sharkeyc4a5f812020-05-03 21:07:14 -06001718 if (!newMimeType.equalsIgnoreCase("null") &&
Sahana Rao5b0b9652019-12-31 17:49:25 +00001719 matchUri(getContentUriForFile(path, newMimeType), allowHidden) == AUDIO_MEDIA) {
1720 computeAudioLocalizedValues(values);
1721 computeAudioKeyValues(values);
1722 }
Sahana Raoea587fc2020-06-03 15:56:23 +01001723 FileUtils.computeValuesFromData(values, isFuseThread());
Sahana Rao5b0b9652019-12-31 17:49:25 +00001724 return values;
1725 }
1726
shafikac34fe92020-02-25 15:28:55 +00001727 private ArrayList<String> getIncludedDefaultDirectories() {
1728 final ArrayList<String> includedDefaultDirs = new ArrayList<>();
1729 if (checkCallingPermissionVideo(/*forWrite*/ true, null)) {
1730 includedDefaultDirs.add(DIRECTORY_DCIM);
1731 includedDefaultDirs.add(DIRECTORY_PICTURES);
1732 includedDefaultDirs.add(DIRECTORY_MOVIES);
1733 } else if (checkCallingPermissionImages(/*forWrite*/ true, null)) {
1734 includedDefaultDirs.add(DIRECTORY_DCIM);
1735 includedDefaultDirs.add(DIRECTORY_PICTURES);
1736 }
1737 return includedDefaultDirs;
1738 }
1739
Sahana Rao5b0b9652019-12-31 17:49:25 +00001740 /**
Sahana Rao50326572020-02-25 13:07:38 +00001741 * Gets all files in the given {@code path} and subdirectories of the given {@code path}.
1742 */
1743 private ArrayList<String> getAllFilesForRenameDirectory(String oldPath) {
1744 final String selection = MediaColumns.RELATIVE_PATH + " REGEXP '^" +
Abhijeet Kaur3bc15772021-11-17 08:40:34 +00001745 extractRelativePathWithDisplayName(oldPath) + "/?.*' and mime_type not like 'null'";
Sahana Rao50326572020-02-25 13:07:38 +00001746 ArrayList<String> fileList = new ArrayList<>();
1747
1748 final LocalCallingIdentity token = clearLocalCallingIdentity();
shafik536982a2020-05-14 17:54:05 +01001749 try (final Cursor c = query(FileUtils.getContentUriForPath(oldPath),
Sahana Rao50326572020-02-25 13:07:38 +00001750 new String[] {MediaColumns.DATA}, selection, null, null)) {
1751 while (c.moveToNext()) {
1752 final String filePath = c.getString(0).replaceFirst("^" + oldPath + "/(.*)", "$1");
1753 fileList.add(filePath);
1754 }
1755 } finally {
1756 restoreLocalCallingIdentity(token);
1757 }
1758 return fileList;
1759 }
1760
1761 /**
Sahana Rao182ec6b2020-01-03 15:00:46 +00001762 * Gets files in the given {@code path} and subdirectories of the given {@code path} for which
1763 * calling package has write permissions.
1764 *
1765 * This method throws {@code IllegalArgumentException} if the directory has one or more
1766 * files for which calling package doesn't have write permission or if file type is not
1767 * supported in {@code newPath}
1768 */
Sahana Rao50326572020-02-25 13:07:38 +00001769 private ArrayList<String> getWritableFilesForRenameDirectory(String oldPath, String newPath)
1770 throws IllegalArgumentException {
shafikac34fe92020-02-25 15:28:55 +00001771 // Try a simple check to see if the caller has full access to the given collections first
1772 // before falling back to performing a query to probe for access.
Abhijeet Kaur3bc15772021-11-17 08:40:34 +00001773 final String oldRelativePath = extractRelativePathWithDisplayName(oldPath);
1774 final String newRelativePath = extractRelativePathWithDisplayName(newPath);
shafikac34fe92020-02-25 15:28:55 +00001775 boolean hasFullAccessToOldPath = false;
1776 boolean hasFullAccessToNewPath = false;
1777 for (String defaultDir : getIncludedDefaultDirectories()) {
1778 if (oldRelativePath.startsWith(defaultDir)) hasFullAccessToOldPath = true;
1779 if (newRelativePath.startsWith(defaultDir)) hasFullAccessToNewPath = true;
1780 }
1781 if (hasFullAccessToNewPath && hasFullAccessToOldPath) {
1782 return getAllFilesForRenameDirectory(oldPath);
1783 }
1784
Sahana Rao182ec6b2020-01-03 15:00:46 +00001785 final int countAllFilesInDirectory;
1786 final String selection = MediaColumns.RELATIVE_PATH + " REGEXP '^" +
Abhijeet Kaur3bc15772021-11-17 08:40:34 +00001787 extractRelativePathWithDisplayName(oldPath) + "/?.*' and mime_type not like 'null'";
shafik536982a2020-05-14 17:54:05 +01001788 final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath);
Sahana Rao182ec6b2020-01-03 15:00:46 +00001789
1790 final LocalCallingIdentity token = clearLocalCallingIdentity();
1791 try (final Cursor c = query(uriOldPath, new String[] {MediaColumns._ID}, selection, null,
1792 null)) {
1793 // get actual number of files in the given directory.
1794 countAllFilesInDirectory = c.getCount();
1795 } finally {
1796 restoreLocalCallingIdentity(token);
1797 }
1798
1799 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE,
1800 matchUri(uriOldPath, isCallingPackageAllowedHidden()), uriOldPath, Bundle.EMPTY,
1801 null);
1802 final DatabaseHelper helper;
1803 try {
1804 helper = getDatabaseForUri(uriOldPath);
1805 } catch (VolumeNotFoundException e) {
1806 throw new IllegalStateException("Volume not found while querying files for renaming "
1807 + oldPath);
1808 }
1809
1810 ArrayList<String> fileList = new ArrayList<>();
1811 final String[] projection = {MediaColumns.DATA, MediaColumns.MIME_TYPE};
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001812 try (Cursor c = qb.query(helper, projection, selection, null,
1813 null, null, null, null, null)) {
Sahana Rao182ec6b2020-01-03 15:00:46 +00001814 // Check if the calling package has write permission to all files in the given
1815 // directory. If calling package has write permission to all files in the directory, the
1816 // query with update uri should return same number of files as previous query.
1817 if (c.getCount() != countAllFilesInDirectory) {
1818 throw new IllegalArgumentException("Calling package doesn't have write permission "
1819 + " to rename one or more files in " + oldPath);
1820 }
1821 while(c.moveToNext()) {
1822 final String filePath = c.getString(0).replaceFirst("^" + oldPath + "/(.*)", "$1");
1823 final String mimeType = c.getString(1);
1824 if (!isMimeTypeSupportedInPath(newPath + "/" + filePath, mimeType)) {
1825 throw new IllegalArgumentException("Can't rename " + oldPath + "/" + filePath
1826 + ". Mime type " + mimeType + " not supported in " + newPath);
1827 }
1828 fileList.add(filePath);
1829 }
1830 }
1831 return fileList;
1832 }
1833
Sahana Raoddc85822020-01-06 20:22:46 +00001834 private int renameInLowerFs(String oldPath, String newPath) {
1835 try {
1836 Os.rename(oldPath, newPath);
1837 return 0;
1838 } catch (ErrnoException e) {
1839 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed.";
1840 Log.e(TAG, errorMessage, e);
shafike4fb1462020-01-29 16:25:23 +00001841 return e.errno;
Sahana Raoddc85822020-01-06 20:22:46 +00001842 }
1843 }
1844
Sahana Rao182ec6b2020-01-03 15:00:46 +00001845 /**
1846 * Rename directory from {@code oldPath} to {@code newPath}.
1847 *
1848 * Renaming a directory is only allowed if calling package has write permission to all files in
1849 * the given directory tree and all file types in the given directory tree are supported by the
1850 * top level directory of new path. Renaming a directory is split into three steps:
1851 * 1. Check calling package's permissions for all files in the given directory tree. Also check
1852 * file type support for all files in the {@code newPath}.
1853 * 2. Try updating database for all files in the directory.
1854 * 3. Rename the directory in lower file system. If rename in the lower file system is
1855 * successful, commit database update.
1856 *
1857 * @param oldPath path of the directory to be renamed.
1858 * @param newPath new path of directory to be renamed.
1859 * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed.
1860 * <ul>
1861 * <li>{@link OsConstants#EPERM} Renaming a directory with file types not supported by
1862 * {@code newPath} or renaming a directory with files for which calling package doesn't have
1863 * write permission.
1864 * This method can also return errno returned from {@code Os.rename} function.
1865 */
Sahana Rao50326572020-02-25 13:07:38 +00001866 private int renameDirectoryCheckedForFuse(String oldPath, String newPath) {
Sahana Rao182ec6b2020-01-03 15:00:46 +00001867 final ArrayList<String> fileList;
1868 try {
1869 fileList = getWritableFilesForRenameDirectory(oldPath, newPath);
Sahana Rao50326572020-02-25 13:07:38 +00001870 } catch (IllegalArgumentException e) {
Sahana Rao182ec6b2020-01-03 15:00:46 +00001871 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. ";
1872 Log.e(TAG, errorMessage, e);
shafike4fb1462020-01-29 16:25:23 +00001873 return OsConstants.EPERM;
Sahana Rao182ec6b2020-01-03 15:00:46 +00001874 }
1875
Sahana Rao50326572020-02-25 13:07:38 +00001876 return renameDirectoryUncheckedForFuse(oldPath, newPath, fileList);
1877 }
1878
1879 private int renameDirectoryUncheckedForFuse(String oldPath, String newPath,
1880 ArrayList<String> fileList) {
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001881 final DatabaseHelper helper;
Sahana Rao182ec6b2020-01-03 15:00:46 +00001882 try {
shafik536982a2020-05-14 17:54:05 +01001883 helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath));
Sahana Rao182ec6b2020-01-03 15:00:46 +00001884 } catch (VolumeNotFoundException e) {
1885 throw new IllegalStateException("Volume not found while trying to update database for "
1886 + oldPath, e);
1887 }
1888
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001889 helper.beginTransaction();
Sahana Rao182ec6b2020-01-03 15:00:46 +00001890 try {
shafikac34fe92020-02-25 15:28:55 +00001891 final Bundle qbExtras = new Bundle();
1892 qbExtras.putStringArrayList(INCLUDED_DEFAULT_DIRECTORIES,
1893 getIncludedDefaultDirectories());
Sahana Rao182ec6b2020-01-03 15:00:46 +00001894 for (String filePath : fileList) {
1895 final String newFilePath = newPath + "/" + filePath;
1896 final String mimeType = MimeUtils.resolveMimeType(new File(newFilePath));
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001897 if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath,
Martijn Coenen070bce12020-06-08 21:18:24 +02001898 getContentValuesForFuseRename(newFilePath, mimeType,
1899 false /* checkHidden - will be fixed up below */), qbExtras)) {
Sahana Rao182ec6b2020-01-03 15:00:46 +00001900 Log.e(TAG, "Calling package doesn't have write permission to rename file.");
shafike4fb1462020-01-29 16:25:23 +00001901 return OsConstants.EPERM;
Sahana Rao182ec6b2020-01-03 15:00:46 +00001902 }
1903 }
1904
1905 // Rename the directory in lower file system.
Sahana Raoddc85822020-01-06 20:22:46 +00001906 int errno = renameInLowerFs(oldPath, newPath);
1907 if (errno == 0) {
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001908 helper.setTransactionSuccessful();
Sahana Raoddc85822020-01-06 20:22:46 +00001909 } else {
1910 return errno;
Sahana Rao182ec6b2020-01-03 15:00:46 +00001911 }
Sahana Rao182ec6b2020-01-03 15:00:46 +00001912 } finally {
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001913 helper.endTransaction();
Sahana Rao182ec6b2020-01-03 15:00:46 +00001914 }
Martijn Coenen070bce12020-06-08 21:18:24 +02001915 // Directory movement might have made new/old path hidden.
1916 scanRenamedDirectoryForFuse(oldPath, newPath);
Sahana Rao182ec6b2020-01-03 15:00:46 +00001917 return 0;
1918 }
Sahana Rao5b0b9652019-12-31 17:49:25 +00001919
1920 /**
1921 * Rename a file from {@code oldPath} to {@code newPath}.
1922 *
1923 * Renaming a file is split into three parts:
1924 * 1. Check if {@code newPath} supports new file type.
1925 * 2. Try updating database entry from {@code oldPath} to {@code newPath}. This update may fail
1926 * if calling package doesn't have write permission for {@code oldPath} and {@code newPath}.
1927 * 3. Rename the file in lower file system. If Rename in lower file system succeeds, commit
1928 * database update.
1929 * @param oldPath path of the file to be renamed.
1930 * @param newPath new path of the file to be renamed.
1931 * @return 0 on successful rename, appropriate negated errno value if the rename is not allowed.
1932 * <ul>
1933 * <li>{@link OsConstants#EPERM} Calling package doesn't have write permission for
1934 * {@code oldPath} or {@code newPath}, or file type is not supported by {@code newPath}.
1935 * This method can also return errno returned from {@code Os.rename} function.
1936 */
Sahana Rao50326572020-02-25 13:07:38 +00001937 private int renameFileCheckedForFuse(String oldPath, String newPath) {
Sahana Rao5b0b9652019-12-31 17:49:25 +00001938 // Check if new mime type is supported in new path.
1939 final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
1940 if (!isMimeTypeSupportedInPath(newPath, newMimeType)) {
shafike4fb1462020-01-29 16:25:23 +00001941 return OsConstants.EPERM;
Sahana Rao5b0b9652019-12-31 17:49:25 +00001942 }
Sahana Rao1e8271b2020-04-03 14:01:08 +01001943 return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ false) ;
Sahana Rao50326572020-02-25 13:07:38 +00001944 }
Sahana Rao5b0b9652019-12-31 17:49:25 +00001945
Sahana Rao50326572020-02-25 13:07:38 +00001946 private int renameFileUncheckedForFuse(String oldPath, String newPath) {
Sahana Rao1e8271b2020-04-03 14:01:08 +01001947 return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ true) ;
1948 }
1949
Martijn Coenen070bce12020-06-08 21:18:24 +02001950 private static boolean shouldFileBeHidden(@NonNull File file) {
1951 if (FileUtils.isFileHidden(file)) {
1952 return true;
1953 }
1954 File parent = file.getParentFile();
1955 while (parent != null) {
1956 if (FileUtils.isDirectoryHidden(parent)) {
1957 return true;
1958 }
1959 parent = parent.getParentFile();
1960 }
1961
1962 return false;
1963 }
1964
Sahana Rao1e8271b2020-04-03 14:01:08 +01001965 private int renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions) {
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001966 final DatabaseHelper helper;
Sahana Rao5b0b9652019-12-31 17:49:25 +00001967 try {
shafik536982a2020-05-14 17:54:05 +01001968 helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath));
Sahana Rao5b0b9652019-12-31 17:49:25 +00001969 } catch (VolumeNotFoundException e) {
Sahana Rao1e8271b2020-04-03 14:01:08 +01001970 throw new IllegalStateException("Failed to update database row with " + oldPath, e);
Sahana Rao5b0b9652019-12-31 17:49:25 +00001971 }
1972
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001973 helper.beginTransaction();
Sahana Rao5b0b9652019-12-31 17:49:25 +00001974 try {
Sahana Rao1e8271b2020-04-03 14:01:08 +01001975 final String newMimeType = MimeUtils.resolveMimeType(new File(newPath));
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001976 if (!updateDatabaseForFuseRename(helper, oldPath, newPath,
Martijn Coenen070bce12020-06-08 21:18:24 +02001977 getContentValuesForFuseRename(newPath, newMimeType, true /* checkHidden */))) {
Sahana Rao1e8271b2020-04-03 14:01:08 +01001978 if (!bypassRestrictions) {
1979 Log.e(TAG, "Calling package doesn't have write permission to rename file.");
1980 return OsConstants.EPERM;
1981 } else if (!maybeRemoveOwnerPackageForFuseRename(helper, newPath)) {
1982 Log.wtf(TAG, "Couldn't clear owner package name for " + newPath);
1983 return OsConstants.EPERM;
1984 }
Sahana Rao5b0b9652019-12-31 17:49:25 +00001985 }
1986
1987 // Try renaming oldPath to newPath in lower file system.
Sahana Raoddc85822020-01-06 20:22:46 +00001988 int errno = renameInLowerFs(oldPath, newPath);
1989 if (errno == 0) {
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001990 helper.setTransactionSuccessful();
Sahana Raoddc85822020-01-06 20:22:46 +00001991 } else {
1992 return errno;
Sahana Rao5b0b9652019-12-31 17:49:25 +00001993 }
Sahana Rao5b0b9652019-12-31 17:49:25 +00001994 } finally {
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07001995 helper.endTransaction();
Sahana Rao5b0b9652019-12-31 17:49:25 +00001996 }
Martijn Coenen070bce12020-06-08 21:18:24 +02001997 // The above code should have taken are of the mime/media type of the new file,
1998 // even if it was moved to/from a hidden directory.
1999 // This leaves cases where the source/dest of the move is a .nomedia file itself. Eg:
2000 // 1) /sdcard/foo/.nomedia => /sdcard/foo/bar.mp3
2001 // in this case, the code above has given bar.mp3 the correct mime type, but we should
2002 // still can /sdcard/foo, because it's now no longer hidden
2003 // 2) /sdcard/foo/.nomedia => /sdcard/bar/.nomedia
2004 // in this case, we need to scan both /sdcard/foo and /sdcard/bar/
2005 // 3) /sdcard/foo/bar.mp3 => /sdcard/foo/.nomedia
2006 // in this case, we need to scan all of /sdcard/foo
2007 if (extractDisplayName(oldPath).equals(".nomedia")) {
2008 scanFile(new File(oldPath).getParentFile(), REASON_DEMAND);
2009 }
2010 if (extractDisplayName(newPath).equals(".nomedia")) {
2011 scanFile(new File(newPath).getParentFile(), REASON_DEMAND);
2012 }
Sahana Rao5b0b9652019-12-31 17:49:25 +00002013 return 0;
2014 }
2015
2016 /**
Sahana Rao50326572020-02-25 13:07:38 +00002017 * Rename file/directory without imposing any restrictions.
2018 *
2019 * We don't impose any rename restrictions for apps that bypass scoped storage restrictions.
2020 * However, we update database entries for renamed files to keep the database consistent.
Sahana Rao92e27262020-02-19 14:11:56 +00002021 */
2022 private int renameUncheckedForFuse(String oldPath, String newPath) {
Sahana Rao74484532020-04-07 14:58:29 +01002023 if (new File(oldPath).isFile()) {
2024 return renameFileUncheckedForFuse(oldPath, newPath);
2025 } else {
2026 return renameDirectoryUncheckedForFuse(oldPath, newPath,
2027 getAllFilesForRenameDirectory(oldPath));
2028 }
Sahana Rao92e27262020-02-19 14:11:56 +00002029 }
2030
2031 /**
Sahana Rao2c416032019-12-31 13:41:00 +00002032 * Rename file or directory from {@code oldPath} to {@code newPath}.
2033 *
2034 * @param oldPath path of the file or directory to be renamed.
2035 * @param newPath new path of the file or directory to be renamed.
2036 * @param uid UID of the calling package.
shafike4fb1462020-01-29 16:25:23 +00002037 * @return 0 on successful rename, appropriate errno value if the rename is not allowed.
Sahana Rao2c416032019-12-31 13:41:00 +00002038 * <ul>
2039 * <li>{@link OsConstants#ENOENT} Renaming a non-existing file or renaming a file from path that
2040 * is not indexed by MediaProvider database.
Sahana Rao5b0b9652019-12-31 17:49:25 +00002041 * <li>{@link OsConstants#EPERM} Renaming a default directory or renaming a file to a file type
2042 * not supported by new path.
Sahana Rao2c416032019-12-31 13:41:00 +00002043 * This method can also return errno returned from {@code Os.rename} function.
Sahana Rao2c416032019-12-31 13:41:00 +00002044 *
2045 * Called from JNI in jni/MediaProviderWrapper.cpp
2046 */
2047 @Keep
2048 public int renameForFuse(String oldPath, String newPath, int uid) {
2049 final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. ";
shafikd84da092020-04-29 17:53:30 +01002050 final LocalCallingIdentity token =
2051 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
Zim696dea42020-03-07 11:41:42 +00002052
Sahana Rao2c416032019-12-31 13:41:00 +00002053 try {
Martijn Coenen9a1f6792020-03-17 07:13:47 +01002054 if (isPrivatePackagePathNotOwnedByCaller(oldPath)
2055 || isPrivatePackagePathNotOwnedByCaller(newPath)) {
2056 return OsConstants.EACCES;
Sahana Raoddc85822020-01-06 20:22:46 +00002057 }
2058
Sahana Rao6b7baf42020-04-17 20:42:23 +01002059 if (!newPath.equals(getAbsoluteSanitizedPath(newPath))) {
2060 Log.e(TAG, "New path name contains invalid characters.");
2061 return OsConstants.EPERM;
2062 }
2063
shafik63abf8b2020-03-02 15:44:37 +00002064 if (shouldBypassFuseRestrictions(/*forWrite*/ true, oldPath)
2065 && shouldBypassFuseRestrictions(/*forWrite*/ true, newPath)) {
2066 return renameUncheckedForFuse(oldPath, newPath);
2067 }
2068 // Legacy apps that made is this far don't have the right storage permission and hence
2069 // are not allowed to access anything other than their external app directory
2070 if (isCallingPackageRequestingLegacy()) {
2071 return OsConstants.EACCES;
2072 }
2073
Sahana Rao2c416032019-12-31 13:41:00 +00002074 final String[] oldRelativePath = sanitizePath(extractRelativePath(oldPath));
2075 final String[] newRelativePath = sanitizePath(extractRelativePath(newPath));
2076 if (oldRelativePath.length == 0 || newRelativePath.length == 0) {
2077 // Rename not allowed on paths that can't be translated to RELATIVE_PATH.
2078 Log.e(TAG, errorMessage + "Invalid path.");
shafike4fb1462020-01-29 16:25:23 +00002079 return OsConstants.EPERM;
Sahana Rao2c416032019-12-31 13:41:00 +00002080 } else if (oldRelativePath.length == 1 && TextUtils.isEmpty(oldRelativePath[0])) {
2081 // Allow rename of files/folders other than default directories.
2082 final String displayName = extractDisplayName(oldPath);
2083 for (String defaultFolder : sDefaultFolderNames) {
2084 if (displayName.equals(defaultFolder)) {
2085 Log.e(TAG, errorMessage + oldPath + " is a default folder."
2086 + " Renaming a default folder is not allowed.");
shafike4fb1462020-01-29 16:25:23 +00002087 return OsConstants.EPERM;
Sahana Rao2c416032019-12-31 13:41:00 +00002088 }
2089 }
2090 } else if (newRelativePath.length == 1 && TextUtils.isEmpty(newRelativePath[0])) {
2091 Log.e(TAG, errorMessage + newPath + " is in root folder."
2092 + " Renaming a file/directory to root folder is not allowed");
shafike4fb1462020-01-29 16:25:23 +00002093 return OsConstants.EPERM;
Sahana Rao2c416032019-12-31 13:41:00 +00002094 }
2095
2096 final File directoryAndroid = new File(Environment.getExternalStorageDirectory(),
2097 DIRECTORY_ANDROID);
2098 final File directoryAndroidMedia = new File(directoryAndroid, DIRECTORY_MEDIA);
2099 if (directoryAndroidMedia.getAbsolutePath().equals(oldPath)) {
2100 // Don't allow renaming 'Android/media' directory.
2101 // Android/[data|obb] are bind mounted and these paths don't go through FUSE.
2102 Log.e(TAG, errorMessage + oldPath + " is a default folder in app external "
2103 + "directory. Renaming a default folder is not allowed.");
shafike4fb1462020-01-29 16:25:23 +00002104 return OsConstants.EPERM;
Sahana Rao2c416032019-12-31 13:41:00 +00002105 } else if (FileUtils.contains(directoryAndroid, new File(newPath))) {
2106 if (newRelativePath.length == 1) {
2107 // New path is Android/*. Path is directly under Android. Don't allow moving
2108 // files and directories to Android/.
2109 Log.e(TAG, errorMessage + newPath + " is in app external directory. "
2110 + "Renaming a file/directory to app external directory is not "
2111 + "allowed.");
shafike4fb1462020-01-29 16:25:23 +00002112 return OsConstants.EPERM;
Sahana Rao2c416032019-12-31 13:41:00 +00002113 } else if(!FileUtils.contains(directoryAndroidMedia, new File(newPath))) {
2114 // New path is Android/*/*. Don't allow moving of files or directories
2115 // to app external directory other than media directory.
2116 Log.e(TAG, errorMessage + newPath + " is not in external media directory."
2117 + "File/directory can only be renamed to a path in external media "
2118 + "directory. Renaming file/directory to path in other external "
2119 + "directories is not allowed");
shafike4fb1462020-01-29 16:25:23 +00002120 return OsConstants.EPERM;
Sahana Rao2c416032019-12-31 13:41:00 +00002121 }
2122 }
2123
2124 // Continue renaming files/directories if rename of oldPath to newPath is allowed.
Sahana Rao5b0b9652019-12-31 17:49:25 +00002125 if (new File(oldPath).isFile()) {
Sahana Rao50326572020-02-25 13:07:38 +00002126 return renameFileCheckedForFuse(oldPath, newPath);
Sahana Rao5b0b9652019-12-31 17:49:25 +00002127 } else {
Sahana Rao50326572020-02-25 13:07:38 +00002128 return renameDirectoryCheckedForFuse(oldPath, newPath);
Sahana Rao2c416032019-12-31 13:41:00 +00002129 }
2130 } finally {
2131 restoreLocalCallingIdentity(token);
2132 }
2133 }
2134
The Android Open Source Project70215272009-03-03 19:32:43 -08002135 @Override
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -07002136 public int checkUriPermission(@NonNull Uri uri, int uid,
2137 /* @Intent.AccessUriMode */ int modeFlags) {
shafikd84da092020-04-29 17:53:30 +01002138 final LocalCallingIdentity token = clearLocalCallingIdentity(
2139 LocalCallingIdentity.fromExternal(getContext(), uid));
Zim696dea42020-03-07 11:41:42 +00002140
Jeff Sharkey0ee97412019-05-20 14:00:12 -06002141 try {
2142 final boolean allowHidden = isCallingPackageAllowedHidden();
2143 final int table = matchUri(uri, allowHidden);
2144
2145 final DatabaseHelper helper;
Jeff Sharkey0ee97412019-05-20 14:00:12 -06002146 try {
2147 helper = getDatabaseForUri(uri);
Jeff Sharkey0ee97412019-05-20 14:00:12 -06002148 } catch (VolumeNotFoundException e) {
2149 return PackageManager.PERMISSION_DENIED;
2150 }
2151
2152 final int type;
2153 if ((modeFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
2154 type = TYPE_UPDATE;
2155 } else {
2156 type = TYPE_QUERY;
2157 }
2158
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002159 final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, null);
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07002160 try (Cursor c = qb.query(helper,
2161 new String[] { BaseColumns._ID }, null, null, null, null, null, null, null)) {
Jeff Sharkey0ee97412019-05-20 14:00:12 -06002162 if (c.getCount() == 1) {
2163 return PackageManager.PERMISSION_GRANTED;
2164 }
2165 }
2166 } finally {
2167 restoreLocalCallingIdentity(token);
2168 }
2169 return PackageManager.PERMISSION_DENIED;
2170 }
2171
2172 @Override
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002173 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
2174 String sortOrder) {
2175 return query(uri, projection,
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -07002176 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, sortOrder), null);
Jeff Sharkeyca524272018-07-30 13:33:48 -06002177 }
2178
2179 @Override
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002180 public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal) {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06002181 Trace.beginSection("query");
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06002182 try {
2183 return queryInternal(uri, projection, queryArgs, signal);
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06002184 } catch (FallbackException e) {
2185 return e.translateForQuery(getCallingPackageTargetSdkVersion());
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06002186 } finally {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06002187 Trace.endSection();
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06002188 }
2189 }
2190
2191 private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs,
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06002192 CancellationSignal signal) throws FallbackException {
Jeff Sharkeyd6697822020-03-22 20:59:47 -06002193 queryArgs = (queryArgs != null) ? queryArgs : new Bundle();
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002194
Nikita Ioffe710787b2020-06-11 14:35:14 +01002195 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
2196 queryArgs.remove(INCLUDED_DEFAULT_DIRECTORIES);
2197
Jeff Sharkey9c1bb3d2019-11-14 13:59:41 -07002198 final ArraySet<String> honoredArgs = new ArraySet<>();
2199 DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator);
2200
Marco Nelissen01e706a2013-09-12 15:38:42 -07002201 uri = safeUncanonicalize(uri);
2202
Jeff Sharkey27bf6962018-10-20 15:12:28 -06002203 final String volumeName = getVolumeName(uri);
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07002204 final int targetSdkVersion = getCallingPackageTargetSdkVersion();
Jeff Sharkey94461582018-07-12 14:34:47 -06002205 final boolean allowHidden = isCallingPackageAllowedHidden();
2206 final int table = matchUri(uri, allowHidden);
The Android Open Source Project70215272009-03-03 19:32:43 -08002207
Marco Nelissen8a1db2e2017-11-30 12:46:54 -08002208 //Log.v(TAG, "query: uri="+uri+", selection="+selection);
The Android Open Source Project70215272009-03-03 19:32:43 -08002209 // handle MEDIA_SCANNER before calling getDatabaseForUri()
2210 if (table == MEDIA_SCANNER) {
Jeff Sharkey077b71e2019-01-22 13:19:51 -07002211 // create a cursor to return volume currently being scanned by the media scanner
2212 MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME});
2213 c.addRow(new String[] {mMediaScannerVolume});
2214 return c;
The Android Open Source Project70215272009-03-03 19:32:43 -08002215 }
2216
Marco Nelissen00270192010-01-08 08:35:20 -08002217 // Used temporarily (until we have unique media IDs) to get an identifier
2218 // for the current sd card, so that the music app doesn't have to use the
2219 // non-public getFatVolumeId method
2220 if (table == FS_ID) {
2221 MatrixCursor c = new MatrixCursor(new String[] {"fsid"});
2222 c.addRow(new Integer[] {mVolumeId});
2223 return c;
2224 }
2225
Marco Nelissen704a8b52011-02-03 10:43:06 -08002226 if (table == VERSION) {
2227 MatrixCursor c = new MatrixCursor(new String[] {"version"});
Jeff Sharkeyc9ae8592019-10-07 11:41:04 -06002228 c.addRow(new Integer[] {DatabaseHelper.getDatabaseVersion(getContext())});
Marco Nelissen704a8b52011-02-03 10:43:06 -08002229 return c;
2230 }
2231
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06002232 final DatabaseHelper helper = getDatabaseForUri(uri);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002233 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, queryArgs,
2234 honoredArgs::add);
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06002235
Jeff Sharkey9c1bb3d2019-11-14 13:59:41 -07002236 if (targetSdkVersion < Build.VERSION_CODES.R) {
Jeff Sharkeyb8557722019-12-18 17:57:50 -07002237 // Some apps are abusing "ORDER BY" clauses to inject "LIMIT"
2238 // clauses; gracefully lift them out.
2239 DatabaseUtils.recoverAbusiveSortOrder(queryArgs);
2240
Jeff Sharkey9c1bb3d2019-11-14 13:59:41 -07002241 // Some apps are abusing the Uri query parameters to inject LIMIT
2242 // clauses; gracefully lift them out.
2243 DatabaseUtils.recoverAbusiveLimit(uri, queryArgs);
2244 }
2245
2246 if (targetSdkVersion < Build.VERSION_CODES.Q) {
Jeff Sharkey1ed77272018-10-03 13:47:33 -06002247 // Some apps are abusing the "WHERE" clause by injecting "GROUP BY"
2248 // clauses; gracefully lift them out.
Jeff Sharkey9c1bb3d2019-11-14 13:59:41 -07002249 DatabaseUtils.recoverAbusiveSelection(queryArgs);
Jeff Sharkey1ed77272018-10-03 13:47:33 -06002250
2251 // Some apps are abusing the first column to inject "DISTINCT";
2252 // gracefully lift them out.
Jeff Sharkeyf05c4e72019-08-20 10:35:50 -06002253 if ((projection != null) && (projection.length > 0)
2254 && projection[0].startsWith("DISTINCT ")) {
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002255 projection[0] = projection[0].substring("DISTINCT ".length());
Jeff Sharkey1ed77272018-10-03 13:47:33 -06002256 qb.setDistinct(true);
2257 }
Jeff Sharkeyd2568872019-02-09 13:49:05 -07002258
2259 // Some apps are generating thumbnails with getThumbnail(), but then
2260 // ignoring the returned Bitmap and querying the raw table; give
2261 // them a row with enough information to find the original image.
Jeff Sharkey9c1bb3d2019-11-14 13:59:41 -07002262 final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION);
Jeff Sharkeyd2568872019-02-09 13:49:05 -07002263 if ((table == IMAGES_THUMBNAILS || table == VIDEO_THUMBNAILS)
2264 && !TextUtils.isEmpty(selection)) {
2265 final Matcher matcher = PATTERN_SELECTION_ID.matcher(selection);
2266 if (matcher.matches()) {
2267 final long id = Long.parseLong(matcher.group(1));
2268
2269 final Uri fullUri;
2270 if (table == IMAGES_THUMBNAILS) {
2271 fullUri = ContentUris.withAppendedId(
2272 Images.Media.getContentUri(volumeName), id);
2273 } else if (table == VIDEO_THUMBNAILS) {
2274 fullUri = ContentUris.withAppendedId(
2275 Video.Media.getContentUri(volumeName), id);
2276 } else {
2277 throw new IllegalArgumentException();
2278 }
2279
2280 final MatrixCursor cursor = new MatrixCursor(projection);
Jeff Sharkeye2750322020-01-07 22:06:24 -07002281 final File file = ContentResolver.encodeToFile(
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -07002282 fullUri.buildUpon().appendPath("thumbnail").build());
Jeff Sharkeye2750322020-01-07 22:06:24 -07002283 final String data = file.getAbsolutePath();
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -07002284 cursor.newRow().add(MediaColumns._ID, null)
2285 .add(Images.Thumbnails.IMAGE_ID, id)
2286 .add(Video.Thumbnails.VIDEO_ID, id)
2287 .add(MediaColumns.DATA, data);
Jeff Sharkeyd2568872019-02-09 13:49:05 -07002288 return cursor;
2289 }
2290 }
Jeff Sharkey3a1265b2018-08-06 11:36:08 -06002291 }
2292
Jeff Sharkeyd6697822020-03-22 20:59:47 -06002293 final Cursor c = qb.query(helper, projection, queryArgs, signal);
The Android Open Source Project70215272009-03-03 19:32:43 -08002294 if (c != null) {
Jeff Sharkeye2750322020-01-07 22:06:24 -07002295 // As a performance optimization, only configure notifications when
2296 // resulting cursor will leave our process
Jeff Sharkey75b8bd42020-06-19 08:51:14 -06002297 final boolean callerIsRemote = mCallingIdentity.get().pid != android.os.Process.myPid();
2298 if (callerIsRemote && !isFuseThread()) {
Jeff Sharkeye2750322020-01-07 22:06:24 -07002299 c.setNotificationUri(getContext().getContentResolver(), uri);
2300 }
Jeff Sharkey9c1bb3d2019-11-14 13:59:41 -07002301
2302 final Bundle extras = new Bundle();
2303 extras.putStringArray(ContentResolver.EXTRA_HONORED_ARGS,
2304 honoredArgs.toArray(new String[honoredArgs.size()]));
2305 c.setExtras(extras);
The Android Open Source Project70215272009-03-03 19:32:43 -08002306 }
2307 return c;
2308 }
2309
The Android Open Source Project70215272009-03-03 19:32:43 -08002310 @Override
Jeff Sharkey94461582018-07-12 14:34:47 -06002311 public String getType(Uri url) {
Jeff Sharkey7285f262019-05-21 08:36:27 -06002312 final int match = matchUri(url, true);
Jeff Sharkey94461582018-07-12 14:34:47 -06002313 switch (match) {
The Android Open Source Project70215272009-03-03 19:32:43 -08002314 case IMAGES_MEDIA_ID:
2315 case AUDIO_MEDIA_ID:
Jeff Sharkeyd6697822020-03-22 20:59:47 -06002316 case AUDIO_PLAYLISTS_ID:
The Android Open Source Project70215272009-03-03 19:32:43 -08002317 case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
2318 case VIDEO_MEDIA_ID:
Sudheer Shanka56cba322018-12-07 10:55:58 -08002319 case DOWNLOADS_ID:
Mike Lockwoodc198bd92010-09-10 14:55:20 -04002320 case FILES_ID:
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06002321 final LocalCallingIdentity token = clearLocalCallingIdentity();
Jeff Sharkeyb7d359b2019-03-18 20:07:12 -06002322 try (Cursor cursor = queryForSingleItem(url,
2323 new String[] { MediaColumns.MIME_TYPE }, null, null, null)) {
2324 return cursor.getString(0);
2325 } catch (FileNotFoundException e) {
2326 throw new IllegalArgumentException(e.getMessage());
Ray Chen26f297a2010-04-06 14:52:01 -07002327 } finally {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06002328 restoreLocalCallingIdentity(token);
The Android Open Source Project70215272009-03-03 19:32:43 -08002329 }
The Android Open Source Project70215272009-03-03 19:32:43 -08002330
2331 case IMAGES_MEDIA:
2332 case IMAGES_THUMBNAILS:
2333 return Images.Media.CONTENT_TYPE;
Jeff Sharkey29421112018-07-27 20:56:44 -06002334
Marco Nelissen804f5fe2010-08-13 16:15:54 -07002335 case AUDIO_ALBUMART_ID:
Jeff Sharkey29421112018-07-27 20:56:44 -06002336 case AUDIO_ALBUMART_FILE_ID:
The Android Open Source Project70215272009-03-03 19:32:43 -08002337 case IMAGES_THUMBNAILS_ID:
Jeff Sharkey29421112018-07-27 20:56:44 -06002338 case VIDEO_THUMBNAILS_ID:
The Android Open Source Project70215272009-03-03 19:32:43 -08002339 return "image/jpeg";
2340
2341 case AUDIO_MEDIA:
2342 case AUDIO_GENRES_ID_MEMBERS:
2343 case AUDIO_PLAYLISTS_ID_MEMBERS:
2344 return Audio.Media.CONTENT_TYPE;
2345
2346 case AUDIO_GENRES:
2347 case AUDIO_MEDIA_ID_GENRES:
2348 return Audio.Genres.CONTENT_TYPE;
2349 case AUDIO_GENRES_ID:
2350 case AUDIO_MEDIA_ID_GENRES_ID:
2351 return Audio.Genres.ENTRY_CONTENT_TYPE;
2352 case AUDIO_PLAYLISTS:
The Android Open Source Project70215272009-03-03 19:32:43 -08002353 return Audio.Playlists.CONTENT_TYPE;
The Android Open Source Project70215272009-03-03 19:32:43 -08002354
2355 case VIDEO_MEDIA:
2356 return Video.Media.CONTENT_TYPE;
Sudheer Shanka56cba322018-12-07 10:55:58 -08002357 case DOWNLOADS:
2358 return Downloads.CONTENT_TYPE;
The Android Open Source Project70215272009-03-03 19:32:43 -08002359 }
Marco Nelissen804f5fe2010-08-13 16:15:54 -07002360 throw new IllegalStateException("Unknown URL : " + url);
The Android Open Source Project70215272009-03-03 19:32:43 -08002361 }
2362
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002363 @VisibleForTesting
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002364 void ensureFileColumns(@NonNull Uri uri, @NonNull ContentValues values)
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06002365 throws VolumeArgumentException, VolumeNotFoundException {
Jeff Sharkey74f73732019-11-12 15:36:32 -07002366 final LocalUriMatcher matcher = new LocalUriMatcher(MediaStore.AUTHORITY);
2367 final int match = matcher.matchUri(uri, true);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002368 ensureNonUniqueFileColumns(match, uri, Bundle.EMPTY, values, null /* currentPath */);
Jeff Sharkey0e880712019-02-11 11:01:31 -07002369 }
2370
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002371 private void ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
Jeff Sharkeyab27f022020-04-29 20:58:55 -06002372 @NonNull ContentValues values, @Nullable String currentPath)
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06002373 throws VolumeArgumentException, VolumeNotFoundException {
Jeff Sharkeyab27f022020-04-29 20:58:55 -06002374 ensureFileColumns(match, uri, extras, values, true, currentPath);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002375 }
2376
2377 private void ensureNonUniqueFileColumns(int match, @NonNull Uri uri,
2378 @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath)
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06002379 throws VolumeArgumentException, VolumeNotFoundException {
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002380 ensureFileColumns(match, uri, extras, values, false, currentPath);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002381 }
2382
The Android Open Source Project70215272009-03-03 19:32:43 -08002383 /**
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002384 * Get the various file-related {@link MediaColumns} in the given
2385 * {@link ContentValues} into sane condition. Also validates that defined
2386 * columns are valid for the given {@link Uri}, such as ensuring that only
2387 * {@code image/*} can be inserted into
2388 * {@link android.provider.MediaStore.Images}.
The Android Open Source Project70215272009-03-03 19:32:43 -08002389 */
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002390 private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras,
2391 @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath)
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06002392 throws VolumeArgumentException, VolumeNotFoundException {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06002393 Trace.beginSection("ensureFileColumns");
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06002394
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002395 Objects.requireNonNull(uri);
2396 Objects.requireNonNull(extras);
2397 Objects.requireNonNull(values);
2398
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002399 // Figure out defaults based on Uri being modified
Jeff Sharkey7ea24f22019-08-22 10:14:18 -06002400 String defaultMimeType = ClipDescription.MIMETYPE_UNKNOWN;
Jeff Sharkeyca4a71e2020-01-10 17:18:37 -07002401 int defaultMediaType = FileColumns.MEDIA_TYPE_NONE;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002402 String defaultPrimary = Environment.DIRECTORY_DOWNLOADS;
2403 String defaultSecondary = null;
2404 List<String> allowedPrimary = Arrays.asList(
2405 Environment.DIRECTORY_DOWNLOADS,
2406 Environment.DIRECTORY_DOCUMENTS);
2407 switch (match) {
2408 case AUDIO_MEDIA:
Jeff Sharkey0e880712019-02-11 11:01:31 -07002409 case AUDIO_MEDIA_ID:
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002410 defaultMimeType = "audio/mpeg";
Jeff Sharkeyca4a71e2020-01-10 17:18:37 -07002411 defaultMediaType = FileColumns.MEDIA_TYPE_AUDIO;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002412 defaultPrimary = Environment.DIRECTORY_MUSIC;
2413 allowedPrimary = Arrays.asList(
2414 Environment.DIRECTORY_ALARMS,
shafik5870b202020-03-03 20:21:16 +00002415 Environment.DIRECTORY_AUDIOBOOKS,
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002416 Environment.DIRECTORY_MUSIC,
2417 Environment.DIRECTORY_NOTIFICATIONS,
2418 Environment.DIRECTORY_PODCASTS,
2419 Environment.DIRECTORY_RINGTONES);
2420 break;
2421 case VIDEO_MEDIA:
Jeff Sharkey0e880712019-02-11 11:01:31 -07002422 case VIDEO_MEDIA_ID:
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002423 defaultMimeType = "video/mp4";
Jeff Sharkeyca4a71e2020-01-10 17:18:37 -07002424 defaultMediaType = FileColumns.MEDIA_TYPE_VIDEO;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002425 defaultPrimary = Environment.DIRECTORY_MOVIES;
2426 allowedPrimary = Arrays.asList(
2427 Environment.DIRECTORY_DCIM,
shafikb95e0242020-02-12 17:41:32 +00002428 Environment.DIRECTORY_MOVIES,
2429 Environment.DIRECTORY_PICTURES);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002430 break;
2431 case IMAGES_MEDIA:
Jeff Sharkey0e880712019-02-11 11:01:31 -07002432 case IMAGES_MEDIA_ID:
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002433 defaultMimeType = "image/jpeg";
Jeff Sharkeyca4a71e2020-01-10 17:18:37 -07002434 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002435 defaultPrimary = Environment.DIRECTORY_PICTURES;
2436 allowedPrimary = Arrays.asList(
2437 Environment.DIRECTORY_DCIM,
2438 Environment.DIRECTORY_PICTURES);
2439 break;
2440 case AUDIO_ALBUMART:
Jeff Sharkey0e880712019-02-11 11:01:31 -07002441 case AUDIO_ALBUMART_ID:
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002442 defaultMimeType = "image/jpeg";
Jeff Sharkeyca4a71e2020-01-10 17:18:37 -07002443 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002444 defaultPrimary = Environment.DIRECTORY_MUSIC;
2445 allowedPrimary = Arrays.asList(defaultPrimary);
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07002446 defaultSecondary = DIRECTORY_THUMBNAILS;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002447 break;
2448 case VIDEO_THUMBNAILS:
Jeff Sharkey0e880712019-02-11 11:01:31 -07002449 case VIDEO_THUMBNAILS_ID:
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002450 defaultMimeType = "image/jpeg";
Jeff Sharkeyca4a71e2020-01-10 17:18:37 -07002451 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002452 defaultPrimary = Environment.DIRECTORY_MOVIES;
2453 allowedPrimary = Arrays.asList(defaultPrimary);
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07002454 defaultSecondary = DIRECTORY_THUMBNAILS;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002455 break;
2456 case IMAGES_THUMBNAILS:
Jeff Sharkey0e880712019-02-11 11:01:31 -07002457 case IMAGES_THUMBNAILS_ID:
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002458 defaultMimeType = "image/jpeg";
Jeff Sharkeyca4a71e2020-01-10 17:18:37 -07002459 defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002460 defaultPrimary = Environment.DIRECTORY_PICTURES;
2461 allowedPrimary = Arrays.asList(defaultPrimary);
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07002462 defaultSecondary = DIRECTORY_THUMBNAILS;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002463 break;
2464 case AUDIO_PLAYLISTS:
Jeff Sharkey0e880712019-02-11 11:01:31 -07002465 case AUDIO_PLAYLISTS_ID:
Jeff Sharkey21e297e2019-12-06 18:14:32 -07002466 defaultMimeType = "audio/mpegurl";
Jeff Sharkeyca4a71e2020-01-10 17:18:37 -07002467 defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002468 defaultPrimary = Environment.DIRECTORY_MUSIC;
Jeff Sharkeyca4a71e2020-01-10 17:18:37 -07002469 allowedPrimary = Arrays.asList(
2470 Environment.DIRECTORY_MUSIC,
2471 Environment.DIRECTORY_MOVIES);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002472 break;
Sudheer Shankaaa626512018-11-15 20:29:28 -08002473 case DOWNLOADS:
Jeff Sharkey0e880712019-02-11 11:01:31 -07002474 case DOWNLOADS_ID:
Sudheer Shankaaa626512018-11-15 20:29:28 -08002475 defaultPrimary = Environment.DIRECTORY_DOWNLOADS;
2476 allowedPrimary = Arrays.asList(defaultPrimary);
2477 break;
Jeff Sharkey0e880712019-02-11 11:01:31 -07002478 case FILES:
2479 case FILES_ID:
2480 // Use defaults above
2481 break;
2482 default:
2483 Log.w(TAG, "Unhandled location " + uri + "; assuming generic files");
2484 break;
The Android Open Source Project70215272009-03-03 19:32:43 -08002485 }
2486
Jeff Sharkey71437302019-04-09 23:46:52 -06002487 final String resolvedVolumeName = resolveVolumeName(uri);
2488
Jeff Sharkeybac84e22018-12-20 15:11:17 -07002489 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))
Jeff Sharkey71437302019-04-09 23:46:52 -06002490 && MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)) {
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002491 // TODO: promote this to top-level check
2492 throw new UnsupportedOperationException(
2493 "Writing to internal storage is not supported.");
2494 }
2495
2496 // Force values when raw path provided
Jeff Sharkeybac84e22018-12-20 15:11:17 -07002497 if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
Sahana Raoea587fc2020-06-03 15:56:23 +01002498 FileUtils.computeValuesFromData(values, isFuseThread());
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002499 }
Bram Bonné2c46f522020-02-27 13:35:06 +01002500
Sahana Raob105c222020-06-17 20:18:48 +01002501 final boolean isTargetSdkROrHigher =
2502 getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R;
2503 final String displayName = values.getAsString(MediaColumns.DISPLAY_NAME);
2504 final String mimeTypeFromExt = TextUtils.isEmpty(displayName) ? null :
2505 MimeUtils.resolveMimeType(new File(displayName));
2506
2507 if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) {
2508 if (isTargetSdkROrHigher) {
2509 // Extract the MIME type from the display name if we couldn't resolve it from the
2510 // raw path
2511 if (mimeTypeFromExt != null) {
2512 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
2513 } else {
2514 // We couldn't resolve mimeType, it means that both display name and MIME type
2515 // were missing in values, so we use defaultMimeType.
2516 values.put(MediaColumns.MIME_TYPE, defaultMimeType);
2517 }
2518 } else if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
2519 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
2520 } else {
2521 // We don't use mimeTypeFromExt to preserve legacy behavior.
2522 values.put(MediaColumns.MIME_TYPE, defaultMimeType);
2523 }
2524 }
2525
2526 String mimeType = values.getAsString(MediaColumns.MIME_TYPE);
2527 if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
2528 // We allow any mimeType for generic uri with default media type as MEDIA_TYPE_NONE.
2529 } else if (mimeType != null &&
2530 MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) == null) {
2531 if (mimeTypeFromExt != null &&
2532 defaultMediaType == MimeUtils.resolveMediaType(mimeTypeFromExt)) {
2533 // If mimeType from extension matches the defaultMediaType of uri, we use mimeType
2534 // from file extension as mimeType. This is an effort to guess the mimeType when we
2535 // get unsupported mimeType.
2536 // Note: We can't force defaultMimeType because when we force defaultMimeType, we
2537 // will force the file extension as well. For example, if DISPLAY_NAME=Foo.png and
2538 // mimeType="image/*". If we force mimeType to be "image/jpeg", we append the file
2539 // name with the new file extension i.e., "Foo.png.jpg" where as the expected file
2540 // name was "Foo.png"
2541 values.put(MediaColumns.MIME_TYPE, mimeTypeFromExt);
2542 } else if (isTargetSdkROrHigher) {
2543 // We are here because given mimeType is unsupported also we couldn't guess valid
2544 // mimeType from file extension.
2545 throw new IllegalArgumentException("Unsupported MIME type " + mimeType);
2546 } else {
2547 // We can't throw error for legacy apps, so we try to use defaultMimeType.
2548 values.put(MediaColumns.MIME_TYPE, defaultMimeType);
Bram Bonné2c46f522020-02-27 13:35:06 +01002549 }
2550 }
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002551
2552 // Give ourselves sane defaults when missing
Jeff Sharkeybac84e22018-12-20 15:11:17 -07002553 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) {
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002554 values.put(MediaColumns.DISPLAY_NAME,
2555 String.valueOf(System.currentTimeMillis()));
2556 }
Sudheer Shanka2b79a5c2019-04-12 10:04:20 -07002557 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT);
2558 final int format = formatObject == null ? 0 : formatObject.intValue();
2559 if (format == MtpConstants.FORMAT_ASSOCIATION) {
2560 values.putNull(MediaColumns.MIME_TYPE);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002561 }
2562
Sahana Raob105c222020-06-17 20:18:48 +01002563 mimeType = values.getAsString(MediaColumns.MIME_TYPE);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002564 // Sanity check MIME type against table
Jeff Sharkeyca4a71e2020-01-10 17:18:37 -07002565 if (mimeType != null) {
2566 final int actualMediaType = MimeUtils.resolveMediaType(mimeType);
2567 if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
2568 // Give callers an opportunity to work with playlists and
2569 // subtitles using the generic files table
2570 switch (actualMediaType) {
2571 case FileColumns.MEDIA_TYPE_PLAYLIST:
2572 defaultMimeType = "audio/mpegurl";
2573 defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST;
2574 defaultPrimary = Environment.DIRECTORY_MUSIC;
2575 allowedPrimary = Arrays.asList(
2576 Environment.DIRECTORY_MUSIC,
2577 Environment.DIRECTORY_MOVIES);
2578 break;
2579 case FileColumns.MEDIA_TYPE_SUBTITLE:
2580 defaultMimeType = "application/x-subrip";
2581 defaultMediaType = FileColumns.MEDIA_TYPE_SUBTITLE;
2582 defaultPrimary = Environment.DIRECTORY_MOVIES;
2583 allowedPrimary = Arrays.asList(
2584 Environment.DIRECTORY_MUSIC,
2585 Environment.DIRECTORY_MOVIES);
2586 break;
2587 }
2588 } else if (defaultMediaType != actualMediaType) {
2589 final String[] split = defaultMimeType.split("/");
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002590 throw new IllegalArgumentException(
2591 "MIME type " + mimeType + " cannot be inserted into " + uri
2592 + "; expected MIME type under " + split[0] + "/*");
2593 }
2594 }
2595
Jeff Sharkey89149b62020-03-29 22:03:44 -06002596 // Use default directories when missing
2597 if (TextUtils.isEmpty(values.getAsString(MediaColumns.RELATIVE_PATH))) {
2598 if (defaultSecondary != null) {
2599 values.put(MediaColumns.RELATIVE_PATH,
2600 defaultPrimary + '/' + defaultSecondary + '/');
2601 } else {
2602 values.put(MediaColumns.RELATIVE_PATH,
2603 defaultPrimary + '/');
2604 }
2605 }
2606
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002607 // Generate path when undefined
Jeff Sharkeybac84e22018-12-20 15:11:17 -07002608 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) {
Jeff Sharkey89149b62020-03-29 22:03:44 -06002609 File volumePath;
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002610 try {
Jeff Sharkey89149b62020-03-29 22:03:44 -06002611 volumePath = getVolumePath(resolvedVolumeName);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002612 } catch (FileNotFoundException e) {
2613 throw new IllegalArgumentException(e);
2614 }
Jeff Sharkey89149b62020-03-29 22:03:44 -06002615
Sahana Rao76b8b5b2020-04-17 20:21:59 +01002616 FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ !isFuseThread());
Sahana Raoea587fc2020-06-03 15:56:23 +01002617 FileUtils.computeDataFromValues(values, volumePath, isFuseThread());
Jeff Sharkey89149b62020-03-29 22:03:44 -06002618
2619 // Create result file
2620 File res = new File(values.getAsString(MediaColumns.DATA));
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002621 try {
Jeff Sharkey0e880712019-02-11 11:01:31 -07002622 if (makeUnique) {
Jeff Sharkey89149b62020-03-29 22:03:44 -06002623 res = FileUtils.buildUniqueFile(res.getParentFile(),
2624 mimeType, res.getName());
Jeff Sharkey0e880712019-02-11 11:01:31 -07002625 } else {
Jeff Sharkey89149b62020-03-29 22:03:44 -06002626 res = FileUtils.buildNonUniqueFile(res.getParentFile(),
2627 mimeType, res.getName());
Jeff Sharkey0e880712019-02-11 11:01:31 -07002628 }
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002629 } catch (FileNotFoundException e) {
2630 throw new IllegalStateException(
Jeff Sharkey89149b62020-03-29 22:03:44 -06002631 "Failed to build unique file: " + res + " " + values);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002632 }
Nikita Ioffeb5650d12019-05-22 13:24:41 +01002633
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002634 // Require that content lives under well-defined directories to help
2635 // keep the user's content organized
Nikita Ioffeb5650d12019-05-22 13:24:41 +01002636
Jeff Sharkeyab27f022020-04-29 20:58:55 -06002637 // Start by saying unchanged directories are valid
2638 final String currentDir = (currentPath != null)
2639 ? new File(currentPath).getParent() : null;
2640 boolean validPath = res.getParent().equals(currentDir);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002641
2642 // Next, consider allowing based on allowed primary directory
Jeff Sharkey89149b62020-03-29 22:03:44 -06002643 final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/");
2644 final String primary = (relativePath.length > 0) ? relativePath[0] : null;
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002645 if (!validPath) {
2646 validPath = allowedPrimary.contains(primary);
2647 }
2648
2649 // Next, consider allowing paths when referencing a related item
2650 final Uri relatedUri = extras.getParcelable(QUERY_ARG_RELATED_URI);
2651 if (!validPath && relatedUri != null) {
2652 try (Cursor c = queryForSingleItem(relatedUri, new String[] {
2653 MediaColumns.MIME_TYPE,
2654 MediaColumns.RELATIVE_PATH,
2655 }, null, null, null)) {
2656 // If top-level MIME type matches, and relative path
2657 // matches, then allow caller to place things here
2658
2659 final String expectedType = MimeUtils.extractPrimaryType(
2660 c.getString(0));
2661 final String actualType = MimeUtils.extractPrimaryType(
2662 values.getAsString(MediaColumns.MIME_TYPE));
2663 if (!Objects.equals(expectedType, actualType)) {
2664 throw new IllegalArgumentException("Placement of " + actualType
2665 + " item not allowed in relation to " + expectedType + " item");
2666 }
2667
2668 final String expectedPath = c.getString(1);
2669 final String actualPath = values.getAsString(MediaColumns.RELATIVE_PATH);
2670 if (!Objects.equals(expectedPath, actualPath)) {
2671 throw new IllegalArgumentException("Placement of " + actualPath
2672 + " item not allowed in relation to " + expectedPath + " item");
2673 }
2674
2675 // If we didn't see any trouble above, then we'll allow it
2676 validPath = true;
2677 } catch (FileNotFoundException e) {
2678 Log.w(TAG, "Failed to find related item " + relatedUri + ": " + e);
2679 }
2680 }
2681
Sahana Rao80ecfba2020-04-03 10:48:01 +01002682 // Consider allowing external media directory of calling package
2683 if (!validPath) {
2684 final String pathOwnerPackage = extractPathOwnerPackageName(res.getAbsolutePath());
2685 if (pathOwnerPackage != null) {
2686 validPath = isExternalMediaDirectory(res.getAbsolutePath()) &&
2687 isCallingIdentitySharedPackageName(pathOwnerPackage);
2688 }
2689 }
2690
Sahana Rao406cf6d2020-04-08 21:52:59 +01002691 // Allow apps with MANAGE_EXTERNAL_STORAGE to create files anywhere
2692 if (!validPath) {
Jeff Sharkey8411c402020-04-29 22:12:36 -06002693 validPath = isCallingPackageManager();
Sahana Rao406cf6d2020-04-08 21:52:59 +01002694 }
2695
2696 // Allow system gallery to create image/video files.
2697 if (!validPath) {
2698 // System gallery can create image/video files in any existing directory, it can
2699 // also create subdirectories in any existing top-level directory. However, system
2700 // gallery is not allowed to create non-default top level directory.
2701 final boolean createNonDefaultTopLevelDir = primary != null &&
2702 !FileUtils.buildPath(volumePath, primary).exists();
2703 validPath = !createNonDefaultTopLevelDir &&
2704 canAccessMediaFile(res.getAbsolutePath(), /*allowLegacy*/ false);
2705 }
2706
Jeff Sharkey61378cb2019-11-23 16:11:09 -07002707 // Nothing left to check; caller can't use this path
2708 if (!validPath) {
Nikita Ioffeb5650d12019-05-22 13:24:41 +01002709 throw new IllegalArgumentException(
2710 "Primary directory " + primary + " not allowed for " + uri
2711 + "; allowed directories are " + allowedPrimary);
2712 }
2713
2714 // Ensure all parent folders of result file exist
2715 res.getParentFile().mkdirs();
2716 if (!res.getParentFile().exists()) {
2717 throw new IllegalStateException("Failed to create directory: " + res);
2718 }
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002719 values.put(MediaColumns.DATA, res.getAbsolutePath());
Sahana Raob105c222020-06-17 20:18:48 +01002720 // buildFile may have changed the file name, compute values to extract new DISPLAY_NAME.
2721 // Note: We can't extract displayName from res.getPath() because for pending & trashed
2722 // files DISPLAY_NAME will not be same as file name.
2723 FileUtils.computeValuesFromData(values, isFuseThread());
Jeff Sharkey077b71e2019-01-22 13:19:51 -07002724 } else {
2725 assertFileColumnsSane(match, uri, values);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002726 }
2727
Abhijeet Kaur3bc15772021-11-17 08:40:34 +00002728 assertPrivatePathNotInValues(values);
2729
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002730 // Drop columns that aren't relevant for special tables
2731 switch (match) {
2732 case AUDIO_ALBUMART:
2733 case VIDEO_THUMBNAILS:
2734 case IMAGES_THUMBNAILS:
Jeff Sharkey89149b62020-03-29 22:03:44 -06002735 final Set<String> valid = getProjectionMap(MediaStore.Images.Thumbnails.class)
2736 .keySet();
2737 for (String key : new ArraySet<>(values.keySet())) {
2738 if (!valid.contains(key)) {
2739 values.remove(key);
2740 }
2741 }
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06002742 break;
2743 }
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06002744
Jeff Sharkey0b801a52019-08-08 11:19:51 -06002745 Trace.endSection();
The Android Open Source Project70215272009-03-03 19:32:43 -08002746 }
2747
Jeff Sharkey077b71e2019-01-22 13:19:51 -07002748 /**
Abhijeet Kaur3bc15772021-11-17 08:40:34 +00002749 * For apps targetSdk >= S: Check that values does not contain any external private path.
2750 * For all apps: Check that values does not contain any other app's external private paths.
2751 */
2752 private void assertPrivatePathNotInValues(ContentValues values)
2753 throws IllegalArgumentException {
2754 ArrayList<String> relativePaths = new ArrayList<String>();
2755 relativePaths.add(extractRelativePath(values.getAsString(MediaColumns.DATA)));
2756 relativePaths.add(values.getAsString(MediaColumns.RELATIVE_PATH));
2757
2758 for (final String relativePath : relativePaths) {
2759 if (!isDataOrObbRelativePath(relativePath)) {
2760 continue;
2761 }
2762
2763 /**
2764 * Don't allow apps to insert/update database row to files in Android/data or
2765 * Android/obb dirs. These are app private directories and files in these private
2766 * directories can't be added to public media collection.
2767 *
2768 * Note: For backwards compatibility we allow apps with targetSdk < S to insert private
2769 * files to MediaProvider
2770 */
2771 if (CompatChanges.isChangeEnabled(ENABLE_CHECKS_FOR_PRIVATE_FILES,
2772 Binder.getCallingUid())) {
2773 throw new IllegalArgumentException(
2774 "Inserting private file: " + relativePath + " is not allowed.");
2775 }
2776
2777 /**
2778 * Restrict all (legacy and non-legacy) apps from inserting paths in other
2779 * app's private directories.
2780 * Allow legacy apps to insert/update files in app private directories for backward
2781 * compatibility but don't allow them to do so in other app's private directories.
2782 */
2783 if (!isCallingIdentityAllowedAccessToDataOrObbPath(relativePath)) {
2784 throw new IllegalArgumentException(
2785 "Inserting private file: " + relativePath + " is not allowed.");
2786 }
2787 }
2788 }
2789
2790 /**
Jeff Sharkey077b71e2019-01-22 13:19:51 -07002791 * Sanity check that any requested {@link MediaColumns#DATA} paths actually
2792 * live on the storage volume being targeted.
2793 */
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06002794 private void assertFileColumnsSane(int match, Uri uri, ContentValues values)
2795 throws VolumeArgumentException, VolumeNotFoundException {
Jeff Sharkey077b71e2019-01-22 13:19:51 -07002796 if (!values.containsKey(MediaColumns.DATA)) return;
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06002797
2798 final String volumeName = resolveVolumeName(uri);
Jeff Sharkey077b71e2019-01-22 13:19:51 -07002799 try {
2800 // Sanity check that the requested path actually lives on volume
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06002801 final Collection<File> allowed = getVolumeScanPaths(volumeName);
Jeff Sharkey077b71e2019-01-22 13:19:51 -07002802 final File actual = new File(values.getAsString(MediaColumns.DATA))
2803 .getCanonicalFile();
2804 if (!FileUtils.contains(allowed, actual)) {
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07002805 throw new VolumeArgumentException(actual, allowed);
Jeff Sharkey077b71e2019-01-22 13:19:51 -07002806 }
2807 } catch (IOException e) {
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06002808 throw new VolumeNotFoundException(volumeName);
Jeff Sharkey077b71e2019-01-22 13:19:51 -07002809 }
2810 }
2811
The Android Open Source Project70215272009-03-03 19:32:43 -08002812 @Override
2813 public int bulkInsert(Uri uri, ContentValues values[]) {
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07002814 final int targetSdkVersion = getCallingPackageTargetSdkVersion();
Jeff Sharkey94461582018-07-12 14:34:47 -06002815 final boolean allowHidden = isCallingPackageAllowedHidden();
2816 final int match = matchUri(uri, allowHidden);
2817
The Android Open Source Project70215272009-03-03 19:32:43 -08002818 if (match == VOLUMES) {
2819 return super.bulkInsert(uri, values);
2820 }
Jeff Sharkey5ed33602019-01-23 14:31:30 -07002821
2822 final DatabaseHelper helper;
Jeff Sharkey5ed33602019-01-23 14:31:30 -07002823 try {
2824 helper = getDatabaseForUri(uri);
Jeff Sharkey5ed33602019-01-23 14:31:30 -07002825 } catch (VolumeNotFoundException e) {
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07002826 return e.translateForUpdateDelete(targetSdkVersion);
Chih-Chung Chang5fde6702010-12-17 18:11:53 +08002827 }
Marco Nelissenccf3e3c2010-01-25 15:37:40 -08002828
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06002829 helper.beginTransaction();
2830 try {
Jeff Sharkey7cfeb762019-03-25 09:56:23 -06002831 final int result = super.bulkInsert(uri, values);
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06002832 helper.setTransactionSuccessful();
Jeff Sharkey7cfeb762019-03-25 09:56:23 -06002833 return result;
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06002834 } finally {
2835 helper.endTransaction();
The Android Open Source Project70215272009-03-03 19:32:43 -08002836 }
The Android Open Source Project70215272009-03-03 19:32:43 -08002837 }
2838
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06002839 private long insertDirectory(@NonNull SQLiteDatabase db, @NonNull String path) {
Jeff Sharkeyc55994b2019-12-20 19:43:59 -07002840 if (LOGV) Log.v(TAG, "inserting directory " + path);
Mike Lockwooded9bbc42011-01-12 19:32:44 -05002841 ContentValues values = new ContentValues();
2842 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
2843 values.put(FileColumns.DATA, path);
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06002844 values.put(FileColumns.PARENT, getParent(db, path));
Jeff Sharkey71437302019-04-09 23:46:52 -06002845 values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path));
2846 values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
2847 values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
2848 values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
Jeff Sharkeya9473e92020-04-17 15:54:30 -06002849 values.put(FileColumns.IS_DOWNLOAD, isDownload(path) ? 1 : 0);
Mike Lockwoodf2921d92011-01-18 09:12:46 -08002850 File file = new File(path);
2851 if (file.exists()) {
2852 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
2853 }
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06002854 return db.insert("files", FileColumns.DATE_MODIFIED, values);
Mike Lockwooded9bbc42011-01-12 19:32:44 -05002855 }
2856
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06002857 private long getParent(@NonNull SQLiteDatabase db, @NonNull String path) {
Jeff Sharkey29c91542019-04-15 15:39:40 -06002858 final String parentPath = new File(path).getParent();
2859 if (Objects.equals("/", parentPath)) {
2860 return -1;
Mike Lockwoodb78ad0d2010-07-03 00:45:10 -04002861 } else {
Jeff Sharkey29c91542019-04-15 15:39:40 -06002862 synchronized (mDirectoryCache) {
2863 Long id = mDirectoryCache.get(parentPath);
2864 if (id != null) {
2865 return id;
2866 }
2867 }
2868
2869 final long id;
2870 try (Cursor c = db.query("files", new String[] { FileColumns._ID },
2871 FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) {
2872 if (c.moveToFirst()) {
2873 id = c.getLong(0);
2874 } else {
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06002875 id = insertDirectory(db, parentPath);
Jeff Sharkey29c91542019-04-15 15:39:40 -06002876 }
2877 }
2878
2879 synchronized (mDirectoryCache) {
2880 mDirectoryCache.put(parentPath, id);
2881 }
2882 return id;
Mike Lockwoodb78ad0d2010-07-03 00:45:10 -04002883 }
2884 }
2885
Sean Stoutcceb5e42017-09-08 11:16:00 -07002886 /**
Sean Stout247d9182018-01-23 11:00:37 -08002887 * @param c the Cursor whose title to retrieve
2888 * @return the result of {@link #getDefaultTitle(String)} if the result is valid; otherwise
2889 * the value of the {@code MediaStore.Audio.Media.TITLE} column
2890 */
2891 private String getDefaultTitleFromCursor(Cursor c) {
2892 String title = null;
2893 final int columnIndex = c.getColumnIndex("title_resource_uri");
2894 // Necessary to check for existence because we may be reading from an old DB version
2895 if (columnIndex > -1) {
2896 final String titleResourceUri = c.getString(columnIndex);
2897 if (titleResourceUri != null) {
2898 try {
2899 title = getDefaultTitle(titleResourceUri);
2900 } catch (Exception e) {
2901 // Best attempt only
2902 }
2903 }
2904 }
2905 if (title == null) {
2906 title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE));
2907 }
2908 return title;
2909 }
2910
2911 /**
2912 * @param title_resource_uri The title resource for which to retrieve the default localization
2913 * @return The title localized to {@code Locale.US}, or {@code null} if unlocalizable
2914 * @throws Exception Thrown if the title appears to be localizable, but the localization failed
2915 * for any reason. For example, the application from which the localized title is fetched is not
2916 * installed, or it does not have the resource which needs to be localized
2917 */
2918 private String getDefaultTitle(String title_resource_uri) throws Exception{
2919 try {
2920 return getTitleFromResourceUri(title_resource_uri, false);
2921 } catch (Exception e) {
2922 Log.e(TAG, "Error getting default title for " + title_resource_uri, e);
2923 throw e;
2924 }
2925 }
2926
2927 /**
2928 * @param title_resource_uri The title resource to localize
2929 * @return The localized title, or {@code null} if unlocalizable
2930 * @throws Exception Thrown if the title appears to be localizable, but the localization failed
2931 * for any reason. For example, the application from which the localized title is fetched is not
2932 * installed, or it does not have the resource which needs to be localized
2933 */
2934 private String getLocalizedTitle(String title_resource_uri) throws Exception {
2935 try {
2936 return getTitleFromResourceUri(title_resource_uri, true);
2937 } catch (Exception e) {
2938 Log.e(TAG, "Error getting localized title for " + title_resource_uri, e);
2939 throw e;
2940 }
2941 }
2942
2943 /**
Sean Stoutcceb5e42017-09-08 11:16:00 -07002944 * Localizable titles conform to this URI pattern:
2945 * Scheme: {@link ContentResolver.SCHEME_ANDROID_RESOURCE}
2946 * Authority: Package Name of ringtone title provider
2947 * First Path Segment: Type of resource (must be "string")
Sean Stout247d9182018-01-23 11:00:37 -08002948 * Second Path Segment: Resource name of title
Sean Stoutcceb5e42017-09-08 11:16:00 -07002949 *
Sean Stout247d9182018-01-23 11:00:37 -08002950 * @param title_resource_uri The title resource to retrieve
2951 * @param localize Whether or not to localize the title
2952 * @return The title, or {@code null} if unlocalizable
Sean Stoutcceb5e42017-09-08 11:16:00 -07002953 * @throws Exception Thrown if the title appears to be localizable, but the localization failed
2954 * for any reason. For example, the application from which the localized title is fetched is not
Sean Stout247d9182018-01-23 11:00:37 -08002955 * installed, or it does not have the resource which needs to be localized
Sean Stoutcceb5e42017-09-08 11:16:00 -07002956 */
Sean Stout247d9182018-01-23 11:00:37 -08002957 private String getTitleFromResourceUri(String title_resource_uri, boolean localize)
2958 throws Exception {
2959 if (TextUtils.isEmpty(title_resource_uri)) {
2960 return null;
Sean Stoutcceb5e42017-09-08 11:16:00 -07002961 }
Sean Stout247d9182018-01-23 11:00:37 -08002962 final Uri titleUri = Uri.parse(title_resource_uri);
2963 final String scheme = titleUri.getScheme();
2964 if (!ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
2965 return null;
2966 }
2967 final List<String> pathSegments = titleUri.getPathSegments();
2968 if (pathSegments.size() != 2) {
2969 Log.e(TAG, "Error getting localized title for " + title_resource_uri
2970 + ", must have 2 path segments");
2971 return null;
2972 }
2973 final String type = pathSegments.get(0);
2974 if (!"string".equals(type)) {
2975 Log.e(TAG, "Error getting localized title for " + title_resource_uri
2976 + ", first path segment must be \"string\"");
2977 return null;
2978 }
2979 final String packageName = titleUri.getAuthority();
2980 final Resources resources;
2981 if (localize) {
2982 resources = mPackageManager.getResourcesForApplication(packageName);
2983 } else {
2984 final Context packageContext = getContext().createPackageContext(packageName, 0);
2985 final Configuration configuration = packageContext.getResources().getConfiguration();
2986 configuration.setLocale(Locale.US);
2987 resources = packageContext.createConfigurationContext(configuration).getResources();
2988 }
2989 final String resourceIdentifier = pathSegments.get(1);
2990 final int id = resources.getIdentifier(resourceIdentifier, type, packageName);
2991 return resources.getString(id);
Sean Stoutcceb5e42017-09-08 11:16:00 -07002992 }
2993
Jeff Sharkey7eed0eb2019-01-19 17:27:46 -07002994 public void onLocaleChanged() {
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06002995 mInternalDatabase.runWithTransaction((db) -> {
2996 localizeTitles(db);
2997 return null;
2998 });
Jeff Sharkey7eed0eb2019-01-19 17:27:46 -07002999 }
3000
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06003001 private void localizeTitles(@NonNull SQLiteDatabase db) {
Jeff Sharkey71437302019-04-09 23:46:52 -06003002 try (Cursor c = db.query("files", new String[]{"_id", "title_resource_uri"},
3003 "title_resource_uri IS NOT NULL", null, null, null, null)) {
3004 while (c.moveToNext()) {
3005 final String id = c.getString(0);
3006 final String titleResourceUri = c.getString(1);
3007 final ContentValues values = new ContentValues();
3008 try {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003009 values.put(AudioColumns.TITLE_RESOURCE_URI, titleResourceUri);
3010 computeAudioLocalizedValues(values);
3011 computeAudioKeyValues(values);
Jeff Sharkey71437302019-04-09 23:46:52 -06003012 db.update("files", values, "_id=?", new String[]{id});
3013 } catch (Exception e) {
3014 Log.e(TAG, "Error updating localized title for " + titleResourceUri
3015 + ", keeping old localization");
Sean Stoutcceb5e42017-09-08 11:16:00 -07003016 }
3017 }
3018 }
3019 }
3020
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003021 private Uri insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper,
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07003022 int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values,
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003023 int mediaType) throws VolumeArgumentException, VolumeNotFoundException {
Winsonb653af22019-06-05 12:14:13 -07003024 boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA)
3025 || TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA));
3026
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06003027 // Make sure all file-related columns are defined
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003028 ensureUniqueFileColumns(match, uri, extras, values, null);
Mike Lockwood5d7e71a2010-07-12 08:49:32 -04003029
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003030 switch (mediaType) {
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003031 case FileColumns.MEDIA_TYPE_AUDIO: {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003032 computeAudioLocalizedValues(values);
3033 computeAudioKeyValues(values);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003034 break;
3035 }
3036 }
3037
Mike Lockwoodfb598dd2010-11-16 10:23:43 -05003038 // compute bucket_id and bucket_display_name for all files
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003039 String path = values.getAsString(MediaStore.MediaColumns.DATA);
Sahana Raoea587fc2020-06-03 15:56:23 +01003040 FileUtils.computeValuesFromData(values, isFuseThread());
Mike Lockwoodfb598dd2010-11-16 10:23:43 -05003041 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003042
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003043 String title = values.getAsString(MediaStore.MediaColumns.TITLE);
Marco Nelissen3572ac72010-11-05 15:46:31 -07003044 if (title == null && path != null) {
Jeff Sharkeye152d5762019-10-11 17:14:51 -06003045 title = extractFileName(path);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003046 }
3047 values.put(FileColumns.TITLE, title);
3048
Sudheer Shanka0e03f662019-04-23 12:10:46 -07003049 String mimeType = null;
3050 int format = MtpConstants.FORMAT_ASSOCIATION;
3051 if (path != null && new File(path).isDirectory()) {
3052 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION);
3053 values.putNull(MediaStore.MediaColumns.MIME_TYPE);
3054 } else {
3055 mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE);
3056 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT);
3057 format = (formatObject == null ? 0 : formatObject.intValue());
3058 }
3059
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003060 if (format == 0) {
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -07003061 format = MimeUtils.resolveFormatCode(mimeType);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003062 }
Marco Nelissen89734e12016-09-30 15:21:35 -07003063 if (path != null && path.endsWith("/")) {
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003064 // TODO: convert to using FallbackException once VERSION_CODES.S is defined
Marco Nelissen759a9b72016-09-19 14:02:20 -07003065 Log.e(TAG, "directory has trailing slash: " + path);
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003066 return null;
Marco Nelissen759a9b72016-09-19 14:02:20 -07003067 }
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003068 if (format != 0) {
3069 values.put(FileColumns.FORMAT, format);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003070 }
3071
Marco Nelissen8247cc42016-09-28 13:41:25 -07003072 if (mimeType == null && path != null && format != MtpConstants.FORMAT_ASSOCIATION) {
Jeff Sharkeye152d5762019-10-11 17:14:51 -06003073 mimeType = MimeUtils.resolveMimeType(new File(path));
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003074 }
Marco Nelissenff93c6c2018-04-25 12:25:24 -07003075
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003076 if (mimeType != null) {
3077 values.put(FileColumns.MIME_TYPE, mimeType);
Jeff Sharkey8411c402020-04-29 22:12:36 -06003078 if (isCallingPackageSelf() && values.containsKey(FileColumns.MEDIA_TYPE)) {
Sahana Rao663f1442020-04-27 20:13:09 +01003079 // Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and
3080 // FileColumns.MEDIA_TYPE is already populated.
Sahana Rao549c9c32020-06-15 14:06:55 +01003081 } else if (path != null && shouldFileBeHidden(new File(path))) {
3082 values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE);
3083 } else {
Sahana Rao663f1442020-04-27 20:13:09 +01003084 values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType));
3085 }
Jeff Sharkey21e297e2019-12-06 18:14:32 -07003086 } else {
3087 values.put(FileColumns.MEDIA_TYPE, mediaType);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003088 }
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003089
Jeff Sharkey4a1293b2019-10-18 18:56:36 -06003090 final long rowId;
3091 {
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003092 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
Marco Nelissen3572ac72010-11-05 15:46:31 -07003093 String name = values.getAsString(Audio.Playlists.NAME);
Mike Lockwood282dc902010-11-12 10:44:38 -05003094 if (name == null && path == null) {
3095 // MediaScanner will compute the name from the path if we have one
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003096 throw new IllegalArgumentException(
Mike Lockwood282dc902010-11-12 10:44:38 -05003097 "no name was provided when inserting abstract playlist");
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003098 }
3099 } else {
Brian Muramatsua36cfae2010-11-30 13:46:02 -08003100 if (path == null) {
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003101 // path might be null for playlists created on the device
3102 // or transfered via MTP
3103 throw new IllegalArgumentException(
3104 "no path was provided when inserting new file");
3105 }
3106 }
3107
Mike Lockwoodf2921d92011-01-18 09:12:46 -08003108 // make sure modification date and size are set
3109 if (path != null) {
3110 File file = new File(path);
3111 if (file.exists()) {
3112 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000);
Marco Nelissen34d6cfa2012-08-02 10:20:53 -07003113 if (!values.containsKey(FileColumns.SIZE)) {
3114 values.put(FileColumns.SIZE, file.length());
3115 }
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003116 }
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003117 }
3118
Nandana Dutta1059a42019-12-23 12:48:00 +00003119 rowId = insertAllowingUpsert(qb, helper, values, path);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003120 }
Marco Nelissend4e13122012-05-14 13:23:34 -07003121 if (format == MtpConstants.FORMAT_ASSOCIATION) {
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06003122 synchronized (mDirectoryCache) {
Marco Nelissen971408e2016-09-12 16:43:50 -07003123 mDirectoryCache.put(path, rowId);
3124 }
Marco Nelissend4e13122012-05-14 13:23:34 -07003125 }
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003126
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003127 return ContentUris.withAppendedId(uri, rowId);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003128 }
3129
Nandana Dutta1059a42019-12-23 12:48:00 +00003130 /**
3131 * Inserts a new row in MediaProvider database with {@code values}. Treats insert as upsert for
3132 * double inserts from same package.
3133 */
3134 private long insertAllowingUpsert(@NonNull SQLiteQueryBuilder qb,
3135 @NonNull DatabaseHelper helper, @NonNull ContentValues values, String path)
3136 throws SQLiteConstraintException {
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06003137 return helper.runWithTransaction((db) -> {
Jeff Sharkey564929d2020-04-06 16:51:58 -06003138 Long parent = values.getAsLong(FileColumns.PARENT);
3139 if (parent == null) {
3140 if (path != null) {
3141 final long parentId = getParent(db, path);
3142 values.put(FileColumns.PARENT, parentId);
3143 }
3144 }
3145
Nandana Dutta1059a42019-12-23 12:48:00 +00003146 try {
3147 return qb.insert(helper, values);
3148 } catch (SQLiteConstraintException e) {
Sahana Rao25db6492020-06-06 16:22:23 +01003149 final String packages = getAllowedPackagesForUpsert(
3150 values.getAsString(MediaColumns.OWNER_PACKAGE_NAME));
Sahana Raobb3d94a2020-04-07 13:15:05 +01003151 SQLiteQueryBuilder qbForUpsert = getQueryBuilderForUpsert(path);
Sahana Rao25db6492020-06-06 16:22:23 +01003152 final long rowId = getIdIfPathOwnedByPackages(qbForUpsert, helper, path, packages);
Nandana Dutta1059a42019-12-23 12:48:00 +00003153 // Apps sometimes create a file via direct path and then insert it into
3154 // MediaStore via ContentResolver. The former should create a database entry,
3155 // so we have to treat the latter as an upsert.
3156 // TODO(b/149917493) Perform all INSERT operations as UPSERT.
Sahana Raobb3d94a2020-04-07 13:15:05 +01003157 if (rowId != -1 && qbForUpsert.update(helper, values, "_id=?",
Nandana Dutta1059a42019-12-23 12:48:00 +00003158 new String[]{Long.toString(rowId)}) == 1) {
3159 return rowId;
3160 }
3161 // Rethrow SQLiteConstraintException on failed upsert.
3162 throw e;
3163 }
3164 });
3165 }
3166
3167 /**
Sahana Rao25db6492020-06-06 16:22:23 +01003168 * @return row id of the entry with path {@code path} if the owner is one of {@code packages}.
Nandana Dutta1059a42019-12-23 12:48:00 +00003169 */
Sahana Rao25db6492020-06-06 16:22:23 +01003170 private long getIdIfPathOwnedByPackages(@NonNull SQLiteQueryBuilder qb,
3171 @NonNull DatabaseHelper helper, String path, String packages) {
3172 final String[] projection = new String[] {FileColumns._ID};
3173 final String ownerPackageMatchClause = DatabaseUtils.bindSelection(
3174 MediaColumns.OWNER_PACKAGE_NAME + " IN " + packages);
3175 final String selection = FileColumns.DATA + " =? AND " + ownerPackageMatchClause;
Nandana Dutta1059a42019-12-23 12:48:00 +00003176
Sahana Rao4a81bbc2020-04-07 13:01:21 +01003177 try (Cursor c = qb.query(helper, projection, selection, new String[] {path}, null, null,
3178 null, null, null)) {
Nandana Dutta1059a42019-12-23 12:48:00 +00003179 if (c.moveToFirst()) {
Sahana Rao25db6492020-06-06 16:22:23 +01003180 return c.getLong(0);
Nandana Dutta1059a42019-12-23 12:48:00 +00003181 }
3182 }
3183 return -1;
3184 }
3185
Sahana Raobb3d94a2020-04-07 13:15:05 +01003186 /**
Sahana Rao25db6492020-06-06 16:22:23 +01003187 * Gets packages that should match to upsert a db row.
3188 *
3189 * A database row can be upserted if
3190 * <ul>
3191 * <li> Calling package or one of the shared packages owns the db row.
3192 * <li> {@code givenOwnerPackage} owns the db row. This is useful when DownloadProvider
3193 * requests upsert on behalf of another app
3194 * </ul>
3195 */
3196 private String getAllowedPackagesForUpsert(@Nullable String givenOwnerPackage) {
3197 ArrayList<String> packages = new ArrayList<>();
3198 packages.addAll(Arrays.asList(mCallingIdentity.get().getSharedPackageNames()));
3199
3200 // If givenOwnerPackage is CallingIdentity, packages list would already have shared package
3201 // names of givenOwnerPackage. If givenOwnerPackage is not CallingIdentity, since
3202 // DownloadProvider can upsert a row on behalf of app, we should include all shared packages
3203 // of givenOwnerPackage.
Jeff Sharkey8411c402020-04-29 22:12:36 -06003204 if (givenOwnerPackage != null && isCallingPackageDelegator() &&
Sahana Rao25db6492020-06-06 16:22:23 +01003205 !isCallingIdentitySharedPackageName(givenOwnerPackage)) {
3206 // Allow DownloadProvider to Upsert if givenOwnerPackage is owner of the db row.
3207 packages.addAll(Arrays.asList(getSharedPackagesForPackage(givenOwnerPackage)));
3208 }
3209 return bindList((Object[]) packages.toArray());
3210 }
3211
3212 /**
Sahana Raobb3d94a2020-04-07 13:15:05 +01003213 * @return {@link SQLiteQueryBuilder} for upsert with Files uri. This disables strict columns
3214 * check to allow upsert to update any column with Files uri.
3215 */
3216 private SQLiteQueryBuilder getQueryBuilderForUpsert(@NonNull String path) {
Sahana Raobb3d94a2020-04-07 13:15:05 +01003217 final boolean allowHidden = isCallingPackageAllowedHidden();
Sahana Raoea587fc2020-06-03 15:56:23 +01003218 Bundle extras = new Bundle();
3219 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE);
3220 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE);
3221
Sahana Raobb3d94a2020-04-07 13:15:05 +01003222 // When Fuse inserts a file to database it doesn't set is_download column. When app tries
3223 // insert with Downloads uri, upsert fails because getIdIfPathExistsForCallingPackage can't
Sahana Raoea587fc2020-06-03 15:56:23 +01003224 // find a row ID with is_download=1. Use Files uri to get queryBuilder & update any existing
3225 // row irrespective of is_download=1.
3226 final Uri uri = FileUtils.getContentUriForPath(path);
Sahana Raobb3d94a2020-04-07 13:15:05 +01003227 SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri,
Sahana Raoea587fc2020-06-03 15:56:23 +01003228 extras, null);
Sahana Raobb3d94a2020-04-07 13:15:05 +01003229
3230 // We won't be able to update columns that are not part of projection map of Files table. We
3231 // have already checked strict columns in previous insert operation which failed with
3232 // exception. Any malicious column usage would have got caught in insert operation, hence we
3233 // can safely disable strict column check for upsert.
3234 qb.setStrictColumns(false);
3235 return qb;
3236 }
3237
Jeff Sharkey58f533a2018-08-06 18:31:51 -06003238 private void maybePut(@NonNull ContentValues values, @NonNull String key,
3239 @Nullable String value) {
3240 if (value != null) {
3241 values.put(key, value);
3242 }
3243 }
3244
Sudheer Shanka56cba322018-12-07 10:55:58 -08003245 private boolean maybeMarkAsDownload(@NonNull ContentValues values) {
3246 final String path = values.getAsString(MediaColumns.DATA);
3247 if (path != null && isDownload(path)) {
Jeff Sharkeya9473e92020-04-17 15:54:30 -06003248 values.put(FileColumns.IS_DOWNLOAD, 1);
Sudheer Shanka56cba322018-12-07 10:55:58 -08003249 return true;
3250 }
3251 return false;
3252 }
3253
Jeff Sharkey71437302019-04-09 23:46:52 -06003254 private static @NonNull String resolveVolumeName(@NonNull Uri uri) {
3255 final String volumeName = getVolumeName(uri);
3256 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
3257 return MediaStore.VOLUME_EXTERNAL_PRIMARY;
3258 } else {
3259 return volumeName;
3260 }
3261 }
3262
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003263 /**
3264 * @deprecated all operations should be routed through the overload that
3265 * accepts a {@link Bundle} of extras.
3266 */
Jeff Sharkey199f8c82019-03-23 11:54:21 -06003267 @Override
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003268 @Deprecated
3269 public Uri insert(Uri uri, ContentValues values) {
3270 return insert(uri, values, null);
3271 }
3272
3273 @Override
3274 public @Nullable Uri insert(@NonNull Uri uri, @Nullable ContentValues values,
3275 @Nullable Bundle extras) {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06003276 Trace.beginSection("insert");
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06003277 try {
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07003278 try {
3279 return insertInternal(uri, values, extras);
3280 } catch (SQLiteConstraintException e) {
3281 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
3282 throw e;
3283 } else {
3284 return null;
3285 }
3286 }
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003287 } catch (FallbackException e) {
3288 return e.translateForInsert(getCallingPackageTargetSdkVersion());
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06003289 } finally {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06003290 Trace.endSection();
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06003291 }
3292 }
3293
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003294 private @Nullable Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
3295 @Nullable Bundle extras) throws FallbackException {
Jeff Sharkeyd6697822020-03-22 20:59:47 -06003296 extras = (extras != null) ? extras : new Bundle();
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003297
Nikita Ioffe710787b2020-06-11 14:35:14 +01003298 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
3299 extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
3300
Jeff Sharkey199f8c82019-03-23 11:54:21 -06003301 final boolean allowHidden = isCallingPackageAllowedHidden();
3302 final int match = matchUri(uri, allowHidden);
3303
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07003304 final int targetSdkVersion = getCallingPackageTargetSdkVersion();
Jeff Sharkey71437302019-04-09 23:46:52 -06003305 final String originalVolumeName = getVolumeName(uri);
3306 final String resolvedVolumeName = resolveVolumeName(uri);
Jeff Sharkeyb39b32d2013-09-26 13:49:07 -07003307
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003308 // handle MEDIA_SCANNER before calling getDatabaseForUri()
3309 if (match == MEDIA_SCANNER) {
Mike Lockwoodbc442ef2011-07-20 08:29:28 -07003310 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME);
Jeff Sharkey5ed33602019-01-23 14:31:30 -07003311
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003312 final DatabaseHelper helper = getDatabaseForUri(
3313 MediaStore.Files.getContentUri(mMediaScannerVolume));
Jeff Sharkey5ed33602019-01-23 14:31:30 -07003314
Jeff Sharkey7ea24f22019-08-22 10:14:18 -06003315 helper.mScanStartTime = SystemClock.elapsedRealtime();
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003316 return MediaStore.getMediaScannerUri();
3317 }
3318
Jeff Sharkey5ed33602019-01-23 14:31:30 -07003319 if (match == VOLUMES) {
3320 String name = initialValues.getAsString("name");
Zim604f4522020-06-05 15:30:09 +01003321 Uri attachedVolume = attachVolume(name, /* validate */ true);
Jeff Sharkey5ed33602019-01-23 14:31:30 -07003322 if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003323 final DatabaseHelper helper = getDatabaseForUri(
3324 MediaStore.Files.getContentUri(mMediaScannerVolume));
Jeff Sharkey7ea24f22019-08-22 10:14:18 -06003325 helper.mScanStartTime = SystemClock.elapsedRealtime();
Jeff Sharkey5ed33602019-01-23 14:31:30 -07003326 }
3327 return attachedVolume;
3328 }
3329
Jeff Sharkeyd6697822020-03-22 20:59:47 -06003330 switch (match) {
3331 case AUDIO_PLAYLISTS_ID:
3332 case AUDIO_PLAYLISTS_ID_MEMBERS: {
3333 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
3334 final Uri playlistUri = ContentUris.withAppendedId(
3335 MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId);
3336
3337 final long audioId = initialValues
3338 .getAsLong(MediaStore.Audio.Playlists.Members.AUDIO_ID);
3339 final Uri audioUri = ContentUris.withAppendedId(
3340 MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId);
3341
3342 // Require that caller has write access to underlying media
3343 enforceCallingPermission(playlistUri, Bundle.EMPTY, true);
3344 enforceCallingPermission(audioUri, Bundle.EMPTY, false);
3345
3346 // Playlist contents are always persisted directly into playlist
3347 // files on disk to ensure that we can reliably migrate between
3348 // devices and recover from database corruption
3349 final long id = addPlaylistMembers(playlistUri, initialValues);
3350 return ContentUris.withAppendedId(MediaStore.Audio.Playlists.Members
3351 .getContentUri(originalVolumeName, playlistId), id);
3352 }
3353 }
3354
Marco Nelissen38b43642012-01-27 09:40:07 -08003355 String path = null;
Jeff Sharkey58f533a2018-08-06 18:31:51 -06003356 String ownerPackageName = null;
Mike Lockwoodb8f9b762011-07-31 17:51:07 -04003357 if (initialValues != null) {
Jeff Sharkey41f18542019-10-16 13:03:38 -06003358 // IDs are forever; nobody should be editing them
3359 initialValues.remove(MediaColumns._ID);
3360
Jeff Sharkey05c3a032020-04-09 16:57:04 -06003361 // Expiration times are hard-coded; let's derive them
3362 FileUtils.computeDateExpires(initialValues);
3363
Jeff Sharkeyb0cab582019-04-16 12:42:42 -06003364 // Ignore or augment incoming raw filesystem paths
Jeff Sharkey0218c142018-10-19 15:37:00 -06003365 for (String column : sDataColumns.keySet()) {
3366 if (!initialValues.containsKey(column)) continue;
Jeff Sharkeyb0cab582019-04-16 12:42:42 -06003367
Jeff Sharkey8411c402020-04-29 22:12:36 -06003368 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06003369 // Mutation allowed
Jeff Sharkeyb0cab582019-04-16 12:42:42 -06003370 } else {
3371 Log.w(TAG, "Ignoring mutation of " + column + " from "
3372 + getCallingPackageOrSelf());
3373 initialValues.remove(column);
3374 }
Jeff Sharkey16fb8052018-10-18 15:22:53 -06003375 }
3376
Marco Nelissen38b43642012-01-27 09:40:07 -08003377 path = initialValues.getAsString(MediaStore.MediaColumns.DATA);
Jeff Sharkey58f533a2018-08-06 18:31:51 -06003378
Jeff Sharkey8411c402020-04-29 22:12:36 -06003379 if (!isCallingPackageSelf()) {
Sudheer Shankae20b0072018-12-21 10:33:48 -08003380 initialValues.remove(FileColumns.IS_DOWNLOAD);
3381 }
Sudheer Shankaaa626512018-11-15 20:29:28 -08003382
Jeff Sharkeyd2a38222018-12-04 11:23:48 -07003383 // We no longer track location metadata
3384 if (initialValues.containsKey(ImageColumns.LATITUDE)) {
3385 initialValues.putNull(ImageColumns.LATITUDE);
3386 }
3387 if (initialValues.containsKey(ImageColumns.LONGITUDE)) {
3388 initialValues.putNull(ImageColumns.LONGITUDE);
3389 }
3390
Jeff Sharkey8411c402020-04-29 22:12:36 -06003391 if (isCallingPackageSelf() || isCallingPackageShell()) {
3392 // When media inserted by ourselves during a scan, or by the
3393 // shell, the best we can do is guess ownership based on path
3394 // when it's not explicitly provided
Sudheer Shankaff5328a2019-04-22 13:13:03 -07003395 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
3396 if (TextUtils.isEmpty(ownerPackageName)) {
3397 ownerPackageName = extractPathOwnerPackageName(path);
3398 }
Jeff Sharkey8411c402020-04-29 22:12:36 -06003399 } else if (isCallingPackageDelegator()) {
3400 // When caller is a delegator, we handle ownership as a hybrid
3401 // of the two other cases: we're willing to accept any ownership
3402 // transfer attempted during insert, but we fall back to using
3403 // the Binder identity if they don't request a specific owner
3404 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME);
3405 if (TextUtils.isEmpty(ownerPackageName)) {
3406 ownerPackageName = getCallingPackageOrSelf();
3407 }
Jeff Sharkey58f533a2018-08-06 18:31:51 -06003408 } else {
Sudheer Shankaff5328a2019-04-22 13:13:03 -07003409 // Remote callers have no direct control over owner column; we force
3410 // it be whoever is creating the content.
3411 initialValues.remove(FileColumns.OWNER_PACKAGE_NAME);
Jeff Sharkey0218c142018-10-19 15:37:00 -06003412 ownerPackageName = getCallingPackageOrSelf();
Jeff Sharkey58f533a2018-08-06 18:31:51 -06003413 }
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06003414 }
Marco Nelissen38b43642012-01-27 09:40:07 -08003415
Jeff Sharkey199f8c82019-03-23 11:54:21 -06003416 long rowId = -1;
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003417 Uri newUri = null;
Marco Nelissen10af34f2011-12-16 17:59:52 -08003418
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003419 final DatabaseHelper helper = getDatabaseForUri(uri);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07003420 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null);
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003421
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003422 switch (match) {
3423 case IMAGES_MEDIA: {
Jeff Sharkey58f533a2018-08-06 18:31:51 -06003424 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
Sudheer Shanka56cba322018-12-07 10:55:58 -08003425 final boolean isDownload = maybeMarkAsDownload(initialValues);
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003426 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
3427 FileColumns.MEDIA_TYPE_IMAGE);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003428 break;
3429 }
3430
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003431 case IMAGES_THUMBNAILS: {
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06003432 if (helper.mInternal) {
3433 throw new UnsupportedOperationException(
3434 "Writing to internal storage is not supported.");
3435 }
3436
3437 // Require that caller has write access to underlying media
3438 final long imageId = initialValues.getAsLong(MediaStore.Images.Thumbnails.IMAGE_ID);
3439 enforceCallingPermission(ContentUris.withAppendedId(
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003440 MediaStore.Images.Media.getContentUri(resolvedVolumeName), imageId),
3441 extras, true);
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06003442
Jeff Sharkeyab27f022020-04-29 20:58:55 -06003443 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06003444
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07003445 rowId = qb.insert(helper, initialValues);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003446 if (rowId > 0) {
3447 newUri = ContentUris.withAppendedId(Images.Thumbnails.
Jeff Sharkey71437302019-04-09 23:46:52 -06003448 getContentUri(originalVolumeName), rowId);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003449 }
3450 break;
3451 }
3452
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003453 case VIDEO_THUMBNAILS: {
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06003454 if (helper.mInternal) {
3455 throw new UnsupportedOperationException(
3456 "Writing to internal storage is not supported.");
3457 }
3458
3459 // Require that caller has write access to underlying media
3460 final long videoId = initialValues.getAsLong(MediaStore.Video.Thumbnails.VIDEO_ID);
3461 enforceCallingPermission(ContentUris.withAppendedId(
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003462 MediaStore.Video.Media.getContentUri(resolvedVolumeName), videoId),
3463 Bundle.EMPTY, true);
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06003464
Jeff Sharkeyab27f022020-04-29 20:58:55 -06003465 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06003466
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07003467 rowId = qb.insert(helper, initialValues);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003468 if (rowId > 0) {
3469 newUri = ContentUris.withAppendedId(Video.Thumbnails.
Jeff Sharkey71437302019-04-09 23:46:52 -06003470 getContentUri(originalVolumeName), rowId);
Mike Lockwoodafa157c2010-09-14 19:34:41 -04003471 }
3472 break;
3473 }
3474
3475 case AUDIO_MEDIA: {
Jeff Sharkey58f533a2018-08-06 18:31:51 -06003476 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
Sudheer Shanka56cba322018-12-07 10:55:58 -08003477 final boolean isDownload = maybeMarkAsDownload(initialValues);
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003478 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
3479 FileColumns.MEDIA_TYPE_AUDIO);
The Android Open Source Project70215272009-03-03 19:32:43 -08003480 break;
3481 }
3482
3483 case AUDIO_MEDIA_ID_GENRES: {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003484 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
The Android Open Source Project70215272009-03-03 19:32:43 -08003485 }
3486
The Android Open Source Project70215272009-03-03 19:32:43 -08003487 case AUDIO_GENRES: {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003488 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
The Android Open Source Project70215272009-03-03 19:32:43 -08003489 }
3490
3491 case AUDIO_GENRES_ID_MEMBERS: {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003492 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
The Android Open Source Project70215272009-03-03 19:32:43 -08003493 }
3494
3495 case AUDIO_PLAYLISTS: {
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06003496 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
Sudheer Shanka56cba322018-12-07 10:55:58 -08003497 final boolean isDownload = maybeMarkAsDownload(initialValues);
Mike Lockwoodbc442ef2011-07-20 08:29:28 -07003498 ContentValues values = new ContentValues(initialValues);
The Android Open Source Project70215272009-03-03 19:32:43 -08003499 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000);
Jeff Sharkeyd6697822020-03-22 20:59:47 -06003500 // Playlist names are stored as display names, but leave
3501 // values untouched if the caller is ModernMediaScanner
Jeff Sharkey8411c402020-04-29 22:12:36 -06003502 if (!isCallingPackageSelf()) {
Jeff Sharkeyd6697822020-03-22 20:59:47 -06003503 if (values.containsKey(Playlists.NAME)) {
3504 values.put(MediaColumns.DISPLAY_NAME, values.getAsString(Playlists.NAME));
3505 }
3506 if (!values.containsKey(MediaColumns.MIME_TYPE)) {
3507 values.put(MediaColumns.MIME_TYPE, "audio/mpegurl");
3508 }
3509 }
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003510 newUri = insertFile(qb, helper, match, uri, extras, values,
3511 FileColumns.MEDIA_TYPE_PLAYLIST);
3512 if (newUri != null) {
Jeff Sharkeyd6697822020-03-22 20:59:47 -06003513 // Touch empty playlist file on disk so its ready for renames
3514 if (Binder.getCallingUid() != android.os.Process.myUid()) {
3515 try (OutputStream out = ContentResolver.wrap(this)
3516 .openOutputStream(newUri)) {
3517 } catch (IOException ignored) {
3518 }
3519 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003520 }
3521 break;
3522 }
3523
3524 case VIDEO_MEDIA: {
Jeff Sharkey58f533a2018-08-06 18:31:51 -06003525 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
Sudheer Shanka56cba322018-12-07 10:55:58 -08003526 final boolean isDownload = maybeMarkAsDownload(initialValues);
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003527 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
3528 FileColumns.MEDIA_TYPE_VIDEO);
The Android Open Source Project70215272009-03-03 19:32:43 -08003529 break;
3530 }
3531
Mike Lockwoodc198bd92010-09-10 14:55:20 -04003532 case AUDIO_ALBUMART: {
Marco Nelissen10af34f2011-12-16 17:59:52 -08003533 if (helper.mInternal) {
The Android Open Source Project70215272009-03-03 19:32:43 -08003534 throw new UnsupportedOperationException("no internal album art allowed");
3535 }
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06003536
Jeff Sharkeyab27f022020-04-29 20:58:55 -06003537 ensureUniqueFileColumns(match, uri, extras, initialValues, null);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06003538
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07003539 rowId = qb.insert(helper, initialValues);
The Android Open Source Project70215272009-03-03 19:32:43 -08003540 if (rowId > 0) {
3541 newUri = ContentUris.withAppendedId(uri, rowId);
3542 }
3543 break;
Mike Lockwoodc198bd92010-09-10 14:55:20 -04003544 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003545
Sudheer Shanka56cba322018-12-07 10:55:58 -08003546 case FILES: {
Jeff Sharkey58f533a2018-08-06 18:31:51 -06003547 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
Sudheer Shanka56cba322018-12-07 10:55:58 -08003548 final boolean isDownload = maybeMarkAsDownload(initialValues);
Jeff Sharkeye3650642020-04-03 18:50:03 -06003549 final String mimeType = initialValues.getAsString(MediaColumns.MIME_TYPE);
3550 final int mediaType = MimeUtils.resolveMediaType(mimeType);
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003551 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
3552 mediaType);
Mike Lockwoodfc824ed2010-10-26 14:40:48 -04003553 break;
Sudheer Shanka56cba322018-12-07 10:55:58 -08003554 }
Mike Lockwoodfc824ed2010-10-26 14:40:48 -04003555
Sudheer Shankaaa626512018-11-15 20:29:28 -08003556 case DOWNLOADS:
3557 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName);
Jeff Sharkeya9473e92020-04-17 15:54:30 -06003558 initialValues.put(FileColumns.IS_DOWNLOAD, 1);
Jeff Sharkeyf95b06f2020-06-02 11:10:35 -06003559 newUri = insertFile(qb, helper, match, uri, extras, initialValues,
3560 FileColumns.MEDIA_TYPE_NONE);
Sudheer Shankaaa626512018-11-15 20:29:28 -08003561 break;
3562
The Android Open Source Project70215272009-03-03 19:32:43 -08003563 default:
3564 throw new UnsupportedOperationException("Invalid URI " + uri);
3565 }
3566
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06003567 // Remember that caller is owner of this item, to speed up future
3568 // permission checks for this caller
3569 mCallingIdentity.get().setOwned(rowId, true);
3570
Jeff Sharkey470b97e2019-10-15 16:32:04 -06003571 if (path != null && path.toLowerCase(Locale.ROOT).endsWith("/.nomedia")) {
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07003572 mMediaScanner.scanFile(new File(path).getParentFile(), REASON_DEMAND);
Marco Nelissen38b43642012-01-27 09:40:07 -08003573 }
Jeff Sharkey199f8c82019-03-23 11:54:21 -06003574
The Android Open Source Project70215272009-03-03 19:32:43 -08003575 return newUri;
3576 }
3577
Marco Nelissencb0c5a62009-12-08 13:44:19 -08003578 @Override
3579 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
3580 throws OperationApplicationException {
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06003581 // Open transactions on databases for requested volumes
Hyoungho Choib0ddb732020-03-25 16:49:02 +09003582 final Set<DatabaseHelper> transactions = new ArraySet<>();
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06003583 try {
3584 for (ContentProviderOperation op : operations) {
Hyoungho Choib0ddb732020-03-25 16:49:02 +09003585 final DatabaseHelper helper = getDatabaseForUri(op.getUri());
Jeff Sharkeyb84a9992020-05-01 20:06:02 -06003586 if (transactions.contains(helper)) continue;
3587
3588 if (!helper.isTransactionActive()) {
Hyoungho Choib0ddb732020-03-25 16:49:02 +09003589 helper.beginTransaction();
3590 transactions.add(helper);
Jeff Sharkeyb84a9992020-05-01 20:06:02 -06003591 } else {
3592 // We normally don't allow nested transactions (since we
3593 // don't have a good way to selectively roll them back) but
3594 // if the incoming operation is ignoring exceptions, then we
3595 // don't need to worry about partial rollback and can
3596 // piggyback on the larger active transaction
3597 if (!op.isExceptionAllowed()) {
3598 throw new IllegalStateException("Nested transactions not supported");
3599 }
Marco Nelissenc80fa202017-04-19 11:59:01 -07003600 }
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06003601 }
Jeff Sharkey5ed33602019-01-23 14:31:30 -07003602
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06003603 final ContentProviderResult[] result = super.applyBatch(operations);
Hyoungho Choib0ddb732020-03-25 16:49:02 +09003604 for (DatabaseHelper helper : transactions) {
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06003605 helper.setTransactionSuccessful();
3606 }
3607 return result;
Hyoungho Choib0ddb732020-03-25 16:49:02 +09003608 } catch (VolumeNotFoundException e) {
3609 throw e.rethrowAsIllegalArgumentException();
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06003610 } finally {
Hyoungho Choib0ddb732020-03-25 16:49:02 +09003611 for (DatabaseHelper helper : transactions) {
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06003612 helper.endTransaction();
Marco Nelissencb0c5a62009-12-08 13:44:19 -08003613 }
3614 }
3615 }
3616
Sahana Rao02fb8f42020-05-14 16:54:35 +01003617 private void appendWhereStandaloneMatch(@NonNull SQLiteQueryBuilder qb,
3618 @NonNull String column, /* @Match */ int match, Uri uri) {
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003619 switch (match) {
3620 case MATCH_INCLUDE:
3621 // No special filtering needed
3622 break;
3623 case MATCH_EXCLUDE:
Sahana Raob02e7152020-06-12 17:07:31 +01003624 appendWhereStandalone(qb, getWhereClauseForMatchExclude(column));
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003625 break;
3626 case MATCH_ONLY:
3627 appendWhereStandalone(qb, column + "=?", 1);
3628 break;
Sahana Raoea587fc2020-06-03 15:56:23 +01003629 case MATCH_VISIBLE_FOR_FILEPATH:
3630 final String whereClause =
3631 getWhereClauseForMatchableVisibleFromFilePath(uri, column);
Sahana Rao02fb8f42020-05-14 16:54:35 +01003632 if (whereClause != null) {
3633 appendWhereStandalone(qb, whereClause);
3634 }
3635 break;
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003636 default:
3637 throw new IllegalArgumentException();
3638 }
3639 }
3640
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06003641 private static void appendWhereStandalone(@NonNull SQLiteQueryBuilder qb,
3642 @Nullable String selection, @Nullable Object... selectionArgs) {
3643 qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs));
3644 }
3645
Jeff Sharkey9d297672020-01-15 13:11:54 -07003646 private static void appendWhereStandaloneFilter(@NonNull SQLiteQueryBuilder qb,
3647 @NonNull String[] columns, @Nullable String filter) {
3648 if (TextUtils.isEmpty(filter)) return;
3649 for (String filterWord : filter.split("\\s+")) {
3650 appendWhereStandalone(qb, String.join("||", columns) + " LIKE ? ESCAPE '\\'",
3651 "%" + DatabaseUtils.escapeForLike(Audio.keyFor(filterWord)) + "%");
3652 }
3653 }
3654
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06003655 @Deprecated
Sahana Rao0bbd3e12020-06-06 15:56:44 +01003656 private String getSharedPackages() {
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06003657 final String[] sharedPackageNames = mCallingIdentity.get().getSharedPackageNames();
3658 return bindList((Object[]) sharedPackageNames);
Gavin Corkery75251c42019-05-07 16:08:57 +01003659 }
3660
Sahana Rao25db6492020-06-06 16:22:23 +01003661 /**
3662 * Gets shared packages names for given {@code packageName}
3663 */
3664 private String[] getSharedPackagesForPackage(String packageName) {
3665 try {
3666 final int packageUid = getContext().getPackageManager()
3667 .getPackageUid(packageName, 0);
3668 return getContext().getPackageManager().getPackagesForUid(packageUid);
3669 } catch (NameNotFoundException ignored) {
3670 return new String[] {packageName};
3671 }
3672 }
3673
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003674 private static final int TYPE_QUERY = 0;
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003675 private static final int TYPE_INSERT = 1;
3676 private static final int TYPE_UPDATE = 2;
3677 private static final int TYPE_DELETE = 3;
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003678
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07003679 /**
3680 * Generate a {@link SQLiteQueryBuilder} that is filtered based on the
3681 * runtime permissions and/or {@link Uri} grants held by the caller.
3682 * <ul>
3683 * <li>If caller holds a {@link Uri} grant, access is allowed according to
3684 * that grant.
3685 * <li>If caller holds the write permission for a collection, they can
3686 * read/write all contents of that collection.
3687 * <li>If caller holds the read permission for a collection, they can read
3688 * all contents of that collection, but writes are limited to content they
3689 * own.
3690 * <li>If caller holds no permissions for a collection, all reads/write are
3691 * limited to content they own.
3692 * </ul>
3693 */
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003694 private @NonNull SQLiteQueryBuilder getQueryBuilder(int type, int match,
Jeff Sharkey61378cb2019-11-23 16:11:09 -07003695 @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06003696 Trace.beginSection("getQueryBuilder");
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06003697 try {
Jeff Sharkey61378cb2019-11-23 16:11:09 -07003698 return getQueryBuilderInternal(type, match, uri, extras, honored);
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06003699 } finally {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06003700 Trace.endSection();
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06003701 }
3702 }
3703
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003704 private @NonNull SQLiteQueryBuilder getQueryBuilderInternal(int type, int match,
Jeff Sharkey61378cb2019-11-23 16:11:09 -07003705 @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) {
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06003706 final boolean forWrite;
3707 switch (type) {
3708 case TYPE_QUERY: forWrite = false; break;
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003709 case TYPE_INSERT: forWrite = true; break;
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06003710 case TYPE_UPDATE: forWrite = true; break;
3711 case TYPE_DELETE: forWrite = true; break;
3712 default: throw new IllegalStateException();
3713 }
3714
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003715 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
Jeff Sharkeya9473e92020-04-17 15:54:30 -06003716 if (uri.getBooleanQueryParameter("distinct", false)) {
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003717 qb.setDistinct(true);
3718 }
Jeff Sharkey3a1265b2018-08-06 11:36:08 -06003719 qb.setStrict(true);
Jeff Sharkey8411c402020-04-29 22:12:36 -06003720 if (isCallingPackageSelf()) {
Jeff Sharkeye3650642020-04-03 18:50:03 -06003721 // When caller is system, such as the media scanner, we're willing
3722 // to let them access any columns they want
3723 } else {
Jeff Sharkey8e80fca2020-05-06 13:41:15 -06003724 qb.setTargetSdkVersion(getCallingPackageTargetSdkVersion());
Jeff Sharkeyd1961bd2020-04-10 14:20:03 -06003725 qb.setStrictColumns(true);
3726 qb.setStrictGrammar(true);
Jeff Sharkey910ba4a2020-01-08 10:57:55 -07003727 }
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003728
Jeff Sharkey71437302019-04-09 23:46:52 -06003729 // TODO: throw when requesting a currently unmounted volume
3730 final String volumeName = MediaStore.getVolumeName(uri);
3731 final String includeVolumes;
3732 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) {
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06003733 includeVolumes = bindList(getExternalVolumeNames().toArray());
Jeff Sharkey71437302019-04-09 23:46:52 -06003734 } else {
3735 includeVolumes = bindList(volumeName);
3736 }
Sahana Rao0bbd3e12020-06-06 15:56:44 +01003737 final String sharedPackages = getSharedPackages();
Sahana Rao3279b462020-04-23 00:29:39 +01003738 final String matchSharedPackagesClause = FileColumns.OWNER_PACKAGE_NAME + " IN "
3739 + sharedPackages;
3740
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07003741 final boolean allowGlobal = checkCallingPermissionGlobal(uri, forWrite);
shafike87a1d22020-02-27 15:52:09 +00003742 final boolean allowLegacy =
3743 forWrite ? isCallingPackageLegacyWrite() : isCallingPackageLegacyRead();
Jeff Sharkey48166072019-04-18 14:38:47 -06003744 final boolean allowLegacyRead = allowLegacy && !forWrite;
Jeff Sharkey0bf693f2018-10-27 19:47:17 -06003745
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003746 int matchPending = extras.getInt(QUERY_ARG_MATCH_PENDING, MATCH_DEFAULT);
3747 int matchTrashed = extras.getInt(QUERY_ARG_MATCH_TRASHED, MATCH_DEFAULT);
3748 int matchFavorite = extras.getInt(QUERY_ARG_MATCH_FAVORITE, MATCH_DEFAULT);
3749
shafikac34fe92020-02-25 15:28:55 +00003750 final ArrayList<String> includedDefaultDirs = extras.getStringArrayList(
3751 INCLUDED_DEFAULT_DIRECTORIES);
3752
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003753 // Handle callers using legacy arguments
3754 if (MediaStore.getIncludePending(uri)) matchPending = MATCH_INCLUDE;
3755
3756 // Resolve any remaining default options
Sahana Rao02fb8f42020-05-14 16:54:35 +01003757 final int defaultMatchForPendingAndTrashed;
3758 if (isFuseThread()) {
3759 // Write operations always check for file ownership, we don't need additional write
3760 // permission check for is_pending and is_trashed.
Sahana Raoea587fc2020-06-03 15:56:23 +01003761 defaultMatchForPendingAndTrashed =
3762 forWrite ? MATCH_INCLUDE : MATCH_VISIBLE_FOR_FILEPATH;
Sahana Rao02fb8f42020-05-14 16:54:35 +01003763 } else {
3764 defaultMatchForPendingAndTrashed = MATCH_EXCLUDE;
3765 }
3766 if (matchPending == MATCH_DEFAULT) matchPending = defaultMatchForPendingAndTrashed;
3767 if (matchTrashed == MATCH_DEFAULT) matchTrashed = defaultMatchForPendingAndTrashed;
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003768 if (matchFavorite == MATCH_DEFAULT) matchFavorite = MATCH_INCLUDE;
3769
Jeff Sharkey9d297672020-01-15 13:11:54 -07003770 // Handle callers using legacy filtering
3771 final String filter = uri.getQueryParameter("filter");
3772
Jeff Sharkey71437302019-04-09 23:46:52 -06003773 boolean includeAllVolumes = false;
Sahana Rao0bbd3e12020-06-06 15:56:44 +01003774 final String callingPackage = getCallingPackageOrSelf();
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06003775
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003776 switch (match) {
The Android Open Source Project70215272009-03-03 19:32:43 -08003777 case IMAGES_MEDIA_ID:
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06003778 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003779 matchPending = MATCH_INCLUDE;
3780 matchTrashed = MATCH_INCLUDE;
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06003781 // fall-through
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003782 case IMAGES_MEDIA: {
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003783 if (type == TYPE_QUERY) {
3784 qb.setTables("images");
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003785 qb.setProjectionMap(
3786 getProjectionMap(Images.Media.class));
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003787 } else {
3788 qb.setTables("files");
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003789 qb.setProjectionMap(
3790 getProjectionMap(Images.Media.class, Files.FileColumns.class));
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003791 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
3792 FileColumns.MEDIA_TYPE_IMAGE);
3793 }
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07003794 if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) {
Sahana Rao3279b462020-04-23 00:29:39 +01003795 appendWhereStandalone(qb, matchSharedPackagesClause);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06003796 }
Sahana Rao02fb8f42020-05-14 16:54:35 +01003797 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
3798 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
3799 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07003800 if (honored != null) {
3801 honored.accept(QUERY_ARG_MATCH_PENDING);
3802 honored.accept(QUERY_ARG_MATCH_TRASHED);
3803 honored.accept(QUERY_ARG_MATCH_FAVORITE);
3804 }
Jeff Sharkey71437302019-04-09 23:46:52 -06003805 if (!includeAllVolumes) {
3806 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
3807 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003808 break;
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003809 }
Ray Chenb3861122009-09-07 23:39:01 -07003810 case IMAGES_THUMBNAILS_ID:
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06003811 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003812 // fall-through
Jeff Sharkeya57867a2019-02-14 13:27:35 -07003813 case IMAGES_THUMBNAILS: {
Jeff Sharkey556d2d92018-07-12 19:51:27 -06003814 qb.setTables("thumbnails");
Jeff Sharkeya57867a2019-02-14 13:27:35 -07003815
3816 final ArrayMap<String, String> projectionMap = new ArrayMap<>(
3817 getProjectionMap(Images.Thumbnails.class));
3818 projectionMap.put(Images.Thumbnails.THUMB_DATA,
3819 "NULL AS " + Images.Thumbnails.THUMB_DATA);
3820 qb.setProjectionMap(projectionMap);
3821
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07003822 if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) {
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06003823 appendWhereStandalone(qb,
Sahana Rao3279b462020-04-23 00:29:39 +01003824 "image_id IN (SELECT _id FROM images WHERE "
3825 + matchSharedPackagesClause + ")");
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06003826 }
Ray Chenb3861122009-09-07 23:39:01 -07003827 break;
Jeff Sharkeya57867a2019-02-14 13:27:35 -07003828 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003829 case AUDIO_MEDIA_ID:
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06003830 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003831 matchPending = MATCH_INCLUDE;
3832 matchTrashed = MATCH_INCLUDE;
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003833 // fall-through
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003834 case AUDIO_MEDIA: {
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003835 if (type == TYPE_QUERY) {
3836 qb.setTables("audio");
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003837 qb.setProjectionMap(
3838 getProjectionMap(Audio.Media.class));
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003839 } else {
3840 qb.setTables("files");
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003841 qb.setProjectionMap(
3842 getProjectionMap(Audio.Media.class, Files.FileColumns.class));
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003843 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
3844 FileColumns.MEDIA_TYPE_AUDIO);
3845 }
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07003846 if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) {
Jeff Sharkeyec2508f2019-02-20 18:13:54 -07003847 // Apps without Audio permission can only see their own
3848 // media, but we also let them see ringtone-style media to
3849 // support legacy use-cases.
3850 appendWhereStandalone(qb,
Sahana Rao3279b462020-04-23 00:29:39 +01003851 DatabaseUtils.bindSelection(matchSharedPackagesClause
Gavin Corkery75251c42019-05-07 16:08:57 +01003852 + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1"));
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06003853 }
Jeff Sharkey9d297672020-01-15 13:11:54 -07003854 appendWhereStandaloneFilter(qb, new String[] {
3855 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
3856 }, filter);
Sahana Rao02fb8f42020-05-14 16:54:35 +01003857 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
3858 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
3859 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07003860 if (honored != null) {
3861 honored.accept(QUERY_ARG_MATCH_PENDING);
3862 honored.accept(QUERY_ARG_MATCH_TRASHED);
3863 honored.accept(QUERY_ARG_MATCH_FAVORITE);
3864 }
Jeff Sharkey71437302019-04-09 23:46:52 -06003865 if (!includeAllVolumes) {
3866 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
3867 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003868 break;
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003869 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003870 case AUDIO_MEDIA_ID_GENRES_ID:
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003871 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5));
3872 // fall-through
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003873 case AUDIO_MEDIA_ID_GENRES: {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003874 if (type == TYPE_QUERY) {
3875 qb.setTables("audio_genres");
3876 qb.setProjectionMap(getProjectionMap(Audio.Genres.class));
3877 } else {
3878 throw new UnsupportedOperationException("Genres cannot be directly modified");
3879 }
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003880 appendWhereStandalone(qb, "_id IN (SELECT genre_id FROM " +
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003881 "audio WHERE _id=?)", uri.getPathSegments().get(3));
Jeff Sharkey1f6253a2019-02-15 17:38:56 -07003882 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3883 // We don't have a great way to filter parsed metadata by
3884 // owner, so callers need to hold READ_MEDIA_AUDIO
3885 appendWhereStandalone(qb, "0");
3886 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003887 break;
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003888 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003889 case AUDIO_GENRES_ID:
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06003890 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003891 // fall-through
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003892 case AUDIO_GENRES: {
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003893 qb.setTables("audio_genres");
Jeff Sharkeya57867a2019-02-14 13:27:35 -07003894 qb.setProjectionMap(getProjectionMap(Audio.Genres.class));
Jeff Sharkey1f6253a2019-02-15 17:38:56 -07003895 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3896 // We don't have a great way to filter parsed metadata by
3897 // owner, so callers need to hold READ_MEDIA_AUDIO
3898 appendWhereStandalone(qb, "0");
3899 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003900 break;
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003901 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003902 case AUDIO_GENRES_ID_MEMBERS:
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06003903 appendWhereStandalone(qb, "genre_id=?", uri.getPathSegments().get(3));
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06003904 // fall-through
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003905 case AUDIO_GENRES_ALL_MEMBERS: {
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06003906 if (type == TYPE_QUERY) {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003907 qb.setTables("audio");
3908
3909 final ArrayMap<String, String> projectionMap = new ArrayMap<>(
3910 getProjectionMap(Audio.Genres.Members.class));
3911 projectionMap.put(Audio.Genres.Members.AUDIO_ID,
3912 "_id AS " + Audio.Genres.Members.AUDIO_ID);
3913 qb.setProjectionMap(projectionMap);
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06003914 } else {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06003915 throw new UnsupportedOperationException("Genres cannot be directly modified");
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06003916 }
Jeff Sharkey9d297672020-01-15 13:11:54 -07003917 appendWhereStandaloneFilter(qb, new String[] {
3918 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
3919 }, filter);
Jeff Sharkey1f6253a2019-02-15 17:38:56 -07003920 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3921 // We don't have a great way to filter parsed metadata by
3922 // owner, so callers need to hold READ_MEDIA_AUDIO
3923 appendWhereStandalone(qb, "0");
3924 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003925 break;
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003926 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003927 case AUDIO_PLAYLISTS_ID:
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06003928 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07003929 matchPending = MATCH_INCLUDE;
3930 matchTrashed = MATCH_INCLUDE;
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003931 // fall-through
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003932 case AUDIO_PLAYLISTS: {
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003933 if (type == TYPE_QUERY) {
3934 qb.setTables("audio_playlists");
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003935 qb.setProjectionMap(
3936 getProjectionMap(Audio.Playlists.class));
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003937 } else {
3938 qb.setTables("files");
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003939 qb.setProjectionMap(
3940 getProjectionMap(Audio.Playlists.class, Files.FileColumns.class));
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06003941 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
3942 FileColumns.MEDIA_TYPE_PLAYLIST);
3943 }
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07003944 if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) {
Sahana Rao3279b462020-04-23 00:29:39 +01003945 appendWhereStandalone(qb, matchSharedPackagesClause);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06003946 }
Sahana Rao02fb8f42020-05-14 16:54:35 +01003947 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
3948 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
3949 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07003950 if (honored != null) {
3951 honored.accept(QUERY_ARG_MATCH_PENDING);
3952 honored.accept(QUERY_ARG_MATCH_TRASHED);
3953 honored.accept(QUERY_ARG_MATCH_FAVORITE);
3954 }
Jeff Sharkey71437302019-04-09 23:46:52 -06003955 if (!includeAllVolumes) {
3956 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
3957 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003958 break;
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003959 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003960 case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06003961 appendWhereStandalone(qb, "audio_playlists_map._id=?",
Jeff Sharkey556d2d92018-07-12 19:51:27 -06003962 uri.getPathSegments().get(5));
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06003963 // fall-through
Jeff Sharkeya57867a2019-02-14 13:27:35 -07003964 case AUDIO_PLAYLISTS_ID_MEMBERS: {
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06003965 appendWhereStandalone(qb, "playlist_id=?", uri.getPathSegments().get(3));
3966 if (type == TYPE_QUERY) {
3967 qb.setTables("audio_playlists_map, audio");
Jeff Sharkeya57867a2019-02-14 13:27:35 -07003968
3969 final ArrayMap<String, String> projectionMap = new ArrayMap<>(
3970 getProjectionMap(Audio.Playlists.Members.class));
3971 projectionMap.put(Audio.Playlists.Members._ID,
3972 "audio_playlists_map._id AS " + Audio.Playlists.Members._ID);
3973 qb.setProjectionMap(projectionMap);
3974
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06003975 appendWhereStandalone(qb, "audio._id = audio_id");
3976 } else {
3977 qb.setTables("audio_playlists_map");
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07003978 qb.setProjectionMap(getProjectionMap(Audio.Playlists.Members.class));
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06003979 }
Jeff Sharkey9d297672020-01-15 13:11:54 -07003980 appendWhereStandaloneFilter(qb, new String[] {
3981 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
3982 }, filter);
Jeff Sharkey1f6253a2019-02-15 17:38:56 -07003983 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
3984 // We don't have a great way to filter parsed metadata by
3985 // owner, so callers need to hold READ_MEDIA_AUDIO
3986 appendWhereStandalone(qb, "0");
3987 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003988 break;
Jeff Sharkeya57867a2019-02-14 13:27:35 -07003989 }
The Android Open Source Project70215272009-03-03 19:32:43 -08003990 case AUDIO_ALBUMART_ID:
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06003991 appendWhereStandalone(qb, "album_id=?", uri.getPathSegments().get(3));
Jeff Sharkey9b7078d2019-03-22 14:30:28 -06003992 // fall-through
3993 case AUDIO_ALBUMART: {
Jeff Sharkey71437302019-04-09 23:46:52 -06003994 qb.setTables("album_art");
Jeff Sharkey9b7078d2019-03-22 14:30:28 -06003995
3996 final ArrayMap<String, String> projectionMap = new ArrayMap<>(
3997 getProjectionMap(Audio.Thumbnails.class));
3998 projectionMap.put(Audio.Thumbnails._ID,
3999 "album_id AS " + Audio.Thumbnails._ID);
4000 qb.setProjectionMap(projectionMap);
4001
Jeff Sharkey1f6253a2019-02-15 17:38:56 -07004002 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
4003 // We don't have a great way to filter parsed metadata by
4004 // owner, so callers need to hold READ_MEDIA_AUDIO
4005 appendWhereStandalone(qb, "0");
4006 }
The Android Open Source Project70215272009-03-03 19:32:43 -08004007 break;
Jeff Sharkey9b7078d2019-03-22 14:30:28 -06004008 }
Jeff Sharkeya57867a2019-02-14 13:27:35 -07004009 case AUDIO_ARTISTS_ID_ALBUMS: {
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004010 if (type == TYPE_QUERY) {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06004011 qb.setTables("audio_albums");
4012 qb.setProjectionMap(getProjectionMap(Audio.Artists.Albums.class));
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004013
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06004014 final String artistId = uri.getPathSegments().get(3);
4015 appendWhereStandalone(qb, "artist_id=?", artistId);
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004016 } else {
4017 throw new UnsupportedOperationException("Albums cannot be directly modified");
4018 }
Jeff Sharkey9d297672020-01-15 13:11:54 -07004019 appendWhereStandaloneFilter(qb, new String[] {
4020 AudioColumns.ALBUM_KEY
4021 }, filter);
Jeff Sharkey1f6253a2019-02-15 17:38:56 -07004022 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
4023 // We don't have a great way to filter parsed metadata by
4024 // owner, so callers need to hold READ_MEDIA_AUDIO
4025 appendWhereStandalone(qb, "0");
4026 }
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004027 break;
Jeff Sharkeya57867a2019-02-14 13:27:35 -07004028 }
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004029 case AUDIO_ARTISTS_ID:
4030 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
4031 // fall-through
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004032 case AUDIO_ARTISTS: {
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004033 if (type == TYPE_QUERY) {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06004034 qb.setTables("audio_artists");
Jeff Sharkeya57867a2019-02-14 13:27:35 -07004035 qb.setProjectionMap(getProjectionMap(Audio.Artists.class));
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004036 } else {
4037 throw new UnsupportedOperationException("Artists cannot be directly modified");
4038 }
Jeff Sharkey9d297672020-01-15 13:11:54 -07004039 appendWhereStandaloneFilter(qb, new String[] {
4040 AudioColumns.ARTIST_KEY
4041 }, filter);
Jeff Sharkey1f6253a2019-02-15 17:38:56 -07004042 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
4043 // We don't have a great way to filter parsed metadata by
4044 // owner, so callers need to hold READ_MEDIA_AUDIO
4045 appendWhereStandalone(qb, "0");
4046 }
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004047 break;
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004048 }
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004049 case AUDIO_ALBUMS_ID:
4050 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
4051 // fall-through
Jeff Sharkeya57867a2019-02-14 13:27:35 -07004052 case AUDIO_ALBUMS: {
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004053 if (type == TYPE_QUERY) {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06004054 qb.setTables("audio_albums");
4055 qb.setProjectionMap(getProjectionMap(Audio.Albums.class));
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004056 } else {
4057 throw new UnsupportedOperationException("Albums cannot be directly modified");
4058 }
Jeff Sharkey9d297672020-01-15 13:11:54 -07004059 appendWhereStandaloneFilter(qb, new String[] {
4060 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY
4061 }, filter);
Jeff Sharkey1f6253a2019-02-15 17:38:56 -07004062 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) {
4063 // We don't have a great way to filter parsed metadata by
4064 // owner, so callers need to hold READ_MEDIA_AUDIO
4065 appendWhereStandalone(qb, "0");
4066 }
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06004067 break;
Jeff Sharkeya57867a2019-02-14 13:27:35 -07004068 }
The Android Open Source Project70215272009-03-03 19:32:43 -08004069 case VIDEO_MEDIA_ID:
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06004070 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004071 matchPending = MATCH_INCLUDE;
4072 matchTrashed = MATCH_INCLUDE;
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06004073 // fall-through
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004074 case VIDEO_MEDIA: {
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06004075 if (type == TYPE_QUERY) {
4076 qb.setTables("video");
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004077 qb.setProjectionMap(
4078 getProjectionMap(Video.Media.class));
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06004079 } else {
4080 qb.setTables("files");
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004081 qb.setProjectionMap(
4082 getProjectionMap(Video.Media.class, Files.FileColumns.class));
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06004083 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?",
4084 FileColumns.MEDIA_TYPE_VIDEO);
4085 }
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07004086 if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) {
Sahana Rao3279b462020-04-23 00:29:39 +01004087 appendWhereStandalone(qb, matchSharedPackagesClause);
Jeff Sharkey2fd9da72018-11-02 23:52:06 -06004088 }
Sahana Rao02fb8f42020-05-14 16:54:35 +01004089 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
4090 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
4091 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07004092 if (honored != null) {
4093 honored.accept(QUERY_ARG_MATCH_PENDING);
4094 honored.accept(QUERY_ARG_MATCH_TRASHED);
4095 honored.accept(QUERY_ARG_MATCH_FAVORITE);
4096 }
Jeff Sharkey71437302019-04-09 23:46:52 -06004097 if (!includeAllVolumes) {
4098 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
4099 }
The Android Open Source Project70215272009-03-03 19:32:43 -08004100 break;
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004101 }
Ray Chenb3861122009-09-07 23:39:01 -07004102 case VIDEO_THUMBNAILS_ID:
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06004103 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3));
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06004104 // fall-through
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004105 case VIDEO_THUMBNAILS: {
Jeff Sharkey556d2d92018-07-12 19:51:27 -06004106 qb.setTables("videothumbnails");
Jeff Sharkeya57867a2019-02-14 13:27:35 -07004107 qb.setProjectionMap(getProjectionMap(Video.Thumbnails.class));
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07004108 if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) {
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06004109 appendWhereStandalone(qb,
Sahana Rao3279b462020-04-23 00:29:39 +01004110 "video_id IN (SELECT _id FROM video WHERE " +
4111 matchSharedPackagesClause + ")");
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06004112 }
Ray Chenb3861122009-09-07 23:39:01 -07004113 break;
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004114 }
Mike Lockwood16dc0fd2010-09-08 12:52:17 -04004115 case FILES_ID:
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06004116 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2));
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004117 matchPending = MATCH_INCLUDE;
4118 matchTrashed = MATCH_INCLUDE;
Jeff Sharkeyea2c9672018-07-26 18:32:44 -06004119 // fall-through
Jeff Sharkeya7a789a2019-12-17 13:45:57 -07004120 case FILES: {
Jeff Sharkey556d2d92018-07-12 19:51:27 -06004121 qb.setTables("files");
Jeff Sharkeya57867a2019-02-14 13:27:35 -07004122 qb.setProjectionMap(getProjectionMap(Files.FileColumns.class));
Jeff Sharkey0bf693f2018-10-27 19:47:17 -06004123
4124 final ArrayList<String> options = new ArrayList<>();
Jeff Sharkey48166072019-04-18 14:38:47 -06004125 if (!allowGlobal && !allowLegacyRead) {
Sahana Rao3279b462020-04-23 00:29:39 +01004126 options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause));
Jeff Sharkey48166072019-04-18 14:38:47 -06004127 if (allowLegacy) {
4128 options.add(DatabaseUtils.bindSelection("volume_name=?",
4129 MediaStore.VOLUME_EXTERNAL_PRIMARY));
4130 }
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07004131 if (checkCallingPermissionAudio(forWrite, callingPackage)) {
4132 options.add(DatabaseUtils.bindSelection("media_type=?",
4133 FileColumns.MEDIA_TYPE_AUDIO));
Jeff Sharkey1f6253a2019-02-15 17:38:56 -07004134 options.add(DatabaseUtils.bindSelection("media_type=?",
4135 FileColumns.MEDIA_TYPE_PLAYLIST));
Jeff Sharkey21e297e2019-12-06 18:14:32 -07004136 options.add(DatabaseUtils.bindSelection("media_type=?",
4137 FileColumns.MEDIA_TYPE_SUBTITLE));
Sahana Rao3279b462020-04-23 00:29:39 +01004138 options.add(matchSharedPackagesClause
4139 + " AND media_type=0 AND mime_type LIKE 'audio/%'");
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07004140 }
4141 if (checkCallingPermissionVideo(forWrite, callingPackage)) {
4142 options.add(DatabaseUtils.bindSelection("media_type=?",
4143 FileColumns.MEDIA_TYPE_VIDEO));
Jeff Sharkey21e297e2019-12-06 18:14:32 -07004144 options.add(DatabaseUtils.bindSelection("media_type=?",
4145 FileColumns.MEDIA_TYPE_SUBTITLE));
Sahana Rao3279b462020-04-23 00:29:39 +01004146 options.add(matchSharedPackagesClause
4147 + " AND media_type=0 AND mime_type LIKE 'video/%'");
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07004148 }
4149 if (checkCallingPermissionImages(forWrite, callingPackage)) {
4150 options.add(DatabaseUtils.bindSelection("media_type=?",
4151 FileColumns.MEDIA_TYPE_IMAGE));
Sahana Rao3279b462020-04-23 00:29:39 +01004152 options.add(matchSharedPackagesClause
4153 + " AND media_type=0 AND mime_type LIKE 'image/%'");
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07004154 }
shafikac34fe92020-02-25 15:28:55 +00004155 if (includedDefaultDirs != null) {
4156 for (String defaultDir : includedDefaultDirs) {
4157 options.add(FileColumns.RELATIVE_PATH + " LIKE '" + defaultDir + "/%'");
4158 }
4159 }
Jeff Sharkey0bf693f2018-10-27 19:47:17 -06004160 }
4161 if (options.size() > 0) {
4162 appendWhereStandalone(qb, TextUtils.join(" OR ", options));
4163 }
4164
Jeff Sharkey9d297672020-01-15 13:11:54 -07004165 appendWhereStandaloneFilter(qb, new String[] {
4166 AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY
4167 }, filter);
Sahana Rao02fb8f42020-05-14 16:54:35 +01004168 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
4169 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
4170 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07004171 if (honored != null) {
4172 honored.accept(QUERY_ARG_MATCH_PENDING);
4173 honored.accept(QUERY_ARG_MATCH_TRASHED);
4174 honored.accept(QUERY_ARG_MATCH_FAVORITE);
4175 }
Jeff Sharkey71437302019-04-09 23:46:52 -06004176 if (!includeAllVolumes) {
4177 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
4178 }
Mike Lockwood17179552010-07-09 10:46:58 -04004179 break;
Jeff Sharkey48166072019-04-18 14:38:47 -06004180 }
Sudheer Shankaaa626512018-11-15 20:29:28 -08004181 case DOWNLOADS_ID:
4182 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2));
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004183 matchPending = MATCH_INCLUDE;
4184 matchTrashed = MATCH_INCLUDE;
Sudheer Shankaaa626512018-11-15 20:29:28 -08004185 // fall-through
Jeff Sharkey48166072019-04-18 14:38:47 -06004186 case DOWNLOADS: {
Sudheer Shankaaa626512018-11-15 20:29:28 -08004187 if (type == TYPE_QUERY) {
4188 qb.setTables("downloads");
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004189 qb.setProjectionMap(
4190 getProjectionMap(Downloads.class));
Sudheer Shankaaa626512018-11-15 20:29:28 -08004191 } else {
4192 qb.setTables("files");
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004193 qb.setProjectionMap(
4194 getProjectionMap(Downloads.class, Files.FileColumns.class));
Sudheer Shankaaa626512018-11-15 20:29:28 -08004195 appendWhereStandalone(qb, FileColumns.IS_DOWNLOAD + "=1");
4196 }
Jeff Sharkey48166072019-04-18 14:38:47 -06004197
4198 final ArrayList<String> options = new ArrayList<>();
4199 if (!allowGlobal && !allowLegacyRead) {
Sahana Rao3279b462020-04-23 00:29:39 +01004200 options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause));
Jeff Sharkey48166072019-04-18 14:38:47 -06004201 if (allowLegacy) {
4202 options.add(DatabaseUtils.bindSelection("volume_name=?",
4203 MediaStore.VOLUME_EXTERNAL_PRIMARY));
4204 }
Sudheer Shankaaa626512018-11-15 20:29:28 -08004205 }
Jeff Sharkey48166072019-04-18 14:38:47 -06004206 if (options.size() > 0) {
4207 appendWhereStandalone(qb, TextUtils.join(" OR ", options));
4208 }
4209
Sahana Rao02fb8f42020-05-14 16:54:35 +01004210 appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri);
4211 appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri);
4212 appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07004213 if (honored != null) {
4214 honored.accept(QUERY_ARG_MATCH_PENDING);
4215 honored.accept(QUERY_ARG_MATCH_TRASHED);
4216 honored.accept(QUERY_ARG_MATCH_FAVORITE);
4217 }
Jeff Sharkey71437302019-04-09 23:46:52 -06004218 if (!includeAllVolumes) {
4219 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes);
4220 }
Sudheer Shankaaa626512018-11-15 20:29:28 -08004221 break;
Jeff Sharkey48166072019-04-18 14:38:47 -06004222 }
The Android Open Source Project70215272009-03-03 19:32:43 -08004223 default:
4224 throw new UnsupportedOperationException(
4225 "Unknown or unsupported URL: " + uri.toString());
4226 }
Jeff Sharkeya57867a2019-02-14 13:27:35 -07004227
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004228 // To ensure we're enforcing our security model, all operations must
4229 // have a projection map configured
4230 if (qb.getProjectionMap() == null) {
4231 throw new IllegalStateException("All queries must have a projection map");
4232 }
Jeff Sharkeya57867a2019-02-14 13:27:35 -07004233
Jeff Sharkeycbdea0f2019-12-16 16:10:18 -07004234 // If caller is an older app, we're willing to let through a
4235 // greylist of technically invalid columns
4236 if (getCallingPackageTargetSdkVersion() < Build.VERSION_CODES.Q) {
4237 qb.setProjectionGreylist(sGreylist);
4238 }
Jeff Sharkey74f73732019-11-12 15:36:32 -07004239
Jeff Sharkey556d2d92018-07-12 19:51:27 -06004240 return qb;
The Android Open Source Project70215272009-03-03 19:32:43 -08004241 }
4242
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07004243 /**
4244 * Determine if given {@link Uri} has a
4245 * {@link MediaColumns#OWNER_PACKAGE_NAME} column.
4246 */
Jeff Sharkey74f73732019-11-12 15:36:32 -07004247 private boolean hasOwnerPackageName(Uri uri) {
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07004248 // It's easier to maintain this as an inverted list
4249 final int table = matchUri(uri, true);
4250 switch (table) {
4251 case IMAGES_THUMBNAILS_ID:
4252 case IMAGES_THUMBNAILS:
4253 case VIDEO_THUMBNAILS_ID:
4254 case VIDEO_THUMBNAILS:
4255 case AUDIO_ALBUMART:
4256 case AUDIO_ALBUMART_ID:
4257 case AUDIO_ALBUMART_FILE_ID:
4258 return false;
4259 default:
4260 return true;
4261 }
4262 }
4263
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004264 /**
4265 * @deprecated all operations should be routed through the overload that
4266 * accepts a {@link Bundle} of extras.
4267 */
The Android Open Source Project70215272009-03-03 19:32:43 -08004268 @Override
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004269 @Deprecated
4270 public int delete(Uri uri, String selection, String[] selectionArgs) {
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -07004271 return delete(uri,
4272 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null));
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004273 }
4274
4275 @Override
4276 public int delete(@NonNull Uri uri, @Nullable Bundle extras) {
Jeff Sharkey22988642020-03-05 17:09:39 -07004277 Trace.beginSection("delete");
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06004278 try {
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004279 return deleteInternal(uri, extras);
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06004280 } catch (FallbackException e) {
4281 return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion());
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06004282 } finally {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06004283 Trace.endSection();
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06004284 }
4285 }
4286
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004287 private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras)
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06004288 throws FallbackException {
Jeff Sharkeyd6697822020-03-22 20:59:47 -06004289 extras = (extras != null) ? extras : new Bundle();
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004290
Nikita Ioffe710787b2020-06-11 14:35:14 +01004291 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
4292 extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
4293
Sahana Rao8affdb02020-06-08 18:28:57 +01004294 uri = safeUncanonicalize(uri);
4295 final boolean allowHidden = isCallingPackageAllowedHidden();
4296 final int match = matchUri(uri, allowHidden);
4297
4298 switch(match) {
4299 case AUDIO_MEDIA_ID:
4300 case AUDIO_PLAYLISTS_ID:
4301 case VIDEO_MEDIA_ID:
4302 case IMAGES_MEDIA_ID:
4303 case DOWNLOADS_ID:
4304 case FILES_ID: {
4305 if (!isFuseThread() && getCachedCallingIdentityForFuse(Binder.getCallingUid()).
4306 removeDeletedRowId(Long.parseLong(uri.getLastPathSegment()))) {
4307 // Apps sometimes delete the file via filePath and then try to delete the db row
4308 // using MediaProvider#delete. Since we would have already deleted the db row
4309 // during the filePath operation, the latter will result in a security
4310 // exception. Apps which don't expect an exception will break here. Since we
4311 // have already deleted the db row, silently return zero as deleted count.
4312 return 0;
4313 }
4314 }
4315 break;
4316 default:
4317 // For other match types, given uri will not correspond to a valid file.
4318 break;
4319 }
4320
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004321 final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION);
4322 final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);
4323
Jeff Sharkeyd6697822020-03-22 20:59:47 -06004324 int count = 0;
Jeff Sharkey94461582018-07-12 14:34:47 -06004325
Jeff Sharkey199f8c82019-03-23 11:54:21 -06004326 final String volumeName = getVolumeName(uri);
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07004327 final int targetSdkVersion = getCallingPackageTargetSdkVersion();
The Android Open Source Project70215272009-03-03 19:32:43 -08004328
4329 // handle MEDIA_SCANNER before calling getDatabaseForUri()
4330 if (match == MEDIA_SCANNER) {
4331 if (mMediaScannerVolume == null) {
4332 return 0;
4333 }
Jeff Sharkey5ed33602019-01-23 14:31:30 -07004334
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06004335 final DatabaseHelper helper = getDatabaseForUri(
4336 MediaStore.Files.getContentUri(mMediaScannerVolume));
Jeff Sharkey5ed33602019-01-23 14:31:30 -07004337
Jeff Sharkey7ea24f22019-08-22 10:14:18 -06004338 helper.mScanStopTime = SystemClock.elapsedRealtime();
Jeff Sharkey5ed33602019-01-23 14:31:30 -07004339
The Android Open Source Project70215272009-03-03 19:32:43 -08004340 mMediaScannerVolume = null;
4341 return 1;
4342 }
4343
Mike Lockwood819cafd2011-01-21 17:05:41 -08004344 if (match == VOLUMES_ID) {
4345 detachVolume(uri);
4346 count = 1;
Jeff Sharkey199f8c82019-03-23 11:54:21 -06004347 }
Jeff Sharkeyb39b32d2013-09-26 13:49:07 -07004348
Jeff Sharkeyd6697822020-03-22 20:59:47 -06004349 switch (match) {
4350 case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
4351 extras.putString(QUERY_ARG_SQL_SELECTION,
4352 BaseColumns._ID + "=" + uri.getPathSegments().get(5));
4353 // fall-through
4354 case AUDIO_PLAYLISTS_ID_MEMBERS: {
4355 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
4356 final Uri playlistUri = ContentUris.withAppendedId(
4357 MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);
4358
4359 // Playlist contents are always persisted directly into playlist
4360 // files on disk to ensure that we can reliably migrate between
4361 // devices and recover from database corruption
4362 return removePlaylistMembers(playlistUri, extras);
4363 }
4364 }
4365
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06004366 final DatabaseHelper helper = getDatabaseForUri(uri);
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07004367 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null);
Jeff Sharkey5ed33602019-01-23 14:31:30 -07004368
Jeff Sharkey199f8c82019-03-23 11:54:21 -06004369 {
Jeff Sharkeyb6485ce2019-04-15 14:20:47 -06004370 // Give callers interacting with a specific media item a chance to
4371 // escalate access if they don't already have it
4372 switch (match) {
4373 case AUDIO_MEDIA_ID:
4374 case VIDEO_MEDIA_ID:
4375 case IMAGES_MEDIA_ID:
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004376 enforceCallingPermission(uri, extras, true);
Jeff Sharkeyb6485ce2019-04-15 14:20:47 -06004377 }
4378
Sudheer Shanka1a79ac02019-01-17 13:14:52 -08004379 final String[] projection = new String[] {
4380 FileColumns.MEDIA_TYPE,
4381 FileColumns.DATA,
4382 FileColumns._ID,
4383 FileColumns.IS_DOWNLOAD,
4384 FileColumns.MIME_TYPE,
Sudheer Shanka1a79ac02019-01-17 13:14:52 -08004385 };
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07004386 final boolean isFilesTable = qb.getTables().equals("files");
Sudheer Shanka1a79ac02019-01-17 13:14:52 -08004387 final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>();
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07004388 if (isFilesTable) {
Marco Nelissenbae40612017-06-22 12:53:21 -07004389 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA);
4390 if (deleteparam == null || ! deleteparam.equals("false")) {
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07004391 Cursor c = qb.query(helper, projection, userWhere, userWhereArgs,
4392 null, null, null, null, null);
Marco Nelissenbae40612017-06-22 12:53:21 -07004393 try {
4394 while (c.moveToNext()) {
4395 final int mediaType = c.getInt(0);
4396 final String data = c.getString(1);
4397 final long id = c.getLong(2);
Sudheer Shanka56cba322018-12-07 10:55:58 -08004398 final int isDownload = c.getInt(3);
Sudheer Shanka1a79ac02019-01-17 13:14:52 -08004399 final String mimeType = c.getString(4);
Jeff Sharkeyb39b32d2013-09-26 13:49:07 -07004400
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06004401 // Forget that caller is owner of this item
4402 mCallingIdentity.get().setOwned(id, false);
4403
shafikf4290c02019-12-06 10:53:26 +00004404 deleteIfAllowed(uri, extras, data);
Jeff Sharkeyd6697822020-03-22 20:59:47 -06004405 count += qb.delete(helper, BaseColumns._ID + "=" + id, null);
shafikf4290c02019-12-06 10:53:26 +00004406
Sudheer Shanka1a79ac02019-01-17 13:14:52 -08004407 // Only need to inform DownloadProvider about the downloads deleted on
4408 // external volume.
Jeff Sharkey71437302019-04-09 23:46:52 -06004409 if (isDownload == 1) {
Sudheer Shanka2b79a5c2019-04-12 10:04:20 -07004410 deletedDownloadIds.put(id, mimeType);
Sudheer Shanka1a79ac02019-01-17 13:14:52 -08004411 }
Jeff Sharkeyd6697822020-03-22 20:59:47 -06004412
4413 // Update any playlists that reference this item
4414 if ((mediaType == FileColumns.MEDIA_TYPE_AUDIO)
4415 && helper.isExternal()) {
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06004416 helper.runWithTransaction((db) -> {
4417 try (Cursor cc = db.query("audio_playlists_map",
4418 new String[] { "playlist_id" }, "audio_id=" + id,
4419 null, "playlist_id", null, null)) {
4420 while (cc.moveToNext()) {
4421 final Uri playlistUri = ContentUris.withAppendedId(
4422 Playlists.getContentUri(volumeName),
4423 cc.getLong(0));
4424 resolvePlaylistMembers(playlistUri);
4425 }
Marco Nelissen4eff7fe2012-04-06 12:41:41 -07004426 }
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06004427 return null;
4428 });
Marco Nelissenbae40612017-06-22 12:53:21 -07004429 }
4430 }
4431 } finally {
Jeff Sharkeyb6781bc2019-07-18 18:45:52 -06004432 FileUtils.closeQuietly(c);
Marco Nelissenbae40612017-06-22 12:53:21 -07004433 }
4434 // Do not allow deletion if the file/object is referenced as parent
4435 // by some other entries. It could cause database corruption.
Jeff Sharkeyf57fa202018-07-26 14:36:41 -06004436 appendWhereStandalone(qb, ID_NOT_PARENT_CLAUSE);
Marco Nelissenbae40612017-06-22 12:53:21 -07004437 }
4438 }
4439
4440 switch (match) {
Marco Nelissenbae40612017-06-22 12:53:21 -07004441 case AUDIO_GENRES_ID_MEMBERS:
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06004442 throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R);
Marco Nelissenbae40612017-06-22 12:53:21 -07004443
4444 case IMAGES_THUMBNAILS_ID:
4445 case IMAGES_THUMBNAILS:
4446 case VIDEO_THUMBNAILS_ID:
4447 case VIDEO_THUMBNAILS:
4448 // Delete the referenced files first.
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07004449 Cursor c = qb.query(helper, sDataOnlyColumn, userWhere, userWhereArgs, null,
4450 null, null, null, null);
Marco Nelissenbae40612017-06-22 12:53:21 -07004451 if (c != null) {
4452 try {
4453 while (c.moveToNext()) {
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004454 deleteIfAllowed(uri, extras, c.getString(0));
Marco Nelissena6207282012-02-03 16:54:57 -08004455 }
Mattias Nilssona79fcf12014-03-26 17:18:35 +01004456 } finally {
Jeff Sharkeyb6781bc2019-07-18 18:45:52 -06004457 FileUtils.closeQuietly(c);
Marco Nelissena6207282012-02-03 16:54:57 -08004458 }
4459 }
Jeff Sharkeyd6697822020-03-22 20:59:47 -06004460 count += deleteRecursive(qb, helper, userWhere, userWhereArgs);
Marco Nelissenbae40612017-06-22 12:53:21 -07004461 break;
Marco Nelissena6207282012-02-03 16:54:57 -08004462
Marco Nelissenbae40612017-06-22 12:53:21 -07004463 default:
Jeff Sharkeyd6697822020-03-22 20:59:47 -06004464 count += deleteRecursive(qb, helper, userWhere, userWhereArgs);
Marco Nelissenbae40612017-06-22 12:53:21 -07004465 break;
The Android Open Source Project70215272009-03-03 19:32:43 -08004466 }
Marco Nelissenbae40612017-06-22 12:53:21 -07004467
Sudheer Shanka1a79ac02019-01-17 13:14:52 -08004468 if (deletedDownloadIds.size() > 0) {
Martijn Coenen24b99352020-06-18 14:12:35 +02004469 // Do this on a background thread, since we don't want to make binder
4470 // calls as part of a FUSE call.
4471 helper.postBackground(() -> {
Jeff Sharkeye2750322020-01-07 22:06:24 -07004472 getContext().getSystemService(DownloadManager.class)
4473 .onMediaStoreDownloadsDeleted(deletedDownloadIds);
Martijn Coenen24b99352020-06-18 14:12:35 +02004474 });
Sudheer Shanka1a79ac02019-01-17 13:14:52 -08004475 }
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07004476
Jeff Sharkey8411c402020-04-29 22:12:36 -06004477 if (isFilesTable && !isCallingPackageSelf()) {
shafikac069d32020-04-02 17:18:04 +01004478 Metrics.logDeletion(volumeName, mCallingIdentity.get().uid,
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07004479 getCallingPackageOrSelf(), count);
4480 }
The Android Open Source Project70215272009-03-03 19:32:43 -08004481 }
4482
4483 return count;
4484 }
4485
Jeff Sharkey556d2d92018-07-12 19:51:27 -06004486 /**
4487 * Executes identical delete repeatedly within a single transaction until
4488 * stability is reached. Combined with {@link #ID_NOT_PARENT_CLAUSE}, this
4489 * can be used to recursively delete all matching entries, since it only
4490 * deletes parents when no references remaining.
4491 */
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07004492 private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere,
Jeff Sharkey556d2d92018-07-12 19:51:27 -06004493 String[] userWhereArgs) {
Jeff Sharkeyebe24b52020-04-21 14:41:44 -06004494 return (int) helper.runWithTransaction((db) -> {
4495 synchronized (mDirectoryCache) {
4496 mDirectoryCache.clear();
4497 }
Jeff Sharkeyc5793122019-08-19 15:58:35 -06004498
Jeff Sharkeyebe24b52020-04-21 14:41:44 -06004499 int n = 0;
4500 int total = 0;
4501 do {
4502 n = qb.delete(helper, userWhere, userWhereArgs);
4503 total += n;
4504 } while (n > 0);
4505 return total;
4506 });
Jeff Sharkey556d2d92018-07-12 19:51:27 -06004507 }
4508
The Android Open Source Project70215272009-03-03 19:32:43 -08004509 @Override
Marco Nelissen38b43642012-01-27 09:40:07 -08004510 public Bundle call(String method, String arg, Bundle extras) {
Jeff Sharkeyc5c39142019-12-15 22:46:03 -07004511 Trace.beginSection("call");
4512 try {
4513 return callInternal(method, arg, extras);
4514 } finally {
4515 Trace.endSection();
4516 }
4517 }
4518
4519 private Bundle callInternal(String method, String arg, Bundle extras) {
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004520 switch (method) {
Jeff Sharkeyd6697822020-03-22 20:59:47 -06004521 case MediaStore.RESOLVE_PLAYLIST_MEMBERS_CALL: {
4522 final LocalCallingIdentity token = clearLocalCallingIdentity();
4523 final CallingIdentity providerToken = clearCallingIdentity();
4524 try {
4525 final Uri playlistUri = extras.getParcelable(MediaStore.EXTRA_URI);
4526 resolvePlaylistMembers(playlistUri);
4527 } finally {
4528 restoreCallingIdentity(providerToken);
4529 restoreLocalCallingIdentity(token);
4530 }
4531 return null;
4532 }
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07004533 case MediaStore.RUN_IDLE_MAINTENANCE_CALL: {
4534 // Protect ourselves from random apps by requiring a generic
4535 // permission held by common debugging components, such as shell
4536 getContext().enforceCallingOrSelfPermission(
4537 android.Manifest.permission.DUMP, TAG);
4538 final LocalCallingIdentity token = clearLocalCallingIdentity();
4539 final CallingIdentity providerToken = clearCallingIdentity();
4540 try {
4541 onIdleMaintenance(new CancellationSignal());
4542 } finally {
4543 restoreCallingIdentity(providerToken);
4544 restoreLocalCallingIdentity(token);
4545 }
4546 return null;
4547 }
Jeff Sharkey4209a842019-06-12 11:01:07 -06004548 case MediaStore.WAIT_FOR_IDLE_CALL: {
Jeff Sharkeye04e2c62020-03-05 10:53:33 -07004549 ForegroundThread.waitForIdle();
4550 BackgroundThread.waitForIdle();
Jeff Sharkey4209a842019-06-12 11:01:07 -06004551 return null;
4552 }
Jeff Sharkey54e874e2019-01-25 17:22:23 -07004553 case MediaStore.SCAN_FILE_CALL:
4554 case MediaStore.SCAN_VOLUME_CALL: {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06004555 final LocalCallingIdentity token = clearLocalCallingIdentity();
Jeff Sharkeyce59a542019-06-11 13:06:13 -06004556 final CallingIdentity providerToken = clearCallingIdentity();
Jeff Sharkey54e874e2019-01-25 17:22:23 -07004557 try {
Jeff Sharkey54e874e2019-01-25 17:22:23 -07004558 final Bundle res = new Bundle();
4559 switch (method) {
Jeff Sharkeyc5c39142019-12-15 22:46:03 -07004560 case MediaStore.SCAN_FILE_CALL: {
4561 final File file = new File(arg);
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07004562 res.putParcelable(Intent.EXTRA_STREAM, scanFile(file, REASON_DEMAND));
Jeff Sharkey54e874e2019-01-25 17:22:23 -07004563 break;
Jeff Sharkeyc5c39142019-12-15 22:46:03 -07004564 }
4565 case MediaStore.SCAN_VOLUME_CALL: {
4566 final String volumeName = arg;
4567 MediaService.onScanVolume(getContext(), volumeName, REASON_DEMAND);
Jeff Sharkey54e874e2019-01-25 17:22:23 -07004568 break;
Jeff Sharkeyc5c39142019-12-15 22:46:03 -07004569 }
Jeff Sharkey54e874e2019-01-25 17:22:23 -07004570 }
4571 return res;
4572 } catch (IOException e) {
4573 throw new RuntimeException(e);
4574 } finally {
Jeff Sharkeyce59a542019-06-11 13:06:13 -06004575 restoreCallingIdentity(providerToken);
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06004576 restoreLocalCallingIdentity(token);
Jeff Sharkey54e874e2019-01-25 17:22:23 -07004577 }
4578 }
Jeff Sharkey44ef4402019-03-01 11:31:10 -07004579 case MediaStore.GET_VERSION_CALL: {
4580 final String volumeName = extras.getString(Intent.EXTRA_TEXT);
4581
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06004582 final DatabaseHelper helper;
Jeff Sharkey44ef4402019-03-01 11:31:10 -07004583 try {
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06004584 helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName));
Jeff Sharkey44ef4402019-03-01 11:31:10 -07004585 } catch (VolumeNotFoundException e) {
4586 throw e.rethrowAsIllegalArgumentException();
4587 }
4588
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06004589 final String version = helper.runWithoutTransaction((db) -> {
4590 return db.getVersion() + ":" + DatabaseHelper.getOrCreateUuid(db);
4591 });
Jeff Sharkey44ef4402019-03-01 11:31:10 -07004592
4593 final Bundle res = new Bundle();
4594 res.putString(Intent.EXTRA_TEXT, version);
4595 return res;
4596 }
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07004597 case MediaStore.GET_GENERATION_CALL: {
4598 final String volumeName = extras.getString(Intent.EXTRA_TEXT);
4599
Jeff Sharkeyb2dbc6c2020-03-31 19:35:28 -06004600 final DatabaseHelper helper;
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07004601 try {
Jeff Sharkeyb2dbc6c2020-03-31 19:35:28 -06004602 helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName));
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07004603 } catch (VolumeNotFoundException e) {
4604 throw e.rethrowAsIllegalArgumentException();
4605 }
4606
Jeff Sharkeyb2dbc6c2020-03-31 19:35:28 -06004607 final long generation = helper.runWithoutTransaction((db) -> {
4608 return DatabaseHelper.getGeneration(db);
4609 });
4610
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07004611 final Bundle res = new Bundle();
4612 res.putLong(Intent.EXTRA_INDEX, generation);
4613 return res;
4614 }
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004615 case MediaStore.GET_DOCUMENT_URI_CALL: {
Jeff Sharkeye2750322020-01-07 22:06:24 -07004616 final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI);
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004617 enforceCallingPermission(mediaUri, extras, false);
Marco Nelissen38b43642012-01-27 09:40:07 -08004618
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004619 final Uri fileUri;
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06004620 final LocalCallingIdentity token = clearLocalCallingIdentity();
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004621 try {
4622 fileUri = Uri.fromFile(queryForDataFile(mediaUri, null));
4623 } catch (FileNotFoundException e) {
4624 throw new IllegalArgumentException(e);
4625 } finally {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06004626 restoreLocalCallingIdentity(token);
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004627 }
4628
4629 try (ContentProviderClient client = getContext().getContentResolver()
4630 .acquireUnstableContentProviderClient(
Jeff Sharkeye2750322020-01-07 22:06:24 -07004631 MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
4632 extras.putParcelable(MediaStore.EXTRA_URI, fileUri);
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004633 return client.call(method, null, extras);
4634 } catch (RemoteException e) {
4635 throw new IllegalStateException(e);
4636 }
4637 }
4638 case MediaStore.GET_MEDIA_URI_CALL: {
Jeff Sharkeye2750322020-01-07 22:06:24 -07004639 final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI);
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004640 getContext().enforceCallingUriPermission(documentUri,
4641 Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG);
4642
4643 final Uri fileUri;
4644 try (ContentProviderClient client = getContext().getContentResolver()
4645 .acquireUnstableContentProviderClient(
Jeff Sharkeye2750322020-01-07 22:06:24 -07004646 MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) {
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004647 final Bundle res = client.call(method, null, extras);
Jeff Sharkeye2750322020-01-07 22:06:24 -07004648 fileUri = res.getParcelable(MediaStore.EXTRA_URI);
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004649 } catch (RemoteException e) {
4650 throw new IllegalStateException(e);
4651 }
4652
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06004653 final LocalCallingIdentity token = clearLocalCallingIdentity();
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004654 try {
4655 final Bundle res = new Bundle();
Jeff Sharkeye2750322020-01-07 22:06:24 -07004656 res.putParcelable(MediaStore.EXTRA_URI,
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004657 queryForMediaUri(new File(fileUri.getPath()), null));
4658 return res;
4659 } catch (FileNotFoundException e) {
4660 throw new IllegalArgumentException(e);
4661 } finally {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06004662 restoreLocalCallingIdentity(token);
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004663 }
4664 }
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07004665 case MediaStore.CREATE_WRITE_REQUEST_CALL:
4666 case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
4667 case MediaStore.CREATE_TRASH_REQUEST_CALL:
4668 case MediaStore.CREATE_DELETE_REQUEST_CALL: {
4669 final PendingIntent pi = createRequest(method, extras);
4670 final Bundle res = new Bundle();
4671 res.putParcelable(MediaStore.EXTRA_RESULT, pi);
4672 return res;
Jeff Sharkey56c34e82019-11-21 15:56:37 -07004673 }
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06004674 default:
4675 throw new UnsupportedOperationException("Unsupported call: " + method);
4676 }
4677 }
Marco Nelissen8a1db2e2017-11-30 12:46:54 -08004678
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07004679 static List<Uri> collectUris(ClipData clipData) {
4680 final ArrayList<Uri> res = new ArrayList<>();
4681 for (int i = 0; i < clipData.getItemCount(); i++) {
4682 res.add(clipData.getItemAt(i).getUri());
4683 }
4684 return res;
4685 }
4686
4687 /**
4688 * Generate the {@link PendingIntent} for the given grant request. This
4689 * method also sanity checks the incoming arguments for security purposes
4690 * before creating the privileged {@link PendingIntent}.
4691 */
4692 private @NonNull PendingIntent createRequest(@NonNull String method, @NonNull Bundle extras) {
4693 final ClipData clipData = extras.getParcelable(MediaStore.EXTRA_CLIP_DATA);
4694 final List<Uri> uris = collectUris(clipData);
4695
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07004696 for (Uri uri : uris) {
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07004697 final int match = matchUri(uri, false);
4698 switch (match) {
4699 case IMAGES_MEDIA_ID:
4700 case AUDIO_MEDIA_ID:
4701 case VIDEO_MEDIA_ID:
4702 // Caller is requesting a specific media item by its ID,
4703 // which means it's valid for requests
4704 break;
4705 default:
4706 throw new IllegalArgumentException(
4707 "All requested items must be referenced by specific ID");
4708 }
4709 }
4710
4711 // Enforce that limited set of columns can be mutated
4712 final ContentValues values = extras.getParcelable(MediaStore.EXTRA_CONTENT_VALUES);
4713 final List<String> allowedColumns;
4714 switch (method) {
4715 case MediaStore.CREATE_FAVORITE_REQUEST_CALL:
4716 allowedColumns = Arrays.asList(
4717 MediaColumns.IS_FAVORITE);
4718 break;
4719 case MediaStore.CREATE_TRASH_REQUEST_CALL:
4720 allowedColumns = Arrays.asList(
Jeff Sharkey05c3a032020-04-09 16:57:04 -06004721 MediaColumns.IS_TRASHED);
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07004722 break;
4723 default:
4724 allowedColumns = Arrays.asList();
4725 break;
4726 }
4727 if (values != null) {
4728 for (String key : values.keySet()) {
4729 if (!allowedColumns.contains(key)) {
4730 throw new IllegalArgumentException("Invalid column " + key);
4731 }
4732 }
4733 }
4734
4735 final Context context = getContext();
4736 final Intent intent = new Intent(method, null, context, PermissionActivity.class);
4737 intent.putExtras(extras);
4738 return PendingIntent.getActivity(context, PermissionActivity.REQUEST_CODE, intent,
4739 FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE);
4740 }
4741
Jeff Sharkeye9876152018-12-08 11:14:13 -07004742 /**
Jeff Sharkey66881302019-10-05 10:50:06 -06004743 * Ensure that all local databases have a custom collator registered for the
4744 * given {@link ULocale} locale.
4745 *
4746 * @return the corresponding custom collation name to be used in
4747 * {@code ORDER BY} clauses.
4748 */
4749 private @NonNull String ensureCustomCollator(@NonNull String locale) {
4750 // Quick sanity check that requested locale looks sane
4751 new ULocale(locale);
4752
4753 final String collationName = "custom_" + locale.replaceAll("[^a-zA-Z]", "");
4754 synchronized (mCustomCollators) {
4755 if (!mCustomCollators.contains(collationName)) {
4756 for (DatabaseHelper helper : new DatabaseHelper[] {
4757 mInternalDatabase,
4758 mExternalDatabase
4759 }) {
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06004760 helper.runWithoutTransaction((db) -> {
4761 db.execPerConnectionSQL("SELECT icu_load_collation(?, ?);",
4762 new String[] { locale, collationName });
4763 return null;
4764 });
Jeff Sharkey66881302019-10-05 10:50:06 -06004765 }
4766 mCustomCollators.add(collationName);
4767 }
4768 }
4769 return collationName;
4770 }
4771
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06004772 private int pruneThumbnails(@NonNull SQLiteDatabase db, @NonNull CancellationSignal signal) {
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07004773 int prunedCount = 0;
4774
Jeff Sharkey71437302019-04-09 23:46:52 -06004775 // Determine all known media items
4776 final LongArray knownIds = new LongArray();
4777 try (Cursor c = db.query(true, "files", new String[] { BaseColumns._ID },
4778 null, null, null, null, null, null, signal)) {
4779 while (c.moveToNext()) {
4780 knownIds.add(c.getLong(0));
4781 }
4782 }
Marco Nelissen8a1db2e2017-11-30 12:46:54 -08004783
Jeff Sharkey71437302019-04-09 23:46:52 -06004784 final long[] knownIdsRaw = knownIds.toArray();
4785 Arrays.sort(knownIdsRaw);
4786
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06004787 for (String volumeName : getExternalVolumeNames()) {
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07004788 final List<File> thumbDirs;
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06004789 try {
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07004790 thumbDirs = getThumbnailDirectories(volumeName);
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06004791 } catch (FileNotFoundException e) {
4792 Log.w(TAG, "Failed to resolve volume " + volumeName, e);
4793 continue;
Marco Nelissen8a1db2e2017-11-30 12:46:54 -08004794 }
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06004795
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06004796 // Reconcile all thumbnails, deleting stale items
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07004797 for (File thumbDir : thumbDirs) {
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06004798 // Possibly bail before digging into each directory
4799 signal.throwIfCanceled();
4800
Jeff Sharkey7ea24f22019-08-22 10:14:18 -06004801 final File[] files = thumbDir.listFiles();
4802 for (File thumbFile : (files != null) ? files : new File[0]) {
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07004803 if (Objects.equals(thumbFile.getName(), FILE_DATABASE_UUID)) continue;
Jeff Sharkeye76c4262019-12-06 14:46:00 -07004804 final String name = FileUtils.extractFileName(thumbFile.getName());
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06004805 try {
4806 final long id = Long.parseLong(name);
4807 if (Arrays.binarySearch(knownIdsRaw, id) >= 0) {
4808 // Thumbnail belongs to known media, keep it
4809 continue;
4810 }
4811 } catch (NumberFormatException e) {
4812 }
4813
4814 Log.v(TAG, "Deleting stale thumbnail " + thumbFile);
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07004815 deleteAndInvalidate(thumbFile);
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07004816 prunedCount++;
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06004817 }
4818 }
Marco Nelissen8a1db2e2017-11-30 12:46:54 -08004819 }
Jeff Sharkey71437302019-04-09 23:46:52 -06004820
4821 // Also delete stale items from legacy tables
4822 db.execSQL("delete from thumbnails "
4823 + "where image_id not in (select _id from images)");
4824 db.execSQL("delete from videothumbnails "
4825 + "where video_id not in (select _id from video)");
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07004826
4827 return prunedCount;
Marco Nelissen8a1db2e2017-11-30 12:46:54 -08004828 }
4829
Jeff Sharkeyc5c39142019-12-15 22:46:03 -07004830 abstract class Thumbnailer {
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004831 final String directoryName;
4832
4833 public Thumbnailer(String directoryName) {
4834 this.directoryName = directoryName;
4835 }
4836
4837 private File getThumbnailFile(Uri uri) throws IOException {
Jeff Sharkey71437302019-04-09 23:46:52 -06004838 final String volumeName = resolveVolumeName(uri);
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06004839 final File volumePath = getVolumePath(volumeName);
Jeff Sharkey7ea24f22019-08-22 10:14:18 -06004840 return FileUtils.buildPath(volumePath, directoryName,
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07004841 DIRECTORY_THUMBNAILS, ContentUris.parseId(uri) + ".jpg");
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004842 }
4843
4844 public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal)
4845 throws IOException;
4846
Jeff Sharkeycce43112020-02-09 18:07:36 -07004847 public ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
4848 throws IOException {
4849 // First attempt to fast-path by opening the thumbnail; if it
4850 // doesn't exist we fall through to create it below
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004851 final File thumbFile = getThumbnailFile(uri);
Jeff Sharkeycce43112020-02-09 18:07:36 -07004852 try {
Jeff Sharkey9a497642020-04-23 13:15:10 -06004853 return FileUtils.openSafely(thumbFile,
Jeff Sharkeycce43112020-02-09 18:07:36 -07004854 ParcelFileDescriptor.MODE_READ_ONLY);
4855 } catch (FileNotFoundException ignored) {
4856 }
4857
Jeff Sharkeyf12923c2020-01-19 12:12:14 -07004858 final File thumbDir = thumbFile.getParentFile();
4859 thumbDir.mkdirs();
Jeff Sharkeycce43112020-02-09 18:07:36 -07004860
4861 // When multiple threads race for the same thumbnail, the second
4862 // thread could return a file with a thumbnail still in
4863 // progress. We could add heavy per-ID locking to mitigate this
4864 // rare race condition, but it's simpler to have both threads
4865 // generate the same thumbnail using temporary files and rename
4866 // them into place once finished.
4867 final File thumbTempFile = File.createTempFile("thumb", null, thumbDir);
4868
4869 ParcelFileDescriptor thumbWrite = null;
4870 ParcelFileDescriptor thumbRead = null;
4871 try {
4872 // Open our temporary file twice: once for local writing, and
4873 // once for remote reading. Both FDs point at the same
4874 // underlying inode on disk, so they're stable across renames
4875 // to avoid race conditions between threads.
Jeff Sharkey9a497642020-04-23 13:15:10 -06004876 thumbWrite = FileUtils.openSafely(thumbTempFile,
Jeff Sharkeycce43112020-02-09 18:07:36 -07004877 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE);
Jeff Sharkey9a497642020-04-23 13:15:10 -06004878 thumbRead = FileUtils.openSafely(thumbTempFile,
Jeff Sharkeycce43112020-02-09 18:07:36 -07004879 ParcelFileDescriptor.MODE_READ_ONLY);
4880
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004881 final Bitmap thumbnail = getThumbnailBitmap(uri, signal);
Jeff Sharkeycce43112020-02-09 18:07:36 -07004882 thumbnail.compress(Bitmap.CompressFormat.JPEG, 90,
4883 new FileOutputStream(thumbWrite.getFileDescriptor()));
4884
4885 try {
4886 // Use direct syscall for better failure logs
4887 Os.rename(thumbTempFile.getAbsolutePath(), thumbFile.getAbsolutePath());
4888 } catch (ErrnoException e) {
4889 e.rethrowAsIOException();
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004890 }
Jeff Sharkeycce43112020-02-09 18:07:36 -07004891
4892 // Everything above went peachy, so return a duplicate of our
4893 // already-opened read FD to keep our finally logic below simple
4894 return thumbRead.dup();
4895
4896 } finally {
4897 // Regardless of success or failure, try cleaning up any
4898 // remaining temporary file and close all our local FDs
4899 FileUtils.closeQuietly(thumbWrite);
4900 FileUtils.closeQuietly(thumbRead);
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07004901 deleteAndInvalidate(thumbTempFile);
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004902 }
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004903 }
4904
4905 public void invalidateThumbnail(Uri uri) throws IOException {
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07004906 deleteAndInvalidate(getThumbnailFile(uri));
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004907 }
4908 }
4909
Jeff Sharkeybeeca1e2019-01-24 15:57:08 -07004910 private Thumbnailer mAudioThumbnailer = new Thumbnailer(Environment.DIRECTORY_MUSIC) {
4911 @Override
4912 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
4913 return ThumbnailUtils.createAudioThumbnail(queryForDataFile(uri, signal),
4914 mThumbSize, signal);
4915 }
4916 };
4917
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004918 private Thumbnailer mVideoThumbnailer = new Thumbnailer(Environment.DIRECTORY_MOVIES) {
4919 @Override
4920 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
4921 return ThumbnailUtils.createVideoThumbnail(queryForDataFile(uri, signal),
4922 mThumbSize, signal);
4923 }
4924 };
4925
4926 private Thumbnailer mImageThumbnailer = new Thumbnailer(Environment.DIRECTORY_PICTURES) {
4927 @Override
4928 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException {
4929 return ThumbnailUtils.createImageThumbnail(queryForDataFile(uri, signal),
4930 mThumbSize, signal);
4931 }
4932 };
4933
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07004934 private List<File> getThumbnailDirectories(String volumeName) throws FileNotFoundException {
4935 final File volumePath = getVolumePath(volumeName);
4936 return Arrays.asList(
4937 FileUtils.buildPath(volumePath, DIRECTORY_MUSIC, DIRECTORY_THUMBNAILS),
4938 FileUtils.buildPath(volumePath, DIRECTORY_MOVIES, DIRECTORY_THUMBNAILS),
4939 FileUtils.buildPath(volumePath, DIRECTORY_PICTURES, DIRECTORY_THUMBNAILS));
4940 }
4941
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004942 private void invalidateThumbnails(Uri uri) {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06004943 Trace.beginSection("invalidateThumbnails");
Jeff Sharkey031af8d2019-04-28 11:11:30 -06004944 try {
4945 invalidateThumbnailsInternal(uri);
4946 } finally {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06004947 Trace.endSection();
Jeff Sharkey031af8d2019-04-28 11:11:30 -06004948 }
4949 }
4950
4951 private void invalidateThumbnailsInternal(Uri uri) {
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004952 final long id = ContentUris.parseId(uri);
4953 try {
Jeff Sharkeybeeca1e2019-01-24 15:57:08 -07004954 mAudioThumbnailer.invalidateThumbnail(uri);
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07004955 mVideoThumbnailer.invalidateThumbnail(uri);
4956 mImageThumbnailer.invalidateThumbnail(uri);
4957 } catch (IOException ignored) {
4958 }
4959
Jeff Sharkey5ed33602019-01-23 14:31:30 -07004960 final DatabaseHelper helper;
Jeff Sharkey5ed33602019-01-23 14:31:30 -07004961 try {
4962 helper = getDatabaseForUri(uri);
Jeff Sharkey5ed33602019-01-23 14:31:30 -07004963 } catch (VolumeNotFoundException e) {
4964 Log.w(TAG, e);
4965 return;
4966 }
4967
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06004968 helper.runWithTransaction((db) -> {
4969 final String idString = Long.toString(id);
4970 try (Cursor c = db.rawQuery("select _data from thumbnails where image_id=?"
4971 + " union all select _data from videothumbnails where video_id=?",
4972 new String[] { idString, idString })) {
4973 while (c.moveToNext()) {
4974 String path = c.getString(0);
4975 deleteIfAllowed(uri, Bundle.EMPTY, path);
4976 }
Marco Nelissen8a1db2e2017-11-30 12:46:54 -08004977 }
Jeff Sharkey031af8d2019-04-28 11:11:30 -06004978
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06004979 db.execSQL("delete from thumbnails where image_id=?", new String[] { idString });
4980 db.execSQL("delete from videothumbnails where video_id=?", new String[] { idString });
4981 return null;
4982 });
Marco Nelissen8a1db2e2017-11-30 12:46:54 -08004983 }
4984
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004985 /**
4986 * @deprecated all operations should be routed through the overload that
4987 * accepts a {@link Bundle} of extras.
4988 */
Marco Nelissen38b43642012-01-27 09:40:07 -08004989 @Override
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004990 @Deprecated
4991 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -07004992 return update(uri, values,
4993 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null));
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07004994 }
4995
4996 @Override
4997 public int update(@NonNull Uri uri, @Nullable ContentValues values,
4998 @Nullable Bundle extras) {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06004999 Trace.beginSection("update");
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06005000 try {
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07005001 return updateInternal(uri, values, extras);
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06005002 } catch (FallbackException e) {
5003 return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion());
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06005004 } finally {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06005005 Trace.endSection();
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06005006 }
5007 }
5008
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07005009 private int updateInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
5010 @Nullable Bundle extras) throws FallbackException {
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005011 extras = (extras != null) ? extras : new Bundle();
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07005012
Jeff Sharkey61378cb2019-11-23 16:11:09 -07005013 // Related items are only considered for new media creation, and they
5014 // can't be leveraged to move existing content into blocked locations
5015 extras.remove(QUERY_ARG_RELATED_URI);
Nikita Ioffe710787b2020-06-11 14:35:14 +01005016 // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
5017 extras.remove(INCLUDED_DEFAULT_DIRECTORIES);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07005018
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07005019 final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION);
5020 final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS);
5021
Nandana Dutt3a931942020-03-20 10:32:24 +00005022 // Limit the hacky workaround to camera targeting Q and below, to allow newer versions
5023 // of camera that does the right thing to work correctly.
5024 if ("com.google.android.GoogleCamera".equals(getCallingPackageOrSelf())
5025 && getCallingPackageTargetSdkVersion() <= Build.VERSION_CODES.Q) {
Jeff Sharkeyd9767f22018-08-02 11:50:02 -06005026 if (matchUri(uri, false) == IMAGES_MEDIA_ID) {
5027 Log.w(TAG, "Working around app bug in b/111966296");
5028 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri));
Jeff Sharkey33966012018-08-06 10:17:58 -06005029 } else if (matchUri(uri, false) == VIDEO_MEDIA_ID) {
5030 Log.w(TAG, "Working around app bug in b/112246630");
5031 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri));
Jeff Sharkeyd9767f22018-08-02 11:50:02 -06005032 }
5033 }
5034
Marco Nelissen01e706a2013-09-12 15:38:42 -07005035 uri = safeUncanonicalize(uri);
Jeff Sharkeyc08c37d2019-02-28 16:39:19 -07005036
The Android Open Source Project70215272009-03-03 19:32:43 -08005037 int count;
Jeff Sharkey94461582018-07-12 14:34:47 -06005038
Jeff Sharkeyd9628b12018-09-28 11:13:27 -06005039 final String volumeName = getVolumeName(uri);
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07005040 final int targetSdkVersion = getCallingPackageTargetSdkVersion();
Jeff Sharkey94461582018-07-12 14:34:47 -06005041 final boolean allowHidden = isCallingPackageAllowedHidden();
5042 final int match = matchUri(uri, allowHidden);
5043
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005044 switch (match) {
5045 case AUDIO_PLAYLISTS_ID_MEMBERS_ID:
5046 extras.putString(QUERY_ARG_SQL_SELECTION,
5047 BaseColumns._ID + "=" + uri.getPathSegments().get(5));
5048 // fall-through
5049 case AUDIO_PLAYLISTS_ID_MEMBERS: {
5050 final long playlistId = Long.parseLong(uri.getPathSegments().get(3));
5051 final Uri playlistUri = ContentUris.withAppendedId(
5052 MediaStore.Audio.Playlists.getContentUri(volumeName), playlistId);
5053
Jeff Sharkeya9473e92020-04-17 15:54:30 -06005054 if (uri.getBooleanQueryParameter("move", false)) {
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005055 // Convert explicit request into query; sigh, moveItem()
5056 // uses zero-based indexing instead of one-based indexing
5057 final int from = Integer.parseInt(uri.getPathSegments().get(5)) + 1;
5058 final int to = initialValues.getAsInteger(Playlists.Members.PLAY_ORDER) + 1;
5059 extras.putString(QUERY_ARG_SQL_SELECTION,
5060 Playlists.Members.PLAY_ORDER + "=" + from);
5061 initialValues.put(Playlists.Members.PLAY_ORDER, to);
5062 }
5063
5064 // Playlist contents are always persisted directly into playlist
5065 // files on disk to ensure that we can reliably migrate between
5066 // devices and recover from database corruption
5067 final int index;
5068 if (initialValues.containsKey(Playlists.Members.PLAY_ORDER)) {
5069 index = movePlaylistMembers(playlistUri, initialValues, extras);
5070 } else {
5071 index = resolvePlaylistIndex(playlistUri, extras);
5072 }
5073 if (initialValues.containsKey(Playlists.Members.AUDIO_ID)) {
5074 final Bundle queryArgs = new Bundle();
5075 queryArgs.putString(QUERY_ARG_SQL_SELECTION,
5076 Playlists.Members.PLAY_ORDER + "=" + (index + 1));
5077 removePlaylistMembers(playlistUri, queryArgs);
5078
5079 final ContentValues values = new ContentValues();
5080 values.put(Playlists.Members.AUDIO_ID,
5081 initialValues.getAsString(Playlists.Members.AUDIO_ID));
5082 values.put(Playlists.Members.PLAY_ORDER, (index + 1));
5083 addPlaylistMembers(playlistUri, values);
5084 }
5085 return 1;
5086 }
5087 }
5088
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06005089 final DatabaseHelper helper = getDatabaseForUri(uri);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07005090 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null);
Jeff Sharkeyb6485ce2019-04-15 14:20:47 -06005091
5092 // Give callers interacting with a specific media item a chance to
5093 // escalate access if they don't already have it
5094 switch (match) {
5095 case AUDIO_MEDIA_ID:
5096 case VIDEO_MEDIA_ID:
5097 case IMAGES_MEDIA_ID:
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07005098 enforceCallingPermission(uri, extras, true);
Jeff Sharkeyb6485ce2019-04-15 14:20:47 -06005099 }
The Android Open Source Project70215272009-03-03 19:32:43 -08005100
Jeff Sharkey031af8d2019-04-28 11:11:30 -06005101 boolean triggerInvalidate = false;
Jeff Sharkey291e7772019-01-03 14:16:33 -07005102 boolean triggerScan = false;
Mike Lockwoodb8f9b762011-07-31 17:51:07 -04005103 if (initialValues != null) {
Jeff Sharkeybac84e22018-12-20 15:11:17 -07005104 // IDs are forever; nobody should be editing them
5105 initialValues.remove(MediaColumns._ID);
Sudheer Shanka56cba322018-12-07 10:55:58 -08005106
Jeff Sharkey05c3a032020-04-09 16:57:04 -06005107 // Expiration times are hard-coded; let's derive them
5108 FileUtils.computeDateExpires(initialValues);
5109
Jeff Sharkeyb0cab582019-04-16 12:42:42 -06005110 // Ignore or augment incoming raw filesystem paths
Jeff Sharkey0218c142018-10-19 15:37:00 -06005111 for (String column : sDataColumns.keySet()) {
5112 if (!initialValues.containsKey(column)) continue;
Jeff Sharkeyb0cab582019-04-16 12:42:42 -06005113
Jeff Sharkey8411c402020-04-29 22:12:36 -06005114 if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06005115 // Mutation allowed
Jeff Sharkeyb0cab582019-04-16 12:42:42 -06005116 } else {
5117 Log.w(TAG, "Ignoring mutation of " + column + " from "
5118 + getCallingPackageOrSelf());
5119 initialValues.remove(column);
5120 }
Jeff Sharkey16fb8052018-10-18 15:22:53 -06005121 }
5122
Jeff Sharkey8411c402020-04-29 22:12:36 -06005123 // Enforce allowed ownership transfers
5124 if (initialValues.containsKey(MediaColumns.OWNER_PACKAGE_NAME)) {
5125 if (isCallingPackageSelf() || isCallingPackageShell()) {
5126 // When the caller is the media scanner or the shell, we let
5127 // them change ownership however they see fit; nothing to do
5128 } else if (isCallingPackageDelegator()) {
5129 // When the caller is a delegator, allow them to shift
5130 // ownership only when current owner, or when ownerless
5131 final String currentOwner;
5132 final String proposedOwner = initialValues
5133 .getAsString(MediaColumns.OWNER_PACKAGE_NAME);
5134 final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
5135 ContentUris.parseId(uri));
5136 try (Cursor c = queryForSingleItem(genericUri,
5137 new String[] { MediaColumns.OWNER_PACKAGE_NAME }, null, null, null)) {
5138 currentOwner = c.getString(0);
5139 } catch (FileNotFoundException e) {
5140 throw new IllegalStateException(e);
5141 }
5142 final boolean transferAllowed = (currentOwner == null)
5143 || Arrays.asList(getSharedPackagesForPackage(getCallingPackageOrSelf()))
5144 .contains(currentOwner);
5145 if (transferAllowed) {
5146 Log.v(TAG, "Ownership transfer from " + currentOwner + " to "
5147 + proposedOwner + " allowed");
5148 } else {
5149 Log.w(TAG, "Ownership transfer from " + currentOwner + " to "
5150 + proposedOwner + " blocked");
5151 initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
5152 }
5153 } else {
5154 // Otherwise no ownership changes are allowed
5155 initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME);
5156 }
5157 }
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06005158
Jeff Sharkey8411c402020-04-29 22:12:36 -06005159 if (!isCallingPackageSelf()) {
5160 Trace.beginSection("filter");
Jeff Sharkeybac84e22018-12-20 15:11:17 -07005161
Jeff Sharkeyd06f2142019-04-29 12:30:17 -06005162 // We default to filtering mutable columns, except when we know
5163 // the single item being updated is pending; when it's finally
5164 // published we'll overwrite these values.
Jeff Sharkey95ee8962019-05-20 09:45:40 -06005165 final Uri finalUri = uri;
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06005166 final Supplier<Boolean> isPending = new CachedSupplier<>(() -> {
Jeff Sharkey95ee8962019-05-20 09:45:40 -06005167 return isPending(finalUri);
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06005168 });
Jeff Sharkeyd06f2142019-04-29 12:30:17 -06005169
Jeff Sharkeybac84e22018-12-20 15:11:17 -07005170 // Column values controlled by media scanner aren't writable by
5171 // apps, since any edits here don't reflect the metadata on
5172 // disk, and they'd be overwritten during a rescan.
5173 for (String column : new ArraySet<>(initialValues.keySet())) {
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06005174 if (sMutableColumns.contains(column)) {
5175 // Mutation normally allowed
5176 } else if (isPending.get()) {
5177 // Mutation relaxed while pending
5178 } else {
Jeff Sharkeybac84e22018-12-20 15:11:17 -07005179 Log.w(TAG, "Ignoring mutation of " + column + " from "
5180 + getCallingPackageOrSelf());
5181 initialValues.remove(column);
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005182 triggerScan = true;
Jeff Sharkeybac84e22018-12-20 15:11:17 -07005183 }
Jeff Sharkeyb3bd14f2019-04-13 19:29:27 -06005184
5185 // If we're publishing this item, perform a blocking scan to
5186 // make sure metadata is updated
5187 if (MediaColumns.IS_PENDING.equals(column)) {
5188 triggerScan = true;
Jeff Sharkeye1a74202020-04-29 20:24:58 -06005189
5190 // Explicitly clear columns used to ignore no-op scans,
5191 // since we need to force a scan on publish
5192 initialValues.putNull(MediaColumns.DATE_MODIFIED);
5193 initialValues.putNull(MediaColumns.SIZE);
Jeff Sharkeyb3bd14f2019-04-13 19:29:27 -06005194 }
Jeff Sharkeybac84e22018-12-20 15:11:17 -07005195 }
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06005196
Jeff Sharkey0b801a52019-08-08 11:19:51 -06005197 Trace.endSection();
Jeff Sharkeybac84e22018-12-20 15:11:17 -07005198 }
5199
Sudheer Shanka56cba322018-12-07 10:55:58 -08005200 if ("files".equals(qb.getTables())) {
5201 maybeMarkAsDownload(initialValues);
5202 }
Jeff Sharkeyd2a38222018-12-04 11:23:48 -07005203
5204 // We no longer track location metadata
5205 if (initialValues.containsKey(ImageColumns.LATITUDE)) {
5206 initialValues.putNull(ImageColumns.LATITUDE);
5207 }
5208 if (initialValues.containsKey(ImageColumns.LONGITUDE)) {
5209 initialValues.putNull(ImageColumns.LONGITUDE);
5210 }
Mike Lockwoodb8f9b762011-07-31 17:51:07 -04005211 }
5212
Jeff Sharkeybac84e22018-12-20 15:11:17 -07005213 // If we're not updating anything, then we can skip
5214 if (initialValues.isEmpty()) return 0;
5215
Jeff Sharkey0e880712019-02-11 11:01:31 -07005216 final boolean isThumbnail;
5217 switch (match) {
5218 case IMAGES_THUMBNAILS:
5219 case IMAGES_THUMBNAILS_ID:
5220 case VIDEO_THUMBNAILS:
5221 case VIDEO_THUMBNAILS_ID:
5222 case AUDIO_ALBUMART:
5223 case AUDIO_ALBUMART_ID:
5224 isThumbnail = true;
5225 break;
5226 default:
5227 isThumbnail = false;
5228 break;
5229 }
5230
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005231 switch (match) {
5232 case AUDIO_PLAYLISTS:
5233 case AUDIO_PLAYLISTS_ID:
5234 // Playlist names are stored as display names, but leave
5235 // values untouched if the caller is ModernMediaScanner
Jeff Sharkey8411c402020-04-29 22:12:36 -06005236 if (!isCallingPackageSelf()) {
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005237 if (initialValues.containsKey(Playlists.NAME)) {
5238 initialValues.put(MediaColumns.DISPLAY_NAME,
5239 initialValues.getAsString(Playlists.NAME));
5240 }
5241 if (!initialValues.containsKey(MediaColumns.MIME_TYPE)) {
5242 initialValues.put(MediaColumns.MIME_TYPE, "audio/mpegurl");
5243 }
5244 }
5245 break;
5246 }
5247
Jeff Sharkey0e880712019-02-11 11:01:31 -07005248 // If we're touching columns that would change placement of a file,
5249 // blend in current values and recalculate path
Jeff Sharkeyfd7d0a32020-04-17 15:21:52 -06005250 final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT,
Jeff Sharkey8411c402020-04-29 22:12:36 -06005251 !isCallingPackageSelf());
Jeff Sharkey0e880712019-02-11 11:01:31 -07005252 if (containsAny(initialValues.keySet(), sPlacementColumns)
5253 && !initialValues.containsKey(MediaColumns.DATA)
Jeff Sharkeyfd7d0a32020-04-17 15:21:52 -06005254 && !isThumbnail
5255 && allowMovement) {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06005256 Trace.beginSection("movement");
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06005257
Jeff Sharkey0ab564c2019-04-28 11:55:21 -06005258 // We only support movement under well-defined collections
5259 switch (match) {
5260 case AUDIO_MEDIA_ID:
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005261 case AUDIO_PLAYLISTS_ID:
Jeff Sharkey0ab564c2019-04-28 11:55:21 -06005262 case VIDEO_MEDIA_ID:
5263 case IMAGES_MEDIA_ID:
5264 case DOWNLOADS_ID:
Jeff Sharkey89149b62020-03-29 22:03:44 -06005265 case FILES_ID:
Jeff Sharkey0ab564c2019-04-28 11:55:21 -06005266 break;
5267 default:
5268 throw new IllegalArgumentException("Movement of " + uri
5269 + " which isn't part of well-defined collection not allowed");
5270 }
5271
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06005272 final LocalCallingIdentity token = clearLocalCallingIdentity();
Jeff Sharkey8411c402020-04-29 22:12:36 -06005273 final Uri genericUri = MediaStore.Files.getContentUri(volumeName,
5274 ContentUris.parseId(uri));
5275 try (Cursor c = queryForSingleItem(genericUri,
Jeff Sharkeyad8ef4e2019-08-20 09:43:48 -06005276 sPlacementColumns.toArray(new String[0]), userWhere, userWhereArgs, null)) {
Jeff Sharkey0e880712019-02-11 11:01:31 -07005277 for (int i = 0; i < c.getColumnCount(); i++) {
5278 final String column = c.getColumnName(i);
5279 if (!initialValues.containsKey(column)) {
5280 initialValues.put(column, c.getString(i));
5281 }
5282 }
5283 } catch (FileNotFoundException e) {
5284 throw new IllegalStateException(e);
5285 } finally {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06005286 restoreLocalCallingIdentity(token);
Jeff Sharkey0e880712019-02-11 11:01:31 -07005287 }
5288
5289 // Regenerate path using blended values; this will throw if caller
5290 // is attempting to place file into invalid location
5291 final String beforePath = initialValues.getAsString(MediaColumns.DATA);
Jeff Sharkeyab919232019-04-15 10:39:35 -06005292 final String beforeVolume = extractVolumeName(beforePath);
5293 final String beforeOwner = extractPathOwnerPackageName(beforePath);
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06005294
Jeff Sharkey0e880712019-02-11 11:01:31 -07005295 initialValues.remove(MediaColumns.DATA);
Jeff Sharkey61378cb2019-11-23 16:11:09 -07005296 ensureNonUniqueFileColumns(match, uri, extras, initialValues, beforePath);
Jeff Sharkey0e880712019-02-11 11:01:31 -07005297
5298 final String probePath = initialValues.getAsString(MediaColumns.DATA);
Jeff Sharkeyab919232019-04-15 10:39:35 -06005299 final String probeVolume = extractVolumeName(probePath);
5300 final String probeOwner = extractPathOwnerPackageName(probePath);
Jeff Sharkey0e880712019-02-11 11:01:31 -07005301 if (Objects.equals(beforePath, probePath)) {
5302 Log.d(TAG, "Identical paths " + beforePath + "; not moving");
Jeff Sharkeyab919232019-04-15 10:39:35 -06005303 } else if (!Objects.equals(beforeVolume, probeVolume)) {
5304 throw new IllegalArgumentException("Changing volume from " + beforePath + " to "
5305 + probePath + " not allowed");
5306 } else if (!Objects.equals(beforeOwner, probeOwner)) {
5307 throw new IllegalArgumentException("Changing ownership from " + beforePath + " to "
5308 + probePath + " not allowed");
Jeff Sharkey0e880712019-02-11 11:01:31 -07005309 } else {
5310 // Now that we've confirmed an actual movement is taking place,
5311 // ensure we have a unique destination
5312 initialValues.remove(MediaColumns.DATA);
Jeff Sharkeyab27f022020-04-29 20:58:55 -06005313 ensureUniqueFileColumns(match, uri, extras, initialValues, beforePath);
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06005314
Jeff Sharkey0e880712019-02-11 11:01:31 -07005315 final String afterPath = initialValues.getAsString(MediaColumns.DATA);
5316
5317 Log.d(TAG, "Moving " + beforePath + " to " + afterPath);
5318 try {
5319 Os.rename(beforePath, afterPath);
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07005320 invalidateFuseDentry(beforePath);
5321 invalidateFuseDentry(afterPath);
Jeff Sharkey0e880712019-02-11 11:01:31 -07005322 } catch (ErrnoException e) {
Jeff Sharkeya2bdf542020-06-15 17:44:27 -06005323 if (e.errno == OsConstants.ENOENT) {
5324 Log.d(TAG, "Missing file at " + beforePath + "; continuing anyway");
5325 } else {
5326 throw new IllegalStateException(e);
5327 }
Jeff Sharkey0e880712019-02-11 11:01:31 -07005328 }
5329 initialValues.put(MediaColumns.DATA, afterPath);
Jeff Sharkey456ca0f2020-01-06 14:02:09 -07005330
5331 // Some indexed metadata may have been derived from the path on
5332 // disk, so scan this item again to update it
5333 triggerScan = true;
Jeff Sharkey0e880712019-02-11 11:01:31 -07005334 }
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06005335
Jeff Sharkey0b801a52019-08-08 11:19:51 -06005336 Trace.endSection();
Jeff Sharkey0e880712019-02-11 11:01:31 -07005337 }
5338
Abhijeet Kaur3bc15772021-11-17 08:40:34 +00005339 assertPrivatePathNotInValues(initialValues);
5340
Jeff Sharkey077b71e2019-01-22 13:19:51 -07005341 // Make sure any updated paths look sane
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06005342 assertFileColumnsSane(match, uri, initialValues);
Jeff Sharkey077b71e2019-01-22 13:19:51 -07005343
Jeff Sharkey031af8d2019-04-28 11:11:30 -06005344 if (initialValues.containsKey(FileColumns.DATA)) {
5345 // If we're changing paths, invalidate any thumbnails
5346 triggerInvalidate = true;
Jeff Sharkey7362fe22020-04-13 17:39:28 -06005347
5348 // If the new file exists, trigger a scan to adjust any metadata
5349 // that might be derived from the path
5350 final String data = initialValues.getAsString(FileColumns.DATA);
5351 if (!TextUtils.isEmpty(data) && new File(data).exists()) {
5352 triggerScan = true;
5353 }
5354 }
5355
5356 // If we're already doing this update from an internal scan, no need to
5357 // kick off another no-op scan
Jeff Sharkey8411c402020-04-29 22:12:36 -06005358 if (isCallingPackageSelf()) {
Jeff Sharkey7362fe22020-04-13 17:39:28 -06005359 triggerScan = false;
Jeff Sharkey031af8d2019-04-28 11:11:30 -06005360 }
5361
5362 // Since the update mutation may prevent us from matching items after
5363 // it's applied, we need to snapshot affected IDs here
5364 final LongArray updatedIds = new LongArray();
5365 if (triggerInvalidate || triggerScan) {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06005366 Trace.beginSection("snapshot");
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06005367 final LocalCallingIdentity token = clearLocalCallingIdentity();
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07005368 try (Cursor c = qb.query(helper, new String[] { FileColumns._ID },
5369 userWhere, userWhereArgs, null, null, null, null, null)) {
Jeff Sharkey031af8d2019-04-28 11:11:30 -06005370 while (c.moveToNext()) {
5371 updatedIds.add(c.getLong(0));
Marco Nelissen8a1db2e2017-11-30 12:46:54 -08005372 }
5373 } finally {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06005374 restoreLocalCallingIdentity(token);
Jeff Sharkey0b801a52019-08-08 11:19:51 -06005375 Trace.endSection();
Marco Nelissen8a1db2e2017-11-30 12:46:54 -08005376 }
5377 }
5378
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06005379 final ContentValues values = new ContentValues(initialValues);
Marco Nelissenbae40612017-06-22 12:53:21 -07005380 switch (match) {
Jeff Sharkeycd389642020-04-21 15:03:09 -06005381 case AUDIO_MEDIA_ID:
Jeff Sharkey74f73732019-11-12 15:36:32 -07005382 case AUDIO_PLAYLISTS_ID:
5383 case VIDEO_MEDIA_ID:
Marco Nelissenbae40612017-06-22 12:53:21 -07005384 case IMAGES_MEDIA_ID:
Jeff Sharkey74f73732019-11-12 15:36:32 -07005385 case FILES_ID:
5386 case DOWNLOADS_ID: {
Sahana Raoea587fc2020-06-03 15:56:23 +01005387 FileUtils.computeValuesFromData(values, isFuseThread());
Marco Nelissenbae40612017-06-22 12:53:21 -07005388 break;
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06005389 }
Jeff Sharkey74f73732019-11-12 15:36:32 -07005390 }
5391
Jeff Sharkeycd389642020-04-21 15:03:09 -06005392 if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) {
5393 final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE);
5394 switch (mediaType) {
5395 case FileColumns.MEDIA_TYPE_AUDIO: {
5396 computeAudioLocalizedValues(values);
5397 computeAudioKeyValues(values);
5398 break;
5399 }
5400 }
5401 }
5402
Sahana Rao34682a72020-05-04 02:02:37 +01005403 count = updateAllowingReplace(qb, helper, values, userWhere, userWhereArgs);
Jeff Sharkey29421112018-07-27 20:56:44 -06005404
Jeff Sharkey291e7772019-01-03 14:16:33 -07005405 // If the caller tried (and failed) to update metadata, the file on disk
5406 // might have changed, to scan it to collect the latest metadata.
Jeff Sharkey031af8d2019-04-28 11:11:30 -06005407 if (triggerInvalidate || triggerScan) {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06005408 Trace.beginSection("invalidate");
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06005409 final LocalCallingIdentity token = clearLocalCallingIdentity();
Jeff Sharkey031af8d2019-04-28 11:11:30 -06005410 try {
5411 for (int i = 0; i < updatedIds.size(); i++) {
5412 final long updatedId = updatedIds.get(i);
5413 final Uri updatedUri = Files.getContentUri(volumeName, updatedId);
Jeff Sharkey22988642020-03-05 17:09:39 -07005414 helper.postBackground(() -> {
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06005415 invalidateThumbnails(updatedUri);
5416 });
Jeff Sharkey031af8d2019-04-28 11:11:30 -06005417
5418 if (triggerScan) {
5419 try (Cursor c = queryForSingleItem(updatedUri,
5420 new String[] { FileColumns.DATA }, null, null, null)) {
Jeff Sharkeyc59bca92020-06-15 19:07:59 -06005421 final File file = new File(c.getString(0));
5422 helper.postBlocking(() -> {
5423 final LocalCallingIdentity tokenInner = clearLocalCallingIdentity();
5424 try {
5425 mMediaScanner.scanFile(file, REASON_DEMAND);
5426 } finally {
5427 restoreLocalCallingIdentity(tokenInner);
5428 }
5429 });
Jeff Sharkey031af8d2019-04-28 11:11:30 -06005430 } catch (Exception e) {
5431 Log.w(TAG, "Failed to update metadata for " + updatedUri, e);
5432 }
5433 }
5434 }
Jeff Sharkey291e7772019-01-03 14:16:33 -07005435 } finally {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06005436 restoreLocalCallingIdentity(token);
Jeff Sharkey0b801a52019-08-08 11:19:51 -06005437 Trace.endSection();
Jeff Sharkey291e7772019-01-03 14:16:33 -07005438 }
5439 }
5440
The Android Open Source Project70215272009-03-03 19:32:43 -08005441 return count;
5442 }
5443
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005444 /**
Sahana Rao34682a72020-05-04 02:02:37 +01005445 * Update row(s) that match {@code userWhere} in MediaProvider database with {@code values}.
5446 * Treats update as replace for updates with conflicts.
5447 */
5448 private int updateAllowingReplace(@NonNull SQLiteQueryBuilder qb,
5449 @NonNull DatabaseHelper helper, @NonNull ContentValues values, String userWhere,
5450 String[] userWhereArgs) throws SQLiteConstraintException {
5451 return helper.runWithTransaction((db) -> {
5452 try {
5453 return qb.update(helper, values, userWhere, userWhereArgs);
5454 } catch (SQLiteConstraintException e) {
5455 // b/155320967 Apps sometimes create a file via file path and then update another
5456 // explicitly inserted db row to this file. We have to resolve this update with a
5457 // replace.
5458
5459 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
5460 // We don't support replace for non-legacy apps. Non legacy apps should have
5461 // clearer interactions with MediaProvider.
5462 throw e;
5463 }
5464
5465 final String path = values.getAsString(FileColumns.DATA);
5466
5467 // We will only handle UNIQUE constraint error for FileColumns.DATA. We will not try
5468 // update and replace if no file exists for conflicting db row.
5469 if (path == null || !new File(path).exists()) {
5470 throw e;
5471 }
5472
shafik536982a2020-05-14 17:54:05 +01005473 final Uri uri = FileUtils.getContentUriForPath(path);
Sahana Rao34682a72020-05-04 02:02:37 +01005474 final boolean allowHidden = isCallingPackageAllowedHidden();
5475 // The db row which caused UNIQUE constraint error may not match all column values
5476 // of the given queryBuilder, hence using a generic queryBuilder with Files uri.
Sahana Raoea587fc2020-06-03 15:56:23 +01005477 Bundle extras = new Bundle();
5478 extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE);
5479 extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE);
Sahana Rao34682a72020-05-04 02:02:37 +01005480 final SQLiteQueryBuilder qbForReplace = getQueryBuilder(TYPE_DELETE,
Sahana Raoea587fc2020-06-03 15:56:23 +01005481 matchUri(uri, allowHidden), uri, extras, null);
Sahana Rao25db6492020-06-06 16:22:23 +01005482 final long rowId = getIdIfPathOwnedByPackages(qbForReplace, helper, path,
Sahana Rao0bbd3e12020-06-06 15:56:44 +01005483 getSharedPackages());
Sahana Rao34682a72020-05-04 02:02:37 +01005484
5485 if (rowId != -1 && qbForReplace.delete(helper, "_id=?",
5486 new String[] {Long.toString(rowId)}) == 1) {
5487 Log.i(TAG, "Retrying database update after deleting conflicting entry");
5488 return qb.update(helper, values, userWhere, userWhereArgs);
5489 }
5490 // Rethrow SQLiteConstraintException if app doesn't own the conflicting db row.
5491 throw e;
5492 }
5493 });
5494 }
5495
5496 /**
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005497 * Update the internal table of {@link MediaStore.Audio.Playlists.Members}
5498 * by parsing the playlist file on disk and resolving it against scanned
5499 * audio items.
5500 * <p>
5501 * When a playlist references a missing audio item, the associated
5502 * {@link Playlists.Members#PLAY_ORDER} is skipped, leaving a gap to ensure
5503 * that the playlist entry is retained to avoid user data loss.
5504 */
5505 private void resolvePlaylistMembers(@NonNull Uri playlistUri) {
5506 Trace.beginSection("resolvePlaylistMembers");
Marco Nelissenf5f9eca2009-12-09 09:26:15 -08005507 try {
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06005508 final DatabaseHelper helper;
5509 try {
5510 helper = getDatabaseForUri(playlistUri);
5511 } catch (VolumeNotFoundException e) {
5512 throw e.rethrowAsIllegalArgumentException();
5513 }
5514
5515 helper.runWithTransaction((db) -> {
5516 resolvePlaylistMembersInternal(playlistUri, db);
5517 return null;
5518 });
Marco Nelissenf5f9eca2009-12-09 09:26:15 -08005519 } finally {
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005520 Trace.endSection();
Marco Nelissenf5f9eca2009-12-09 09:26:15 -08005521 }
5522 }
5523
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06005524 private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri,
5525 @NonNull SQLiteDatabase db) {
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005526 try {
5527 // Refresh playlist members based on what we parse from disk
Jeff Sharkeyc22cfcf2020-04-02 17:41:13 -06005528 final String volumeName = getVolumeName(playlistUri);
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005529 final long playlistId = ContentUris.parseId(playlistUri);
5530 db.delete("audio_playlists_map", "playlist_id=" + playlistId, null);
5531
5532 final Path playlistPath = queryForDataFile(playlistUri, null).toPath();
5533 final Playlist playlist = new Playlist();
5534 playlist.read(playlistPath.toFile());
5535
5536 final List<Path> members = playlist.asList();
5537 for (int i = 0; i < members.size(); i++) {
Jeff Sharkeyc22cfcf2020-04-02 17:41:13 -06005538 try {
5539 final Path audioPath = playlistPath.getParent().resolve(members.get(i));
5540 final long audioId = queryForPlaylistMember(volumeName, audioPath);
5541
5542 final ContentValues values = new ContentValues();
5543 values.put(Playlists.Members.PLAY_ORDER, i + 1);
5544 values.put(Playlists.Members.PLAYLIST_ID, playlistId);
5545 values.put(Playlists.Members.AUDIO_ID, audioId);
5546 db.insert("audio_playlists_map", null, values);
5547 } catch (IOException e) {
5548 Log.w(TAG, "Failed to resolve playlist member", e);
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005549 }
5550 }
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005551 } catch (IOException e) {
5552 Log.w(TAG, "Failed to refresh playlist", e);
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005553 }
5554 }
5555
5556 /**
Jeff Sharkeyc22cfcf2020-04-02 17:41:13 -06005557 * Make two attempts to query this playlist member: first based on the exact
5558 * path, and if that fails, fall back to picking a single item matching the
5559 * display name. When there are multiple items with the same display name,
5560 * we can't resolve between them, and leave this member unresolved.
5561 */
5562 private long queryForPlaylistMember(@NonNull String volumeName, @NonNull Path path)
5563 throws IOException {
5564 final Uri audioUri = Audio.Media.getContentUri(volumeName);
5565 try (Cursor c = queryForSingleItem(audioUri,
5566 new String[] { BaseColumns._ID }, MediaColumns.DATA + "=?",
5567 new String[] { path.toFile().getCanonicalPath() }, null)) {
5568 return c.getLong(0);
5569 } catch (FileNotFoundException ignored) {
5570 }
5571 try (Cursor c = queryForSingleItem(audioUri,
5572 new String[] { BaseColumns._ID }, MediaColumns.DISPLAY_NAME + "=?",
5573 new String[] { path.toFile().getName() }, null)) {
5574 return c.getLong(0);
5575 } catch (FileNotFoundException ignored) {
5576 }
5577 throw new FileNotFoundException();
5578 }
5579
5580 /**
Jeff Sharkeyd6697822020-03-22 20:59:47 -06005581 * Add the given audio item to the given playlist. Defaults to adding at the
5582 * end of the playlist when no {@link Playlists.Members#PLAY_ORDER} is
5583 * defined.
5584 */
5585 private long addPlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values)
5586 throws FallbackException {
5587 final long audioId = values.getAsLong(Audio.Playlists.Members.AUDIO_ID);
5588 final Uri audioUri = Audio.Media.getContentUri(getVolumeName(playlistUri), audioId);
5589
5590 Integer playOrder = values.getAsInteger(Playlists.Members.PLAY_ORDER);
5591 playOrder = (playOrder != null) ? (playOrder - 1) : Integer.MAX_VALUE;
5592
5593 try {
5594 final File playlistFile = queryForDataFile(playlistUri, null);
5595 final File audioFile = queryForDataFile(audioUri, null);
5596
5597 final Playlist playlist = new Playlist();
5598 playlist.read(playlistFile);
5599 playOrder = playlist.add(playOrder,
5600 playlistFile.toPath().getParent().relativize(audioFile.toPath()));
5601 playlist.write(playlistFile);
5602
5603 resolvePlaylistMembers(playlistUri);
5604
5605 // Callers are interested in the actual ID we generated
5606 final Uri membersUri = Playlists.Members.getContentUri(
5607 getVolumeName(playlistUri), ContentUris.parseId(playlistUri));
5608 try (Cursor c = query(membersUri, new String[] { BaseColumns._ID },
5609 Playlists.Members.PLAY_ORDER + "=" + (playOrder + 1), null, null)) {
5610 c.moveToFirst();
5611 return c.getLong(0);
5612 }
5613 } catch (IOException e) {
5614 throw new FallbackException("Failed to update playlist", e,
5615 android.os.Build.VERSION_CODES.R);
5616 }
5617 }
5618
5619 /**
5620 * Move an audio item within the given playlist.
5621 */
5622 private int movePlaylistMembers(@NonNull Uri playlistUri, @NonNull ContentValues values,
5623 @NonNull Bundle queryArgs) throws FallbackException {
5624 final int fromIndex = resolvePlaylistIndex(playlistUri, queryArgs);
5625 final int toIndex = values.getAsInteger(Playlists.Members.PLAY_ORDER) - 1;
5626 if (fromIndex == -1) {
5627 throw new FallbackException("Failed to resolve playlist member " + queryArgs,
5628 android.os.Build.VERSION_CODES.R);
5629 }
5630 try {
5631 final File playlistFile = queryForDataFile(playlistUri, null);
5632
5633 final Playlist playlist = new Playlist();
5634 playlist.read(playlistFile);
5635 final int finalIndex = playlist.move(fromIndex, toIndex);
5636 playlist.write(playlistFile);
5637
5638 resolvePlaylistMembers(playlistUri);
5639 return finalIndex;
5640 } catch (IOException e) {
5641 throw new FallbackException("Failed to update playlist", e,
5642 android.os.Build.VERSION_CODES.R);
5643 }
5644 }
5645
5646 /**
5647 * Remove an audio item from the given playlist.
5648 */
5649 private int removePlaylistMembers(@NonNull Uri playlistUri, @NonNull Bundle queryArgs)
5650 throws FallbackException {
5651 final int index = resolvePlaylistIndex(playlistUri, queryArgs);
5652 try {
5653 final File playlistFile = queryForDataFile(playlistUri, null);
5654
5655 final Playlist playlist = new Playlist();
5656 playlist.read(playlistFile);
5657 final int count;
5658 if (index == -1) {
5659 count = playlist.asList().size();
5660 playlist.clear();
5661 } else {
5662 count = 1;
5663 playlist.remove(index);
5664 }
5665 playlist.write(playlistFile);
5666
5667 resolvePlaylistMembers(playlistUri);
5668 return count;
5669 } catch (IOException e) {
5670 throw new FallbackException("Failed to update playlist", e,
5671 android.os.Build.VERSION_CODES.R);
5672 }
5673 }
5674
5675 /**
5676 * Resolve query arguments that are designed to select a specific playlist
5677 * item using its {@link Playlists.Members#PLAY_ORDER}.
5678 */
5679 private int resolvePlaylistIndex(@NonNull Uri playlistUri, @NonNull Bundle queryArgs) {
5680 final Uri membersUri = Playlists.Members.getContentUri(
5681 getVolumeName(playlistUri), ContentUris.parseId(playlistUri));
5682
5683 final DatabaseHelper helper;
5684 final SQLiteQueryBuilder qb;
5685 try {
5686 helper = getDatabaseForUri(membersUri);
5687 qb = getQueryBuilder(TYPE_DELETE, AUDIO_PLAYLISTS_ID_MEMBERS,
5688 membersUri, queryArgs, null);
5689 } catch (VolumeNotFoundException ignored) {
5690 return -1;
5691 }
5692
5693 try (Cursor c = qb.query(helper,
5694 new String[] { Playlists.Members.PLAY_ORDER }, queryArgs, null)) {
5695 if ((c.getCount() == 1) && c.moveToFirst()) {
5696 return c.getInt(0) - 1;
5697 } else {
5698 return -1;
5699 }
5700 }
Winsonb653af22019-06-05 12:14:13 -07005701 }
The Android Open Source Project70215272009-03-03 19:32:43 -08005702
5703 @Override
Jeff Sharkey29421112018-07-27 20:56:44 -06005704 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
5705 return openFileCommon(uri, mode, null);
5706 }
Marco Nelissen71ece602009-06-16 12:45:10 -07005707
Jeff Sharkey29421112018-07-27 20:56:44 -06005708 @Override
5709 public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal)
5710 throws FileNotFoundException {
5711 return openFileCommon(uri, mode, signal);
5712 }
5713
5714 private ParcelFileDescriptor openFileCommon(Uri uri, String mode, CancellationSignal signal)
5715 throws FileNotFoundException {
Marco Nelissen01e706a2013-09-12 15:38:42 -07005716 uri = safeUncanonicalize(uri);
Jeff Sharkey29421112018-07-27 20:56:44 -06005717
Jeff Sharkey94461582018-07-12 14:34:47 -06005718 final boolean allowHidden = isCallingPackageAllowedHidden();
5719 final int match = matchUri(uri, allowHidden);
Jeff Sharkey2b2667b2019-01-21 15:30:12 -07005720 final String volumeName = getVolumeName(uri);
Jeff Sharkey94461582018-07-12 14:34:47 -06005721
Jeff Sharkeybeeca1e2019-01-24 15:57:08 -07005722 // Handle some legacy cases where we need to redirect thumbnails
5723 switch (match) {
5724 case AUDIO_ALBUMART_ID: {
Jeff Sharkey52e4f062019-04-16 11:43:18 -06005725 final long albumId = Long.parseLong(uri.getPathSegments().get(3));
5726 final Uri targetUri = ContentUris
5727 .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId);
Jeff Sharkeycce43112020-02-09 18:07:36 -07005728 return ensureThumbnail(targetUri, signal);
The Android Open Source Project70215272009-03-03 19:32:43 -08005729 }
Jeff Sharkeybeeca1e2019-01-24 15:57:08 -07005730 case AUDIO_ALBUMART_FILE_ID: {
Jeff Sharkeybeeca1e2019-01-24 15:57:08 -07005731 final long audioId = Long.parseLong(uri.getPathSegments().get(3));
Jeff Sharkeyd2568872019-02-09 13:49:05 -07005732 final Uri targetUri = ContentUris
5733 .withAppendedId(Audio.Media.getContentUri(volumeName), audioId);
Jeff Sharkeycce43112020-02-09 18:07:36 -07005734 return ensureThumbnail(targetUri, signal);
Jeff Sharkeyd2568872019-02-09 13:49:05 -07005735 }
5736 case VIDEO_MEDIA_ID_THUMBNAIL: {
5737 final long videoId = Long.parseLong(uri.getPathSegments().get(3));
5738 final Uri targetUri = ContentUris
5739 .withAppendedId(Video.Media.getContentUri(volumeName), videoId);
Jeff Sharkeycce43112020-02-09 18:07:36 -07005740 return ensureThumbnail(targetUri, signal);
Jeff Sharkeyd2568872019-02-09 13:49:05 -07005741 }
5742 case IMAGES_MEDIA_ID_THUMBNAIL: {
5743 final long imageId = Long.parseLong(uri.getPathSegments().get(3));
5744 final Uri targetUri = ContentUris
5745 .withAppendedId(Images.Media.getContentUri(volumeName), imageId);
Jeff Sharkeycce43112020-02-09 18:07:36 -07005746 return ensureThumbnail(targetUri, signal);
Marco Nelissen71ece602009-06-16 12:45:10 -07005747 }
The Android Open Source Project70215272009-03-03 19:32:43 -08005748 }
Jeff Sharkeybeeca1e2019-01-24 15:57:08 -07005749
5750 return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal);
The Android Open Source Project70215272009-03-03 19:32:43 -08005751 }
5752
Jeff Sharkey29421112018-07-27 20:56:44 -06005753 @Override
5754 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)
5755 throws FileNotFoundException {
5756 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, null);
5757 }
5758
5759 @Override
5760 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts,
5761 CancellationSignal signal) throws FileNotFoundException {
5762 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, signal);
Jeff Sharkey29421112018-07-27 20:56:44 -06005763 }
5764
5765 private AssetFileDescriptor openTypedAssetFileCommon(Uri uri, String mimeTypeFilter,
5766 Bundle opts, CancellationSignal signal) throws FileNotFoundException {
5767 uri = safeUncanonicalize(uri);
5768
Jeff Sharkeybac84e22018-12-20 15:11:17 -07005769 // TODO: enforce that caller has access to this uri
5770
Jeff Sharkey29421112018-07-27 20:56:44 -06005771 // Offer thumbnail of media, when requested
5772 final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE)
Jeff Sharkeyc4a5f812020-05-03 21:07:14 -06005773 && MimeUtils.startsWithIgnoreCase(mimeTypeFilter, "image/");
Jeff Sharkey29421112018-07-27 20:56:44 -06005774 if (wantsThumb) {
Jeff Sharkeycce43112020-02-09 18:07:36 -07005775 final ParcelFileDescriptor pfd = ensureThumbnail(uri, signal);
5776 return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH);
Jeff Sharkey29421112018-07-27 20:56:44 -06005777 }
5778
5779 // Worst case, return the underlying file
5780 return new AssetFileDescriptor(openFileCommon(uri, "r", signal), 0,
5781 AssetFileDescriptor.UNKNOWN_LENGTH);
5782 }
5783
Jeff Sharkeycce43112020-02-09 18:07:36 -07005784 private ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal)
5785 throws FileNotFoundException {
Jeff Sharkeyd2568872019-02-09 13:49:05 -07005786 final boolean allowHidden = isCallingPackageAllowedHidden();
5787 final int match = matchUri(uri, allowHidden);
5788
Jeff Sharkey0b801a52019-08-08 11:19:51 -06005789 Trace.beginSection("ensureThumbnail");
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06005790 final LocalCallingIdentity token = clearLocalCallingIdentity();
Jeff Sharkeybeeca1e2019-01-24 15:57:08 -07005791 try {
Jeff Sharkeyd2568872019-02-09 13:49:05 -07005792 switch (match) {
Jeff Sharkey52e4f062019-04-16 11:43:18 -06005793 case AUDIO_ALBUMS_ID: {
5794 final String volumeName = MediaStore.getVolumeName(uri);
5795 final Uri baseUri = MediaStore.Audio.Media.getContentUri(volumeName);
5796 final long albumId = ContentUris.parseId(uri);
5797 try (Cursor c = query(baseUri, new String[] { MediaStore.Audio.Media._ID },
5798 MediaStore.Audio.Media.ALBUM_ID + "=" + albumId, null, null, signal)) {
5799 if (c.moveToFirst()) {
5800 final long audioId = c.getLong(0);
5801 final Uri targetUri = ContentUris.withAppendedId(baseUri, audioId);
5802 return mAudioThumbnailer.ensureThumbnail(targetUri, signal);
5803 } else {
5804 throw new FileNotFoundException("No media for album " + uri);
5805 }
5806 }
5807 }
Jeff Sharkeyd2568872019-02-09 13:49:05 -07005808 case AUDIO_MEDIA_ID:
5809 return mAudioThumbnailer.ensureThumbnail(uri, signal);
5810 case VIDEO_MEDIA_ID:
5811 return mVideoThumbnailer.ensureThumbnail(uri, signal);
5812 case IMAGES_MEDIA_ID:
5813 return mImageThumbnailer.ensureThumbnail(uri, signal);
Jeff Sharkeycea04d52020-01-06 15:08:39 -07005814 case FILES_ID:
5815 case DOWNLOADS_ID: {
5816 // When item is referenced in a generic way, resolve to actual type
Jeff Sharkeyc4a5f812020-05-03 21:07:14 -06005817 final int mediaType = MimeUtils.resolveMediaType(getType(uri));
5818 switch (mediaType) {
Jeff Sharkeycea04d52020-01-06 15:08:39 -07005819 case FileColumns.MEDIA_TYPE_AUDIO:
5820 return mAudioThumbnailer.ensureThumbnail(uri, signal);
5821 case FileColumns.MEDIA_TYPE_VIDEO:
5822 return mVideoThumbnailer.ensureThumbnail(uri, signal);
5823 case FileColumns.MEDIA_TYPE_IMAGE:
5824 return mImageThumbnailer.ensureThumbnail(uri, signal);
5825 default:
5826 throw new FileNotFoundException();
5827 }
5828 }
Jeff Sharkeyd2568872019-02-09 13:49:05 -07005829 default:
5830 throw new FileNotFoundException();
5831 }
Jeff Sharkeybeeca1e2019-01-24 15:57:08 -07005832 } catch (IOException e) {
5833 Log.w(TAG, e);
5834 throw new FileNotFoundException(e.getMessage());
5835 } finally {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06005836 restoreLocalCallingIdentity(token);
Jeff Sharkey0b801a52019-08-08 11:19:51 -06005837 Trace.endSection();
Jeff Sharkeybeeca1e2019-01-24 15:57:08 -07005838 }
5839 }
5840
Jeff Sharkey29421112018-07-27 20:56:44 -06005841 /**
5842 * Update the metadata columns for the image residing at given {@link Uri}
5843 * by reading data from the underlying image.
5844 */
Jeff Sharkey4b1921d2018-12-11 12:24:46 -07005845 private void updateImageMetadata(ContentValues values, File file) {
5846 final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options();
5847 bitmapOpts.inJustDecodeBounds = true;
5848 BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOpts);
Jeff Sharkey29421112018-07-27 20:56:44 -06005849
Jeff Sharkey4b1921d2018-12-11 12:24:46 -07005850 values.put(MediaColumns.WIDTH, bitmapOpts.outWidth);
5851 values.put(MediaColumns.HEIGHT, bitmapOpts.outHeight);
Jeff Sharkey29421112018-07-27 20:56:44 -06005852 }
5853
Sahana Raoa211c4e2020-03-23 02:59:33 +00005854 private void handleInsertedRowForFuse(long rowId) {
5855 if (isFuseThread()) {
5856 // Removes restored row ID saved list.
5857 mCallingIdentity.get().removeDeletedRowId(rowId);
5858 }
5859 }
5860
5861 private void handleUpdatedRowForFuse(@NonNull String oldPath, @NonNull String ownerPackage,
5862 long oldRowId, long newRowId) {
5863 if (oldRowId == newRowId) {
5864 // Update didn't delete or add row ID. We don't need to save row ID or remove saved
5865 // deleted ID.
5866 return;
5867 }
5868
5869 handleDeletedRowForFuse(oldPath, ownerPackage, oldRowId);
5870 handleInsertedRowForFuse(newRowId);
5871 }
5872
5873 private void handleDeletedRowForFuse(@NonNull String path, @NonNull String ownerPackage,
5874 long rowId) {
5875 if (!isFuseThread()) {
5876 return;
5877 }
5878
5879 // Invalidate saved owned ID's of the previous owner of the deleted path, this prevents old
5880 // owner from gaining access to newly created file with restored row ID.
5881 if (!ownerPackage.equals("null") && !ownerPackage.equals(getCallingPackageOrSelf())) {
5882 invalidateLocalCallingIdentityCache(ownerPackage, "owned_database_row_deleted:"
5883 + path);
5884 }
5885 // Saves row ID corresponding to deleted path. Saved row ID will be restored on subsequent
5886 // create or rename.
5887 mCallingIdentity.get().addDeletedRowId(path, rowId);
5888 }
5889
Sahana Rao1e8271b2020-04-03 14:01:08 +01005890 private void handleOwnerPackageNameChange(@NonNull String oldPath,
5891 @NonNull String oldOwnerPackage, @NonNull String newOwnerPackage) {
5892 if (Objects.equals(oldOwnerPackage, newOwnerPackage)) {
5893 return;
5894 }
5895 // Invalidate saved owned ID's of the previous owner of the renamed path, this prevents old
5896 // owner from gaining access to replaced file.
5897 invalidateLocalCallingIdentityCache(oldOwnerPackage, "owner_package_changed:" + oldPath);
5898 }
5899
Jeff Sharkey007645e2012-03-08 17:45:12 -08005900 /**
5901 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}.
5902 */
Jeff Sharkey29421112018-07-27 20:56:44 -06005903 File queryForDataFile(Uri uri, CancellationSignal signal)
5904 throws FileNotFoundException {
5905 return queryForDataFile(uri, null, null, signal);
5906 }
5907
5908 /**
5909 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}.
5910 */
5911 File queryForDataFile(Uri uri, String selection, String[] selectionArgs,
5912 CancellationSignal signal) throws FileNotFoundException {
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07005913 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns.DATA },
5914 selection, selectionArgs, signal)) {
5915 final String data = cursor.getString(0);
5916 if (TextUtils.isEmpty(data)) {
5917 throw new FileNotFoundException("Missing path for " + uri);
5918 } else {
5919 return new File(data);
Jeff Sharkey007645e2012-03-08 17:45:12 -08005920 }
Jeff Sharkey007645e2012-03-08 17:45:12 -08005921 }
5922 }
5923
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07005924 /**
5925 * Return the {@link Uri} for the given {@code File}.
5926 */
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06005927 Uri queryForMediaUri(File file, CancellationSignal signal) throws FileNotFoundException {
Jeff Sharkeyc5c39142019-12-15 22:46:03 -07005928 final String volumeName = FileUtils.getVolumeName(getContext(), file);
Jeff Sharkey458e40d2019-04-14 11:13:56 -06005929 final Uri uri = Files.getContentUri(volumeName);
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07005930 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns._ID },
5931 MediaColumns.DATA + "=?", new String[] { file.getAbsolutePath() }, signal)) {
5932 return ContentUris.withAppendedId(uri, cursor.getLong(0));
5933 }
5934 }
5935
5936 /**
5937 * Query the given {@link Uri}, expecting only a single item to be found.
5938 *
5939 * @throws FileNotFoundException if no items were found, or multiple items
5940 * were found, or there was trouble reading the data.
5941 */
5942 Cursor queryForSingleItem(Uri uri, String[] projection, String selection,
5943 String[] selectionArgs, CancellationSignal signal) throws FileNotFoundException {
5944 final Cursor c = query(uri, projection,
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -07005945 DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null), signal);
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07005946 if (c == null) {
5947 throw new FileNotFoundException("Missing cursor for " + uri);
5948 } else if (c.getCount() < 1) {
Jeff Sharkeyb6781bc2019-07-18 18:45:52 -06005949 FileUtils.closeQuietly(c);
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07005950 throw new FileNotFoundException("No item at " + uri);
5951 } else if (c.getCount() > 1) {
Jeff Sharkeyb6781bc2019-07-18 18:45:52 -06005952 FileUtils.closeQuietly(c);
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07005953 throw new FileNotFoundException("Multiple items at " + uri);
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06005954 }
5955
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07005956 if (c.moveToFirst()) {
5957 return c;
5958 } else {
Jeff Sharkeyb6781bc2019-07-18 18:45:52 -06005959 FileUtils.closeQuietly(c);
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07005960 throw new FileNotFoundException("Failed to read row from " + uri);
Jeff Sharkeya17c1ee2018-10-24 19:26:19 -06005961 }
5962 }
5963
Jeff Sharkey007645e2012-03-08 17:45:12 -08005964 /**
shafik15e2d612019-10-31 20:10:25 +00005965 * Compares {@code itemOwner} with package name of {@link LocalCallingIdentity} and throws
5966 * {@link IllegalStateException} if it doesn't match.
5967 * Make sure to set calling identity properly before calling.
5968 */
5969 private void requireOwnershipForItem(@Nullable String itemOwner, Uri item) {
5970 final boolean hasOwner = (itemOwner != null);
5971 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), itemOwner);
5972 if (hasOwner && !callerIsOwner) {
5973 throw new IllegalStateException(
5974 "Only owner is able to interact with pending item " + item);
5975 }
5976 }
5977
Zimedbe69e2019-12-13 18:49:36 +00005978 private File getFuseFile(File file) {
5979 String filePath = file.getPath().replaceFirst(
5980 "/storage/", "/mnt/user/" + UserHandle.myUserId() + "/");
5981 return new File(filePath);
5982 }
5983
Jeff Sharkeyf06febd2020-04-07 13:03:30 -06005984 private @NonNull FuseDaemon getFuseDaemonForFile(@NonNull File file)
Jeff Sharkey564929d2020-04-06 16:51:58 -06005985 throws FileNotFoundException {
5986 final FuseDaemon daemon = ExternalStorageServiceImpl.getFuseDaemon(getVolumeId(file));
5987 if (daemon == null) {
5988 throw new FileNotFoundException("Missing FUSE daemon for " + file);
5989 } else {
5990 return daemon;
Zima76c3492020-02-19 01:23:26 +00005991 }
Zima76c3492020-02-19 01:23:26 +00005992 }
5993
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07005994 private void invalidateFuseDentry(@NonNull File file) {
5995 invalidateFuseDentry(file.getAbsolutePath());
5996 }
5997
5998 private void invalidateFuseDentry(@NonNull String path) {
Jeff Sharkey564929d2020-04-06 16:51:58 -06005999 try {
6000 final FuseDaemon daemon = getFuseDaemonForFile(new File(path));
Sahana Raoc22c85a2020-03-16 10:23:48 +00006001 if (isFuseThread()) {
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07006002 // If we are on a FUSE thread, we don't need to invalidate,
6003 // (and *must* not, otherwise we'd crash) because the invalidation
6004 // is already reflected in the lower filesystem
6005 return;
Jeff Sharkey564929d2020-04-06 16:51:58 -06006006 } else {
6007 daemon.invalidateFuseDentryCache(path);
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07006008 }
Jeff Sharkey564929d2020-04-06 16:51:58 -06006009 } catch (FileNotFoundException e) {
6010 Log.w(TAG, "Failed to invalidate FUSE dentry", e);
Zim7e50bbc2020-03-06 13:50:45 +00006011 }
6012 }
6013
shafik15e2d612019-10-31 20:10:25 +00006014 /**
Jeff Sharkey007645e2012-03-08 17:45:12 -08006015 * Replacement for {@link #openFileHelper(Uri, String)} which enforces any
6016 * permissions applicable to the path before returning.
Nandana Dutt4f5e15a2019-11-29 10:45:58 +00006017 *
6018 * <p>This function should never be called from the fuse thread since it tries to open
6019 * a "/mnt/user" path.
Jeff Sharkey007645e2012-03-08 17:45:12 -08006020 */
Jeff Sharkey4b1921d2018-12-11 12:24:46 -07006021 private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match,
6022 String mode, CancellationSignal signal) throws FileNotFoundException {
Zim7e830682020-06-09 11:20:22 +01006023 int modeBits = ParcelFileDescriptor.parseMode(mode);
6024 boolean forWrite = (modeBits & ParcelFileDescriptor.MODE_WRITE_ONLY) != 0;
6025 if (forWrite) {
6026 // Upgrade 'w' only to 'rw'. This allows us acquire a WR_LOCK when calling
6027 // #shouldOpenWithFuse
6028 modeBits |= ParcelFileDescriptor.MODE_READ_WRITE;
6029 }
Jeff Sharkey007645e2012-03-08 17:45:12 -08006030
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006031 final boolean hasOwnerPackageName = hasOwnerPackageName(uri);
Jeff Sharkeydceceae2019-03-08 18:01:18 -07006032 final String[] projection = new String[] {
6033 MediaColumns.DATA,
6034 hasOwnerPackageName ? MediaColumns.OWNER_PACKAGE_NAME : "NULL",
6035 hasOwnerPackageName ? MediaColumns.IS_PENDING : "0",
6036 };
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006037
Jeff Sharkey0218c142018-10-19 15:37:00 -06006038 final File file;
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006039 final String ownerPackageName;
Jeff Sharkeydceceae2019-03-08 18:01:18 -07006040 final boolean isPending;
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06006041 final LocalCallingIdentity token = clearLocalCallingIdentity();
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006042 try (Cursor c = queryForSingleItem(uri, projection, null, null, signal)) {
6043 final String data = c.getString(0);
6044 if (TextUtils.isEmpty(data)) {
6045 throw new FileNotFoundException("Missing path for " + uri);
6046 } else {
Jeff Sharkeyb057c7b2018-12-05 19:18:23 -07006047 file = new File(data).getCanonicalFile();
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006048 }
6049 ownerPackageName = c.getString(1);
Jeff Sharkeydceceae2019-03-08 18:01:18 -07006050 isPending = c.getInt(2) != 0;
Jeff Sharkeyb057c7b2018-12-05 19:18:23 -07006051 } catch (IOException e) {
6052 throw new FileNotFoundException(e.toString());
Jeff Sharkey0218c142018-10-19 15:37:00 -06006053 } finally {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06006054 restoreLocalCallingIdentity(token);
Jeff Sharkey0218c142018-10-19 15:37:00 -06006055 }
Marco Nelissenb2c36952013-08-28 12:19:49 -07006056
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07006057 checkAccess(uri, Bundle.EMPTY, file, forWrite);
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06006058
Sahana Raob02e7152020-06-12 17:07:31 +01006059 // We don't check ownership for files with IS_PENDING set by FUSE
6060 if (isPending && !isPendingFromFuse(file)) {
shafik15e2d612019-10-31 20:10:25 +00006061 requireOwnershipForItem(ownerPackageName, uri);
Jeff Sharkeydceceae2019-03-08 18:01:18 -07006062 }
6063
shafik15e2d612019-10-31 20:10:25 +00006064 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName);
Jeff Sharkeydceceae2019-03-08 18:01:18 -07006065 // Figure out if we need to redact contents
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006066 final boolean redactionNeeded = callerIsOwner ? false : isRedactionNeeded(uri);
shafika2ae9072019-10-28 12:16:00 +00006067 final RedactionInfo redactionInfo;
6068 try {
6069 redactionInfo = redactionNeeded ? getRedactionRanges(file)
6070 : new RedactionInfo(new long[0], new long[0]);
6071 } catch(IOException e) {
6072 throw new IllegalStateException(e);
6073 }
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006074
6075 // Yell if caller requires original, since we can't give it to them
6076 // unless they have access granted above
Jeff Sharkey7ea24f22019-08-22 10:14:18 -06006077 if (redactionNeeded && MediaStore.getRequireOriginal(uri)) {
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006078 throw new UnsupportedOperationException(
6079 "Caller must hold ACCESS_MEDIA_LOCATION permission to access original");
6080 }
6081
Jeff Sharkey4b1921d2018-12-11 12:24:46 -07006082 // Kick off metadata update when writing is finished
6083 final OnCloseListener listener = (e) -> {
Jeff Sharkeybac84e22018-12-20 15:11:17 -07006084 // We always update metadata to reflect the state on disk, even when
6085 // the remote writer tried claiming an exception
Jeff Sharkey7d48f8a2018-12-19 14:52:33 -07006086 invalidateThumbnails(uri);
6087
Jeff Sharkeybac84e22018-12-20 15:11:17 -07006088 try {
6089 switch (match) {
6090 case IMAGES_THUMBNAILS_ID:
6091 case VIDEO_THUMBNAILS_ID:
6092 final ContentValues values = new ContentValues();
6093 updateImageMetadata(values, file);
6094 update(uri, values, null, null);
6095 break;
6096 default:
Jeff Sharkey3c0a6c62019-11-15 20:45:41 -07006097 mMediaScanner.scanFile(file, REASON_DEMAND);
Jeff Sharkeybac84e22018-12-20 15:11:17 -07006098 break;
Jeff Sharkey4b1921d2018-12-11 12:24:46 -07006099 }
Jeff Sharkeybac84e22018-12-20 15:11:17 -07006100 } catch (Exception e2) {
6101 Log.w(TAG, "Failed to update metadata for " + uri, e2);
Jeff Sharkey4b1921d2018-12-11 12:24:46 -07006102 }
6103 };
6104
Jeff Sharkey29421112018-07-27 20:56:44 -06006105 try {
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006106 // First, handle any redaction that is needed for caller
6107 final ParcelFileDescriptor pfd;
Zimedbe69e2019-12-13 18:49:36 +00006108 final String filePath = file.getPath();
Andrew Lewis360b9d32019-07-10 13:11:50 +01006109 if (redactionInfo.redactionRanges.length > 0) {
Nandana Dutt4f5e15a2019-11-29 10:45:58 +00006110 if (SystemProperties.getBoolean(PROP_FUSE, false)) {
6111 // If fuse is enabled, we can provide an fd that points to the fuse
6112 // file system and handle redaction in the fuse handler when the caller reads.
Zimedbe69e2019-12-13 18:49:36 +00006113 Log.i(TAG, "Redacting with new FUSE for " + filePath);
Zim7d249ef2020-05-26 13:55:56 +01006114 long tid = android.os.Process.myTid();
6115 synchronized (mShouldRedactThreadIds) {
6116 mShouldRedactThreadIds.add(tid);
6117 }
6118 try {
6119 pfd = FileUtils.openSafely(getFuseFile(file), modeBits);
6120 } finally {
6121 synchronized (mShouldRedactThreadIds) {
6122 mShouldRedactThreadIds.remove(mShouldRedactThreadIds.indexOf(tid));
6123 }
6124 }
Nandana Dutt4f5e15a2019-11-29 10:45:58 +00006125 } else {
6126 // TODO(b/135341978): Remove this and associated code
6127 // when fuse is on by default.
Zimedbe69e2019-12-13 18:49:36 +00006128 Log.i(TAG, "Redacting with old FUSE for " + filePath);
Nandana Dutt4f5e15a2019-11-29 10:45:58 +00006129 pfd = RedactingFileDescriptor.open(
6130 getContext(),
6131 file,
6132 modeBits,
6133 redactionInfo.redactionRanges,
6134 redactionInfo.freeOffsets);
6135 }
Jeff Sharkey29421112018-07-27 20:56:44 -06006136 } else {
Jeff Sharkeyf06febd2020-04-07 13:03:30 -06006137 FuseDaemon daemon = null;
6138 try {
6139 daemon = getFuseDaemonForFile(file);
6140 } catch (FileNotFoundException ignored) {
6141 }
Jeff Sharkey9a497642020-04-23 13:15:10 -06006142 ParcelFileDescriptor lowerFsFd = FileUtils.openSafely(file, modeBits);
Zim7e830682020-06-09 11:20:22 +01006143 // Always acquire a readLock. This allows us make multiple opens via lower
6144 // filesystem
Zimedbe69e2019-12-13 18:49:36 +00006145 boolean shouldOpenWithFuse = daemon != null
Zim7e830682020-06-09 11:20:22 +01006146 && daemon.shouldOpenWithFuse(filePath, true /* forRead */, lowerFsFd.getFd());
Zimedbe69e2019-12-13 18:49:36 +00006147
6148 if (SystemProperties.getBoolean(PROP_FUSE, false) && shouldOpenWithFuse) {
6149 // If the file is already opened on the FUSE mount with VFS caching enabled
6150 // we return an upper filesystem fd (via FUSE) to avoid file corruption
6151 // resulting from cache inconsistencies between the upper and lower
6152 // filesystem caches
6153 Log.w(TAG, "Using FUSE for " + filePath);
Jeff Sharkey9a497642020-04-23 13:15:10 -06006154 pfd = FileUtils.openSafely(getFuseFile(file), modeBits);
Zimedbe69e2019-12-13 18:49:36 +00006155 try {
6156 lowerFsFd.close();
6157 } catch (IOException e) {
6158 Log.w(TAG, "Failed to close lower filesystem fd " + file.getPath(), e);
6159 }
6160 } else {
6161 Log.i(TAG, "Using lower FS for " + filePath);
Zim782cb072020-04-08 10:41:22 +01006162 if (forWrite) {
6163 // When opening for write on the lower filesystem, invalidate the VFS dentry
6164 // so subsequent open/getattr calls will return correctly.
6165 //
6166 // A 'dirty' dentry with write back cache enabled can cause the kernel to
6167 // ignore file attributes or even see stale page cache data when the lower
6168 // filesystem has been modified outside of the FUSE driver
6169 invalidateFuseDentry(file);
6170 }
6171
Zimedbe69e2019-12-13 18:49:36 +00006172 pfd = lowerFsFd;
6173 }
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006174 }
6175
6176 // Second, wrap in any listener that we've requested
Jeff Sharkey670198e2019-04-28 11:47:13 -06006177 if (!isPending && forWrite && listener != null) {
Jeff Sharkeye2750322020-01-07 22:06:24 -07006178 return ParcelFileDescriptor.wrap(pfd, BackgroundThread.getHandler(), listener);
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006179 } else {
6180 return pfd;
Jeff Sharkey29421112018-07-27 20:56:44 -06006181 }
6182 } catch (IOException e) {
6183 if (e instanceof FileNotFoundException) {
6184 throw (FileNotFoundException) e;
6185 } else {
6186 throw new IllegalStateException(e);
6187 }
6188 }
Marco Nelissenb2c36952013-08-28 12:19:49 -07006189 }
6190
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07006191 private void deleteAndInvalidate(@NonNull Path path) {
6192 deleteAndInvalidate(path.toFile());
6193 }
6194
6195 private void deleteAndInvalidate(@NonNull File file) {
6196 file.delete();
6197 invalidateFuseDentry(file);
6198 }
6199
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07006200 private void deleteIfAllowed(Uri uri, Bundle extras, String path) {
Marco Nelissenb2c36952013-08-28 12:19:49 -07006201 try {
Dipankar Bhardwaj043e4a32022-06-07 07:37:18 +00006202 final File file = new File(path).getCanonicalFile();
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07006203 checkAccess(uri, extras, file, true);
Jeff Sharkeybb4e5e62020-02-09 17:14:08 -07006204 deleteAndInvalidate(file);
Marco Nelissenb2c36952013-08-28 12:19:49 -07006205 } catch (Exception e) {
Jeff Sharkey29421112018-07-27 20:56:44 -06006206 Log.e(TAG, "Couldn't delete " + path, e);
Marco Nelissenb2c36952013-08-28 12:19:49 -07006207 }
6208 }
6209
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06006210 @Deprecated
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06006211 private boolean isPending(Uri uri) {
6212 final int match = matchUri(uri, true);
6213 switch (match) {
6214 case AUDIO_MEDIA_ID:
6215 case VIDEO_MEDIA_ID:
6216 case IMAGES_MEDIA_ID:
6217 try (Cursor c = queryForSingleItem(uri,
6218 new String[] { MediaColumns.IS_PENDING }, null, null, null)) {
6219 return (c.getInt(0) != 0);
6220 } catch (FileNotFoundException e) {
6221 throw new IllegalStateException(e);
6222 }
6223 default:
6224 return false;
6225 }
6226 }
6227
6228 @Deprecated
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006229 private boolean isRedactionNeeded(Uri uri) {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06006230 return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED);
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006231 }
6232
shafikc3f62672019-08-30 11:15:48 +01006233 private boolean isRedactionNeeded() {
6234 return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED);
6235 }
6236
shafik575d0742019-11-25 17:02:57 +00006237 private boolean isCallingPackageRequestingLegacy() {
6238 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_GRANTED);
6239 }
6240
shafik62cf1f62020-01-28 11:12:29 +00006241 private static int getFileMediaType(String path) {
6242 final File file = new File(path);
6243 final String mimeType = MimeUtils.resolveMimeType(file);
6244 return MimeUtils.resolveMediaType(mimeType);
6245 }
6246
Sahana Rao406cf6d2020-04-08 21:52:59 +01006247 private boolean canAccessMediaFile(String filePath, boolean allowLegacy) {
6248 if (!allowLegacy && isCallingPackageRequestingLegacy()) {
6249 return false;
6250 }
shafik62cf1f62020-01-28 11:12:29 +00006251 switch (getFileMediaType(filePath)) {
6252 case FileColumns.MEDIA_TYPE_IMAGE:
6253 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES);
6254 case FileColumns.MEDIA_TYPE_VIDEO:
6255 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO);
6256 default:
6257 return false;
6258 }
6259 }
6260
shafik575d0742019-11-25 17:02:57 +00006261 /**
shafik62cf1f62020-01-28 11:12:29 +00006262 * Returns true if:
6263 * <ul>
6264 * <li>the calling identity is an app targeting Q or older versions AND is requesting legacy
6265 * storage
6266 * <li>the calling identity holds {@code MANAGE_EXTERNAL_STORAGE}
Martijn Coenen9a1f6792020-03-17 07:13:47 +01006267 * <li>the calling identity owns filePath (eg /Android/data/com.foo)
shafik62cf1f62020-01-28 11:12:29 +00006268 * <li>the calling identity has permission to write images and the given file is an image file
6269 * <li>the calling identity has permission to write video and the given file is an video file
6270 * </ul>
shafik575d0742019-11-25 17:02:57 +00006271 */
shafik62cf1f62020-01-28 11:12:29 +00006272 private boolean shouldBypassFuseRestrictions(boolean forWrite, String filePath) {
shafik575d0742019-11-25 17:02:57 +00006273 boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite()
6274 : isCallingPackageLegacyRead();
Abhijeet Kaur7a469742020-02-19 11:51:53 +00006275 if (isRequestingLegacyStorage) {
shafikae5b3492020-01-10 15:55:51 +00006276 return true;
6277 }
shafik575d0742019-11-25 17:02:57 +00006278
Jeff Sharkey8411c402020-04-29 22:12:36 -06006279 if (isCallingPackageManager()) {
shafikae5b3492020-01-10 15:55:51 +00006280 return true;
6281 }
6282
Martijn Coenen9a1f6792020-03-17 07:13:47 +01006283 // Files under the apps own private directory
6284 final String appSpecificDir = extractPathOwnerPackageName(filePath);
6285 if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) {
6286 return true;
6287 }
6288
shafik62cf1f62020-01-28 11:12:29 +00006289 // Apps with write access to images and/or videos can bypass our restrictions if all of the
6290 // the files they're accessing are of the compatible media type.
Sahana Rao406cf6d2020-04-08 21:52:59 +01006291 if (canAccessMediaFile(filePath, /*allowLegacy*/ true)) {
shafik62cf1f62020-01-28 11:12:29 +00006292 return true;
6293 }
6294
shafikae5b3492020-01-10 15:55:51 +00006295 return false;
shafik575d0742019-11-25 17:02:57 +00006296 }
6297
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006298 /**
Martijn Coenen9a1f6792020-03-17 07:13:47 +01006299 * Returns true if the passed in path is an application-private data directory
6300 * (such as Android/data/com.foo or Android/obb/com.foo) that does not belong to the caller.
6301 */
6302 private boolean isPrivatePackagePathNotOwnedByCaller(String path) {
6303 // Files under the apps own private directory
6304 final String appSpecificDir = extractPathOwnerPackageName(path);
6305
6306 if (appSpecificDir == null) {
6307 return false;
6308 }
6309
6310 final String relativePath = extractRelativePath(path);
6311 // Android/media is not considered private, because it contains media that is explicitly
6312 // scanned and shared by other apps
6313 if (relativePath.startsWith("Android/media")) {
6314 return false;
6315 }
6316
6317 // This is a private-package path; return true if not owned by the caller
6318 return !isCallingIdentitySharedPackageName(appSpecificDir);
6319 }
6320
6321 /**
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006322 * Set of Exif tags that should be considered for redaction.
6323 */
Jeff Sharkey60ca2982019-05-11 13:44:09 -06006324 private static final String[] REDACTED_EXIF_TAGS = new String[] {
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006325 ExifInterface.TAG_GPS_ALTITUDE,
6326 ExifInterface.TAG_GPS_ALTITUDE_REF,
6327 ExifInterface.TAG_GPS_AREA_INFORMATION,
6328 ExifInterface.TAG_GPS_DOP,
6329 ExifInterface.TAG_GPS_DATESTAMP,
6330 ExifInterface.TAG_GPS_DEST_BEARING,
6331 ExifInterface.TAG_GPS_DEST_BEARING_REF,
6332 ExifInterface.TAG_GPS_DEST_DISTANCE,
6333 ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
6334 ExifInterface.TAG_GPS_DEST_LATITUDE,
6335 ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
6336 ExifInterface.TAG_GPS_DEST_LONGITUDE,
6337 ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
6338 ExifInterface.TAG_GPS_DIFFERENTIAL,
6339 ExifInterface.TAG_GPS_IMG_DIRECTION,
6340 ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
6341 ExifInterface.TAG_GPS_LATITUDE,
6342 ExifInterface.TAG_GPS_LATITUDE_REF,
6343 ExifInterface.TAG_GPS_LONGITUDE,
6344 ExifInterface.TAG_GPS_LONGITUDE_REF,
6345 ExifInterface.TAG_GPS_MAP_DATUM,
6346 ExifInterface.TAG_GPS_MEASURE_MODE,
6347 ExifInterface.TAG_GPS_PROCESSING_METHOD,
6348 ExifInterface.TAG_GPS_SATELLITES,
6349 ExifInterface.TAG_GPS_SPEED,
6350 ExifInterface.TAG_GPS_SPEED_REF,
6351 ExifInterface.TAG_GPS_STATUS,
6352 ExifInterface.TAG_GPS_TIMESTAMP,
6353 ExifInterface.TAG_GPS_TRACK,
6354 ExifInterface.TAG_GPS_TRACK_REF,
6355 ExifInterface.TAG_GPS_VERSION_ID,
Jeff Sharkey60ca2982019-05-11 13:44:09 -06006356 };
6357
6358 /**
6359 * Set of ISO boxes that should be considered for redaction.
6360 */
6361 private static final int[] REDACTED_ISO_BOXES = new int[] {
6362 IsoInterface.BOX_LOCI,
6363 IsoInterface.BOX_XYZ,
6364 IsoInterface.BOX_GPS,
6365 IsoInterface.BOX_GPS0,
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006366 };
6367
Jeff Sharkey00434422020-02-09 12:57:58 -07006368 public static final Set<String> sRedactedExifTags = new ArraySet<>(
6369 Arrays.asList(REDACTED_EXIF_TAGS));
6370
Andrew Lewis360b9d32019-07-10 13:11:50 +01006371 private static final class RedactionInfo {
6372 public final long[] redactionRanges;
6373 public final long[] freeOffsets;
6374 public RedactionInfo(long[] redactionRanges, long[] freeOffsets) {
6375 this.redactionRanges = redactionRanges;
6376 this.freeOffsets = freeOffsets;
6377 }
6378 }
6379
shafikc3f62672019-08-30 11:15:48 +01006380 /**
shafika51f3ce2019-10-10 17:06:41 +01006381 * Calculates the ranges that need to be redacted for the given file and user that wants to
shafikc3f62672019-08-30 11:15:48 +01006382 * access the file.
6383 *
6384 * @param uid UID of the package wanting to access the file
shafik9dd60eb2019-11-12 20:28:53 +00006385 * @param path File path
Zim7d249ef2020-05-26 13:55:56 +01006386 * @param tid thread id making IO on the FUSE filesystem
shafika2ae9072019-10-28 12:16:00 +00006387 * @return Ranges that should be redacted.
6388 *
6389 * @throws IOException if an error occurs while calculating the redaction ranges
shafikc3f62672019-08-30 11:15:48 +01006390 *
6391 * Called from JNI in jni/MediaProviderWrapper.cpp
6392 */
shafika51f3ce2019-10-10 17:06:41 +01006393 @Keep
shafikc3f62672019-08-30 11:15:48 +01006394 @NonNull
Zim7d249ef2020-05-26 13:55:56 +01006395 public long[] getRedactionRangesForFuse(String path, int uid, int tid) throws IOException {
shafikc580b6d2019-12-10 18:45:17 +00006396 final File file = new File(path);
6397
6398 // When we're calculating redaction ranges for MediaProvider, it means we're actually
Zim7d249ef2020-05-26 13:55:56 +01006399 // calculating redaction ranges for another app that called to MediaProvider through Binder.
6400 // If the tid is in mShouldRedactThreadIds, we should redact, otherwise, we don't redact
shafikc580b6d2019-12-10 18:45:17 +00006401 if (uid == android.os.Process.myUid()) {
Zim7d249ef2020-05-26 13:55:56 +01006402 boolean shouldRedact = false;
6403 synchronized (mShouldRedactThreadIds) {
6404 shouldRedact = mShouldRedactThreadIds.indexOf(tid) != -1;
6405 }
6406 if (shouldRedact) {
6407 return getRedactionRanges(file).redactionRanges;
6408 } else {
6409 return new long[0];
6410 }
shafikc580b6d2019-12-10 18:45:17 +00006411 }
6412
shafikd84da092020-04-29 17:53:30 +01006413 final LocalCallingIdentity token =
6414 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
shafikc3f62672019-08-30 11:15:48 +01006415
6416 long[] res = new long[0];
6417 try {
shafik63abf8b2020-03-02 15:44:37 +00006418 if (!isRedactionNeeded()
6419 || shouldBypassFuseRestrictions(/*forWrite*/ false, path)) {
6420 return res;
6421 }
6422
shafik536982a2020-05-14 17:54:05 +01006423 final Uri contentUri = FileUtils.getContentUriForPath(path);
shafik6958fce2019-12-17 10:49:27 +00006424 final String[] projection = new String[]{
6425 MediaColumns.OWNER_PACKAGE_NAME, MediaColumns._ID };
6426 final String selection = MediaColumns.DATA + "=?";
6427 final String[] selectionArgs = new String[] { path };
6428 final String ownerPackageName;
6429 final Uri item;
6430 try (final Cursor c = queryForSingleItem(contentUri, projection, selection,
6431 selectionArgs, null)) {
6432 c.moveToFirst();
6433 ownerPackageName = c.getString(0);
6434 item = ContentUris.withAppendedId(contentUri, /*item id*/ c.getInt(1));
6435 } catch (FileNotFoundException e) {
6436 // Ideally, this shouldn't happen unless the file was deleted after we checked its
6437 // existence and before we get to the redaction logic here. In this case we throw
6438 // and fail the operation and FuseDaemon should handle this and fail the whole open
6439 // operation gracefully.
6440 throw new FileNotFoundException(
6441 path + " not found while calculating redaction ranges: " + e.getMessage());
6442 }
6443
6444 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(),
6445 ownerPackageName);
6446 final boolean callerHasUriPermission = getContext().checkUriPermission(
6447 item, mCallingIdentity.get().pid, mCallingIdentity.get().uid,
6448 Intent.FLAG_GRANT_WRITE_URI_PERMISSION) == PERMISSION_GRANTED;
6449
6450 if (!callerIsOwner && !callerHasUriPermission) {
shafika2ae9072019-10-28 12:16:00 +00006451 res = getRedactionRanges(file).redactionRanges;
shafikc3f62672019-08-30 11:15:48 +01006452 }
6453 } finally {
6454 restoreLocalCallingIdentity(token);
6455 }
6456 return res;
6457 }
6458
shafika2ae9072019-10-28 12:16:00 +00006459 /**
6460 * Calculates the ranges containing sensitive metadata that should be redacted if the caller
6461 * doesn't have the required permissions.
6462 *
6463 * @param file file to be redacted
6464 * @return the ranges to be redacted in a RedactionInfo object, could be empty redaction ranges
6465 * if there's sensitive metadata
6466 * @throws IOException if an IOException happens while calculating the redaction ranges
6467 */
Jeff Sharkeyf06febd2020-04-07 13:03:30 -06006468 @VisibleForTesting
6469 public static RedactionInfo getRedactionRanges(File file) throws IOException {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06006470 Trace.beginSection("getRedactionRanges");
Jeff Sharkey60ca2982019-05-11 13:44:09 -06006471 final LongArray res = new LongArray();
Andrew Lewis360b9d32019-07-10 13:11:50 +01006472 final LongArray freeOffsets = new LongArray();
shafika2ae9072019-10-28 12:16:00 +00006473 try (FileInputStream is = new FileInputStream(file)) {
Jeff Sharkey1f6ad1a2019-12-20 14:26:34 -07006474 final String mimeType = MimeUtils.resolveMimeType(file);
shafika2ae9072019-10-28 12:16:00 +00006475 if (ExifInterface.isSupportedMimeType(mimeType)) {
6476 final ExifInterface exif = new ExifInterface(is.getFD());
6477 for (String tag : REDACTED_EXIF_TAGS) {
6478 final long[] range = exif.getAttributeRange(tag);
6479 if (range != null) {
6480 res.add(range[0]);
6481 res.add(range[0] + range[1]);
6482 }
6483 }
6484 // Redact xmp where present
Jeff Sharkey00434422020-02-09 12:57:58 -07006485 final XmpInterface exifXmp = XmpInterface.fromContainer(exif);
shafika2ae9072019-10-28 12:16:00 +00006486 res.addAll(exifXmp.getRedactionRanges());
Jeff Sharkey60ca2982019-05-11 13:44:09 -06006487 }
shafikc3f62672019-08-30 11:15:48 +01006488
shafika2ae9072019-10-28 12:16:00 +00006489 if (IsoInterface.isSupportedMimeType(mimeType)) {
6490 final IsoInterface iso = IsoInterface.fromFileDescriptor(is.getFD());
6491 for (int box : REDACTED_ISO_BOXES) {
6492 final long[] ranges = iso.getBoxRanges(box);
6493 for (int i = 0; i < ranges.length; i += 2) {
6494 long boxTypeOffset = ranges[i] - 4;
6495 freeOffsets.add(boxTypeOffset);
6496 res.add(boxTypeOffset);
6497 res.add(ranges[i + 1]);
6498 }
6499 }
6500 // Redact xmp where present
Jeff Sharkey00434422020-02-09 12:57:58 -07006501 final XmpInterface isoXmp = XmpInterface.fromContainer(iso);
shafika2ae9072019-10-28 12:16:00 +00006502 res.addAll(isoXmp.getRedactionRanges());
shafikc3f62672019-08-30 11:15:48 +01006503 }
shafikb8496972019-11-28 16:32:59 +00006504 } catch (FileNotFoundException ignored) {
6505 // If file not found, then there's nothing to redact
shafika2ae9072019-10-28 12:16:00 +00006506 } catch (IOException e) {
6507 throw new IOException("Failed to redact " + file, e);
shafikc3f62672019-08-30 11:15:48 +01006508 }
Jeff Sharkey0b801a52019-08-08 11:19:51 -06006509 Trace.endSection();
Andrew Lewis360b9d32019-07-10 13:11:50 +01006510 return new RedactionInfo(res.toArray(), freeOffsets.toArray());
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07006511 }
6512
shafika51f3ce2019-10-10 17:06:41 +01006513 /**
Sahana Raob02e7152020-06-12 17:07:31 +01006514 * @return {@code true} if {@code file} is pending from FUSE, {@code false} otherwise.
6515 * Files pending from FUSE will not have pending file pattern.
6516 */
6517 private static boolean isPendingFromFuse(@NonNull File file) {
6518 final Matcher matcher =
6519 FileUtils.PATTERN_EXPIRES_FILE.matcher(extractDisplayName(file.getName()));
6520 return !matcher.matches();
6521 }
6522
6523 /**
shafik15e2d612019-10-31 20:10:25 +00006524 * Checks if the app identified by the given UID is allowed to open the given file for the given
6525 * access mode.
6526 *
6527 * @param path the path of the file to be opened
6528 * @param uid UID of the app requesting to open the file
6529 * @param forWrite specifies if the file is to be opened for write
Sahana Raoa91179d2020-06-10 22:31:18 +01006530 * @return 0 upon success. {@link OsConstants#EACCES} if the operation is illegal or not
6531 * permitted for the given {@code uid} or if the calling package is a legacy app that doesn't
6532 * have right storage permission.
shafik15e2d612019-10-31 20:10:25 +00006533 *
6534 * Called from JNI in jni/MediaProviderWrapper.cpp
6535 */
6536 @Keep
shafika2966a72019-12-12 13:02:43 +00006537 public int isOpenAllowedForFuse(String path, int uid, boolean forWrite) {
shafikd84da092020-04-29 17:53:30 +01006538 final LocalCallingIdentity token =
6539 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
Zim696dea42020-03-07 11:41:42 +00006540
shafik15e2d612019-10-31 20:10:25 +00006541 try {
Martijn Coenen9a1f6792020-03-17 07:13:47 +01006542 if (isPrivatePackagePathNotOwnedByCaller(path)) {
6543 Log.e(TAG, "Can't open a file in another app's external directory!");
6544 return OsConstants.ENOENT;
shafik15e2d612019-10-31 20:10:25 +00006545 }
shafikbba5b672019-11-15 16:52:51 +00006546
shafik63abf8b2020-03-02 15:44:37 +00006547 if (shouldBypassFuseRestrictions(forWrite, path)) {
6548 return 0;
6549 }
shafik575d0742019-11-25 17:02:57 +00006550 // Legacy apps that made is this far don't have the right storage permission and hence
6551 // are not allowed to access anything other than their external app directory
6552 if (isCallingPackageRequestingLegacy()) {
shafike4fb1462020-01-29 16:25:23 +00006553 return OsConstants.EACCES;
shafik575d0742019-11-25 17:02:57 +00006554 }
6555
shafik536982a2020-05-14 17:54:05 +01006556 final Uri contentUri = FileUtils.getContentUriForPath(path);
shafik15e2d612019-10-31 20:10:25 +00006557 final String[] projection = new String[]{
6558 MediaColumns._ID,
6559 MediaColumns.OWNER_PACKAGE_NAME,
6560 MediaColumns.IS_PENDING};
6561 final String selection = MediaColumns.DATA + "=?";
6562 final String[] selectionArgs = new String[] { path };
6563 final Uri fileUri;
Sahana Raoea587fc2020-06-03 15:56:23 +01006564 final boolean isPending;
shafik15e2d612019-10-31 20:10:25 +00006565 String ownerPackageName = null;
6566 try (final Cursor c = queryForSingleItem(contentUri, projection, selection,
6567 selectionArgs, null)) {
6568 fileUri = ContentUris.withAppendedId(contentUri, c.getInt(0));
6569 ownerPackageName = c.getString(1);
6570 isPending = c.getInt(2) != 0;
6571 }
6572
6573 final File file = new File(path);
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07006574 checkAccess(fileUri, Bundle.EMPTY, file, forWrite);
shafik15e2d612019-10-31 20:10:25 +00006575
Sahana Raob02e7152020-06-12 17:07:31 +01006576 // We don't check ownership for files with IS_PENDING set by FUSE
6577 if (isPending && !isPendingFromFuse(new File(path))) {
shafik15e2d612019-10-31 20:10:25 +00006578 requireOwnershipForItem(ownerPackageName, fileUri);
6579 }
6580 return 0;
6581 } catch (FileNotFoundException e) {
Sahana Raoa91179d2020-06-10 22:31:18 +01006582 // We are here because
6583 // * App doesn't have read permission to the requested path, hence queryForSingleItem
6584 // couldn't return a valid db row, or,
6585 // * There is no db row corresponding to the requested path, which is more unlikely.
6586 // In both of these cases, it means that app doesn't have access permission to the file.
shafik15e2d612019-10-31 20:10:25 +00006587 Log.e(TAG, "Couldn't find file: " + path);
Sahana Raoa91179d2020-06-10 22:31:18 +01006588 return OsConstants.EACCES;
shafik15e2d612019-10-31 20:10:25 +00006589 } catch (IllegalStateException | SecurityException e) {
6590 Log.e(TAG, "Permission to access file: " + path + " is denied");
Sahana Raoa91179d2020-06-10 22:31:18 +01006591 return OsConstants.EACCES;
shafik15e2d612019-10-31 20:10:25 +00006592 } finally {
6593 restoreLocalCallingIdentity(token);
6594 }
6595 }
6596
6597 /**
shafikbba5b672019-11-15 16:52:51 +00006598 * Returns {@code true} if {@link #mCallingIdentity#getSharedPackages(String)} contains the
6599 * given package name, {@code false} otherwise.
shafik9edfb142019-11-06 11:01:40 +00006600 * <p> Assumes that {@code mCallingIdentity} has been properly set to reflect the calling
6601 * package.
6602 */
shafikbba5b672019-11-15 16:52:51 +00006603 private boolean isCallingIdentitySharedPackageName(@NonNull String packageName) {
6604 for (String sharedPkgName : mCallingIdentity.get().getSharedPackageNames()) {
6605 if (packageName.toLowerCase(Locale.ROOT)
6606 .equals(sharedPkgName.toLowerCase(Locale.ROOT))) {
6607 return true;
shafik9edfb142019-11-06 11:01:40 +00006608 }
6609 }
shafikbba5b672019-11-15 16:52:51 +00006610 return false;
shafik9edfb142019-11-06 11:01:40 +00006611 }
6612
6613 /**
shafika51f3ce2019-10-10 17:06:41 +01006614 * @throws IllegalStateException if path is invalid or doesn't match a volume.
6615 */
6616 @NonNull
Jeff Sharkeyc5c39142019-12-15 22:46:03 -07006617 private Uri getContentUriForFile(@NonNull String filePath, @NonNull String mimeType) {
Sahana Rao97710472020-04-03 11:55:02 +01006618 final String volName = FileUtils.getVolumeName(getContext(), new File(filePath));
6619 Uri uri = Files.getContentUri(volName);
6620 final String topLevelDir = extractTopLevelDir(filePath);
shafika32e93c2019-11-01 12:17:34 +00006621 if (topLevelDir == null) {
6622 // If the file path doesn't match the external storage directory, we use the files URI
6623 // as default and let #insert enforce the restrictions
Sahana Rao97710472020-04-03 11:55:02 +01006624 return uri;
shafika51f3ce2019-10-10 17:06:41 +01006625 }
shafika32e93c2019-11-01 12:17:34 +00006626 switch (topLevelDir) {
shafika32e93c2019-11-01 12:17:34 +00006627 case DIRECTORY_PODCASTS:
6628 case DIRECTORY_RINGTONES:
6629 case DIRECTORY_ALARMS:
6630 case DIRECTORY_NOTIFICATIONS:
6631 case DIRECTORY_AUDIOBOOKS:
Sahana Rao97710472020-04-03 11:55:02 +01006632 uri = Audio.Media.getContentUri(volName);
6633 break;
6634 case DIRECTORY_MUSIC:
6635 if (MimeUtils.isPlaylistMimeType(mimeType)) {
6636 uri = Audio.Playlists.getContentUri(volName);
6637 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) {
6638 // Send Files uri for media type subtitle
6639 uri = Audio.Media.getContentUri(volName);
6640 }
Sahana Rao71693442019-11-13 13:48:07 +00006641 break;
shafika32e93c2019-11-01 12:17:34 +00006642 case DIRECTORY_MOVIES:
Sahana Rao97710472020-04-03 11:55:02 +01006643 if (MimeUtils.isPlaylistMimeType(mimeType)) {
6644 uri = Audio.Playlists.getContentUri(volName);
6645 } else if (!MimeUtils.isSubtitleMimeType(mimeType)) {
6646 // Send Files uri for media type subtitle
6647 uri = Video.Media.getContentUri(volName);
6648 }
Sahana Rao71693442019-11-13 13:48:07 +00006649 break;
shafika32e93c2019-11-01 12:17:34 +00006650 case DIRECTORY_DCIM:
shafikb95e0242020-02-12 17:41:32 +00006651 case DIRECTORY_PICTURES:
Sahana Rao97710472020-04-03 11:55:02 +01006652 if (MimeUtils.isImageMimeType(mimeType)) {
6653 uri = Images.Media.getContentUri(volName);
shafika32e93c2019-11-01 12:17:34 +00006654 } else {
Sahana Rao97710472020-04-03 11:55:02 +01006655 uri = Video.Media.getContentUri(volName);
shafika32e93c2019-11-01 12:17:34 +00006656 }
Sahana Rao71693442019-11-13 13:48:07 +00006657 break;
shafika32e93c2019-11-01 12:17:34 +00006658 case DIRECTORY_DOWNLOADS:
6659 case DIRECTORY_DOCUMENTS:
6660 break;
shafika51f3ce2019-10-10 17:06:41 +01006661 default:
shafika32e93c2019-11-01 12:17:34 +00006662 Log.w(TAG, "Forgot to handle a top level directory in getContentUriForFile?");
shafika51f3ce2019-10-10 17:06:41 +01006663 }
Sahana Rao97710472020-04-03 11:55:02 +01006664 return uri;
shafika51f3ce2019-10-10 17:06:41 +01006665 }
6666
Sahana Rao5b7a9bd2020-06-12 16:32:16 +01006667 private boolean fileExists(@NonNull String absolutePath) {
shafika51f3ce2019-10-10 17:06:41 +01006668 // We don't care about specific columns in the match,
6669 // we just want to check IF there's a match
6670 final String[] projection = {};
6671 final String selection = FileColumns.DATA + " = ?";
6672 final String[] selectionArgs = {absolutePath};
Sahana Rao5b7a9bd2020-06-12 16:32:16 +01006673 final Uri uri = FileUtils.getContentUriForPath(absolutePath);
shafika51f3ce2019-10-10 17:06:41 +01006674
Sahana Rao5b7a9bd2020-06-12 16:32:16 +01006675 final LocalCallingIdentity token = clearLocalCallingIdentity();
6676 try {
6677 try (final Cursor c = query(uri, projection, selection, selectionArgs, null)) {
6678 // Shouldn't return null
6679 return c.getCount() > 0;
6680 }
6681 } finally {
6682 clearLocalCallingIdentity(token);
shafika51f3ce2019-10-10 17:06:41 +01006683 }
6684 }
6685
Sahana Rao80ecfba2020-04-03 10:48:01 +01006686 private boolean isExternalMediaDirectory(@NonNull String path) {
6687 final String relativePath = extractRelativePath(path);
6688 if (relativePath != null) {
6689 return relativePath.startsWith("Android/media");
6690 }
6691 return false;
6692 }
6693
Sahana Raoad971572020-03-30 17:58:28 +01006694 private Uri insertFileForFuse(@NonNull String path, @NonNull Uri uri, @NonNull String mimeType,
6695 boolean useData) {
6696 ContentValues values = new ContentValues();
6697 values.put(FileColumns.OWNER_PACKAGE_NAME, getCallingPackageOrSelf());
6698 values.put(MediaColumns.MIME_TYPE, mimeType);
Sahana Raoea587fc2020-06-03 15:56:23 +01006699 values.put(FileColumns.IS_PENDING, 1);
Sahana Raoad971572020-03-30 17:58:28 +01006700
6701 if (useData) {
Sahana Rao6b7baf42020-04-17 20:42:23 +01006702 values.put(FileColumns.DATA, path);
Sahana Raoad971572020-03-30 17:58:28 +01006703 } else {
Sahana Rao6ff86492020-04-30 12:39:14 +01006704 values.put(FileColumns.VOLUME_NAME, extractVolumeName(path));
Sahana Raoad971572020-03-30 17:58:28 +01006705 values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path));
6706 values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path));
6707 }
6708 return insert(uri, values, Bundle.EMPTY);
6709 }
6710
shafika51f3ce2019-10-10 17:06:41 +01006711 /**
shafik9edfb142019-11-06 11:01:40 +00006712 * Enforces file creation restrictions (see return values) for the given file on behalf of the
6713 * app with the given {@code uid}. If the file is is added to the shared storage, creates a
6714 * database entry for it.
6715 * <p> Does NOT create file.
shafika51f3ce2019-10-10 17:06:41 +01006716 *
6717 * @param path the path of the file
6718 * @param uid UID of the app requesting to create the file
shafik9edfb142019-11-06 11:01:40 +00006719 * @return In case of success, 0. If the operation is illegal or not permitted, returns the
shafike4fb1462020-01-29 16:25:23 +00006720 * appropriate {@code errno} value:
shafika51f3ce2019-10-10 17:06:41 +01006721 * <ul>
shafikbba5b672019-11-15 16:52:51 +00006722 * <li>{@link OsConstants#ENOENT} if the app tries to create file in other app's external dir
6723 * <li>{@link OsConstants#EEXIST} if the file already exists
shafik575d0742019-11-25 17:02:57 +00006724 * <li>{@link OsConstants#EPERM} if the file type doesn't match the relative path, or if the
6725 * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission.
shafikbba5b672019-11-15 16:52:51 +00006726 * <li>{@link OsConstants#EIO} in case of any other I/O exception
shafika51f3ce2019-10-10 17:06:41 +01006727 * </ul>
6728 *
6729 * @throws IllegalStateException if given path is invalid.
6730 *
6731 * Called from JNI in jni/MediaProviderWrapper.cpp
6732 */
6733 @Keep
shafika2966a72019-12-12 13:02:43 +00006734 public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) {
shafika51f3ce2019-10-10 17:06:41 +01006735 final LocalCallingIdentity token =
shafikd84da092020-04-29 17:53:30 +01006736 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
Zim696dea42020-03-07 11:41:42 +00006737
shafika51f3ce2019-10-10 17:06:41 +01006738 try {
Martijn Coenen9a1f6792020-03-17 07:13:47 +01006739 if (isPrivatePackagePathNotOwnedByCaller(path)) {
6740 Log.e(TAG, "Can't create a file in another app's external directory");
6741 return OsConstants.ENOENT;
shafika51f3ce2019-10-10 17:06:41 +01006742 }
6743
Sahana Rao6b7baf42020-04-17 20:42:23 +01006744 if (!path.equals(getAbsoluteSanitizedPath(path))) {
6745 Log.e(TAG, "File name contains invalid characters");
6746 return OsConstants.EPERM;
6747 }
6748
Sahana Raoad971572020-03-30 17:58:28 +01006749 final String mimeType = MimeUtils.resolveMimeType(new File(path));
6750
shafik63abf8b2020-03-02 15:44:37 +00006751 if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) {
Narayan Kamath53a9ac32020-06-16 10:05:32 +01006752 final boolean callerRequestingLegacy = isCallingPackageRequestingLegacy();
Sahana Rao5b7a9bd2020-06-12 16:32:16 +01006753 if (!fileExists(path)) {
6754 // If app has already inserted the db row, inserting the row again might set
6755 // IS_PENDING=1. We shouldn't overwrite existing entry as part of FUSE
6756 // operation, hence, insert the db row only when it doesn't exist.
6757 try {
Narayan Kamath53a9ac32020-06-16 10:05:32 +01006758 insertFileForFuse(path, FileUtils.getContentUriForPath(path),
6759 mimeType, /*useData*/ callerRequestingLegacy);
Sahana Rao5b7a9bd2020-06-12 16:32:16 +01006760 } catch (Exception ignored) {
6761 }
Narayan Kamath53a9ac32020-06-16 10:05:32 +01006762 } else {
6763 // Upon creating a file via FUSE, if a row matching the path already exists
6764 // but a file doesn't exist on the filesystem, we transfer ownership to the
6765 // app attempting to create the file. If we don't update ownership, then the
6766 // app that inserted the original row may be able to observe the contents of
6767 // written file even though they don't hold the right permissions to do so.
6768 if (callerRequestingLegacy) {
6769 final String owner = getCallingPackageOrSelf();
6770 if (owner != null && !updateOwnerForPath(path, owner)) {
6771 return OsConstants.EPERM;
6772 }
6773 }
Sahana Rao5b7a9bd2020-06-12 16:32:16 +01006774 }
Narayan Kamath53a9ac32020-06-16 10:05:32 +01006775
shafik63abf8b2020-03-02 15:44:37 +00006776 return 0;
6777 }
Sahana Raoad971572020-03-30 17:58:28 +01006778
shafik63abf8b2020-03-02 15:44:37 +00006779 // Legacy apps that made is this far don't have the right storage permission and hence
6780 // are not allowed to access anything other than their external app directory
shafik575d0742019-11-25 17:02:57 +00006781 if (isCallingPackageRequestingLegacy()) {
shafike4fb1462020-01-29 16:25:23 +00006782 return OsConstants.EPERM;
shafik575d0742019-11-25 17:02:57 +00006783 }
6784
Sahana Rao5b7a9bd2020-06-12 16:32:16 +01006785 if (fileExists(path)) {
6786 // If the file already exists in the db, we shouldn't allow the file creation.
shafike4fb1462020-01-29 16:25:23 +00006787 return OsConstants.EEXIST;
shafika51f3ce2019-10-10 17:06:41 +01006788 }
6789
Sahana Rao5b7a9bd2020-06-12 16:32:16 +01006790 final Uri contentUri = getContentUriForFile(path, mimeType);
Sahana Raoad971572020-03-30 17:58:28 +01006791 final Uri item = insertFileForFuse(path, contentUri, mimeType, /*useData*/ false);
shafika51f3ce2019-10-10 17:06:41 +01006792 if (item == null) {
shafike4fb1462020-01-29 16:25:23 +00006793 return OsConstants.EPERM;
shafika51f3ce2019-10-10 17:06:41 +01006794 }
shafik9edfb142019-11-06 11:01:40 +00006795 return 0;
shafika51f3ce2019-10-10 17:06:41 +01006796 } catch (IllegalArgumentException e) {
shafik9edfb142019-11-06 11:01:40 +00006797 Log.e(TAG, "insertFileIfNecessary failed", e);
shafike4fb1462020-01-29 16:25:23 +00006798 return OsConstants.EPERM;
shafika51f3ce2019-10-10 17:06:41 +01006799 } finally {
6800 restoreLocalCallingIdentity(token);
6801 }
6802 }
6803
Narayan Kamath53a9ac32020-06-16 10:05:32 +01006804 private boolean updateOwnerForPath(@NonNull String path, @NonNull String newOwner) {
6805 final DatabaseHelper helper;
6806 try {
6807 helper = getDatabaseForUri(FileUtils.getContentUriForPath(path));
6808 } catch (VolumeNotFoundException e) {
6809 // Cannot happen, as this is a path that we already resolved.
6810 throw new AssertionError("Path must already be resolved", e);
6811 }
6812
6813 ContentValues values = new ContentValues(1);
6814 values.put(FileColumns.OWNER_PACKAGE_NAME, newOwner);
6815
6816 return helper.runWithoutTransaction((db) -> {
6817 return db.update("files", values, "_data=?", new String[] { path });
6818 }) == 1;
6819 }
6820
shafik575d0742019-11-25 17:02:57 +00006821 private static int deleteFileUnchecked(@NonNull String path) {
shafik0c0e0d72019-10-16 17:34:17 +01006822 final File toDelete = new File(path);
6823 if (toDelete.delete()) {
6824 return 0;
6825 } else {
shafike4fb1462020-01-29 16:25:23 +00006826 return OsConstants.ENOENT;
shafik0c0e0d72019-10-16 17:34:17 +01006827 }
6828 }
6829
6830 /**
shafik9dd60eb2019-11-12 20:28:53 +00006831 * Deletes file with the given {@code path} on behalf of the app with the given {@code uid}.
shafik0c0e0d72019-10-16 17:34:17 +01006832 * <p>Before deleting, checks if app has permissions to delete this file.
6833 *
6834 * @param path the path of the file
6835 * @param uid UID of the app requesting to delete the file
6836 * @return 0 upon success.
6837 * In case of error, return the appropriate negated {@code errno} value:
6838 * <ul>
shafik575d0742019-11-25 17:02:57 +00006839 * <li>{@link OsConstants#ENOENT} if the file does not exist or if the app tries to delete file
6840 * in another app's external dir
6841 * <li>{@link OsConstants#EPERM} a security exception was thrown by {@link #delete}, or if the
6842 * calling package is a legacy app that doesn't have WRITE_EXTERNAL_STORAGE permission.
shafik0c0e0d72019-10-16 17:34:17 +01006843 * </ul>
6844 *
6845 * Called from JNI in jni/MediaProviderWrapper.cpp
6846 */
6847 @Keep
Sahana Rao8a8af8c2020-02-03 10:40:18 +00006848 public int deleteFileForFuse(@NonNull String path, int uid) throws IOException {
shafik0c0e0d72019-10-16 17:34:17 +01006849 final LocalCallingIdentity token =
shafikd84da092020-04-29 17:53:30 +01006850 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
shafik0c0e0d72019-10-16 17:34:17 +01006851 try {
Martijn Coenen9a1f6792020-03-17 07:13:47 +01006852 if (isPrivatePackagePathNotOwnedByCaller(path)) {
6853 Log.e(TAG, "Can't delete a file in another app's external directory!");
6854 return OsConstants.ENOENT;
shafik0c0e0d72019-10-16 17:34:17 +01006855 }
6856
Sahana Rao74484532020-04-07 14:58:29 +01006857 final boolean shouldBypass = shouldBypassFuseRestrictions(/*forWrite*/ true, path);
6858
shafik575d0742019-11-25 17:02:57 +00006859 // Legacy apps that made is this far don't have the right storage permission and hence
6860 // are not allowed to access anything other than their external app directory
Sahana Rao74484532020-04-07 14:58:29 +01006861 if (!shouldBypass && isCallingPackageRequestingLegacy()) {
shafike4fb1462020-01-29 16:25:23 +00006862 return OsConstants.EPERM;
shafik575d0742019-11-25 17:02:57 +00006863 }
6864
shafik536982a2020-05-14 17:54:05 +01006865 final Uri contentUri = FileUtils.getContentUriForPath(path);
shafik0c0e0d72019-10-16 17:34:17 +01006866 final String where = FileColumns.DATA + " = ?";
Sahana Rao6b7baf42020-04-17 20:42:23 +01006867 final String[] whereArgs = {path};
shafik0c0e0d72019-10-16 17:34:17 +01006868
6869 if (delete(contentUri, where, whereArgs) == 0) {
Sahana Rao74484532020-04-07 14:58:29 +01006870 if (shouldBypass) {
6871 return deleteFileUnchecked(path);
6872 }
shafike4fb1462020-01-29 16:25:23 +00006873 return OsConstants.ENOENT;
shafik0c0e0d72019-10-16 17:34:17 +01006874 } else {
6875 // success - 1 file was deleted
6876 return 0;
6877 }
6878
6879 } catch (SecurityException e) {
6880 Log.e(TAG, "File deletion not allowed", e);
shafike4fb1462020-01-29 16:25:23 +00006881 return OsConstants.EPERM;
shafik0c0e0d72019-10-16 17:34:17 +01006882 } finally {
6883 restoreLocalCallingIdentity(token);
6884 }
6885 }
6886
shafikbba5b672019-11-15 16:52:51 +00006887 /**
6888 * Checks if the app with the given UID is allowed to create or delete the directory with the
6889 * given path.
6890 *
6891 * @param path File path of the directory that the app wants to create/delete
6892 * @param uid UID of the app that wants to create/delete the directory
shafikf0fea692020-02-14 15:49:17 +00006893 * @param forCreate denotes whether the operation is directory creation or deletion
shafike4fb1462020-01-29 16:25:23 +00006894 * @return 0 if the operation is allowed, or the following {@code errno} values:
shafikbba5b672019-11-15 16:52:51 +00006895 * <ul>
6896 * <li>{@link OsConstants#EACCES} if the app tries to create/delete a dir in another app's
shafik575d0742019-11-25 17:02:57 +00006897 * external directory, or if the calling package is a legacy app that doesn't have
6898 * WRITE_EXTERNAL_STORAGE permission.
shafikbba5b672019-11-15 16:52:51 +00006899 * <li>{@link OsConstants#EPERM} if the app tries to create/delete a top-level directory.
6900 * </ul>
6901 *
6902 * Called from JNI in jni/MediaProviderWrapper.cpp
6903 */
6904 @Keep
shafikf0fea692020-02-14 15:49:17 +00006905 public int isDirectoryCreationOrDeletionAllowedForFuse(
6906 @NonNull String path, int uid, boolean forCreate) {
shafikd84da092020-04-29 17:53:30 +01006907 final LocalCallingIdentity token =
6908 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
Zim696dea42020-03-07 11:41:42 +00006909
shafikbba5b672019-11-15 16:52:51 +00006910 try {
shafikbba5b672019-11-15 16:52:51 +00006911 // App dirs are not indexed, so we don't create an entry for the file.
Martijn Coenen9a1f6792020-03-17 07:13:47 +01006912 if (isPrivatePackagePathNotOwnedByCaller(path)) {
6913 Log.e(TAG, "Can't modify another app's external directory!");
6914 return OsConstants.EACCES;
shafikbba5b672019-11-15 16:52:51 +00006915 }
6916
shafik63abf8b2020-03-02 15:44:37 +00006917 if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) {
6918 return 0;
6919 }
shafik575d0742019-11-25 17:02:57 +00006920 // Legacy apps that made is this far don't have the right storage permission and hence
6921 // are not allowed to access anything other than their external app directory
6922 if (isCallingPackageRequestingLegacy()) {
shafike4fb1462020-01-29 16:25:23 +00006923 return OsConstants.EACCES;
shafik575d0742019-11-25 17:02:57 +00006924 }
6925
shafikbba5b672019-11-15 16:52:51 +00006926 final String[] relativePath = sanitizePath(extractRelativePath(path));
shafikf0fea692020-02-14 15:49:17 +00006927 final boolean isTopLevelDir =
6928 relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
6929 if (isTopLevelDir) {
Nandana Dutt992e52f2020-06-25 10:55:52 +01006930 // We allow creating the default top level directories only, all other operations on
shafikf0fea692020-02-14 15:49:17 +00006931 // top level directories are not allowed.
6932 if (forCreate && isDefaultDirectoryName(extractDisplayName(path))) {
6933 return 0;
6934 }
6935 Log.e(TAG,
6936 "Creating a non-default top level directory or deleting an existing"
6937 + " one is not allowed!");
shafike4fb1462020-01-29 16:25:23 +00006938 return OsConstants.EPERM;
shafikbba5b672019-11-15 16:52:51 +00006939 }
6940 return 0;
6941 } finally {
6942 restoreLocalCallingIdentity(token);
6943 }
6944 }
6945
shafik824c1082019-11-22 12:00:52 +00006946 /**
6947 * Checks whether the app with the given UID is allowed to open the directory denoted by the
6948 * given path.
6949 *
6950 * @param path directory's path
6951 * @param uid UID of the requesting app
shafike4fb1462020-01-29 16:25:23 +00006952 * @return 0 if it's allowed to open the diretory, {@link OsConstants#EACCES} if the calling
shafik575d0742019-11-25 17:02:57 +00006953 * package is a legacy app that doesn't have READ_EXTERNAL_STORAGE permission,
shafike4fb1462020-01-29 16:25:23 +00006954 * {@link OsConstants#ENOENT} otherwise.
shafik824c1082019-11-22 12:00:52 +00006955 *
6956 * Called from JNI in jni/MediaProviderWrapper.cpp
6957 */
6958 @Keep
Nandana Dutt992e52f2020-06-25 10:55:52 +01006959 public int isOpendirAllowedForFuse(@NonNull String path, int uid, boolean forWrite) {
shafikd84da092020-04-29 17:53:30 +01006960 final LocalCallingIdentity token =
6961 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
shafik824c1082019-11-22 12:00:52 +00006962 try {
Nandana Duttc7db6a52020-06-11 21:35:28 +01006963 if ("/storage/emulated".equals(path)) {
6964 return OsConstants.EPERM;
6965 }
Martijn Coenen9a1f6792020-03-17 07:13:47 +01006966 if (isPrivatePackagePathNotOwnedByCaller(path)) {
6967 Log.e(TAG, "Can't access another app's external directory!");
6968 return OsConstants.ENOENT;
shafik824c1082019-11-22 12:00:52 +00006969 }
shafik575d0742019-11-25 17:02:57 +00006970
Sahana Raod19d3f92020-06-09 17:10:33 +01006971 // Do not allow apps to open Android/data or Android/obb dirs. Installer and
6972 // MOUNT_EXTERNAL_ANDROID_WRITABLE apps won't be blocked by this, as their OBB dirs
6973 // are mounted to lowerfs directly.
6974 if (isDataOrObbPath(path)) {
6975 return OsConstants.EACCES;
6976 }
6977
Nandana Dutt992e52f2020-06-25 10:55:52 +01006978 if (shouldBypassFuseRestrictions(forWrite, path)) {
shafik63abf8b2020-03-02 15:44:37 +00006979 return 0;
6980 }
shafik575d0742019-11-25 17:02:57 +00006981 // Legacy apps that made is this far don't have the right storage permission and hence
6982 // are not allowed to access anything other than their external app directory
6983 if (isCallingPackageRequestingLegacy()) {
shafike4fb1462020-01-29 16:25:23 +00006984 return OsConstants.EACCES;
shafik575d0742019-11-25 17:02:57 +00006985 }
Nandana Dutt992e52f2020-06-25 10:55:52 +01006986 // This is a non-legacy app. Rest of the directories are generally writable
6987 // except for non-default top-level directories.
6988 if (forWrite) {
6989 final String[] relativePath = sanitizePath(extractRelativePath(path));
Zim7838bee2020-07-03 14:21:32 +01006990 if (relativePath.length == 0) {
6991 Log.e(TAG, "Directoy write not allowed on invalid relative path for " + path);
6992 return OsConstants.EPERM;
6993 }
Nandana Dutt992e52f2020-06-25 10:55:52 +01006994 final boolean isTopLevelDir =
6995 relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]);
6996 if (isTopLevelDir) {
6997 if (isDefaultDirectoryName(extractDisplayName(path))) {
6998 return 0;
6999 } else {
7000 Log.e(TAG,
7001 "Writing to a non-default top level directory is not allowed!");
7002 return OsConstants.EACCES;
7003 }
7004 }
7005 }
shafik575d0742019-11-25 17:02:57 +00007006
shafik824c1082019-11-22 12:00:52 +00007007 return 0;
7008 } finally {
7009 restoreLocalCallingIdentity(token);
7010 }
7011 }
7012
Ricky Waif40c4022020-04-15 19:00:06 +01007013 @Keep
7014 public boolean isUidForPackageForFuse(@NonNull String packageName, int uid) {
7015 final LocalCallingIdentity token =
shafikd84da092020-04-29 17:53:30 +01007016 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
Ricky Waif40c4022020-04-15 19:00:06 +01007017 try {
7018 return isCallingIdentitySharedPackageName(packageName);
7019 } finally {
7020 restoreLocalCallingIdentity(token);
7021 }
7022 }
7023
Abhijeet Kaur3bc15772021-11-17 08:40:34 +00007024 private boolean isCallingIdentityAllowedAccessToDataOrObbPath(String relativePath) {
7025 // Files under the apps own private directory
7026 final String appSpecificDir = extractOwnerPackageNameFromRelativePath(relativePath);
7027
7028 if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) {
7029 return true;
7030 }
7031 // This is a private-package relativePath; return true if accessible by the caller
7032 return isCallingIdentityAllowedSpecialPrivatePathAccess(relativePath);
7033 }
7034
7035 /**
7036 * @return true iff the caller has installer privileges which gives write access to obb dirs.
7037 */
7038 private boolean isCallingIdentityAllowedInstallerAccess() {
7039 final boolean hasWrite = mCallingIdentity.get().
7040 hasPermission(PERMISSION_WRITE_EXTERNAL_STORAGE);
7041
7042 if (!hasWrite) {
7043 return false;
7044 }
7045
7046 // We're only willing to give out installer access if they also hold
7047 // runtime permission; this is a firm CDD requirement
7048 final boolean hasInstall = mCallingIdentity.get().
7049 hasPermission(PERMISSION_INSTALL_PACKAGES);
7050
7051 if (hasInstall) {
7052 return true;
7053 }
7054
7055 // OPSTR_REQUEST_INSTALL_PACKAGES is granted/denied per package but vold can't
7056 // update mountpoints of a specific package. So, check the appop for all packages
7057 // sharing the uid and allow same level of storage access for all packages even if
7058 // one of the packages has the appop granted.
7059 // To maintain consistency of access in primary volume and secondary volumes use the same
7060 // logic as we do for Zygote.MOUNT_EXTERNAL_INSTALLER view.
7061 return mCallingIdentity.get().hasPermission(APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID);
7062 }
7063
7064 private String getExternalStorageProviderAuthority() {
7065 return MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY;
7066 }
7067
7068 private String getDownloadsProviderAuthority() {
7069 return DOWNLOADS_PROVIDER_AUTHORITY;
7070 }
7071
7072 private boolean isCallingIdentityDownloadProvider() {
7073 return getCallingUidOrSelf() == mDownloadsAuthorityAppId;
7074 }
7075
7076 private boolean isCallingIdentityExternalStorageProvider() {
7077 return getCallingUidOrSelf() == mExternalStorageAuthorityAppId;
7078 }
7079
7080 private boolean isCallingIdentityMtp() {
7081 return mCallingIdentity.get().hasPermission(PERMISSION_ACCESS_MTP);
7082 }
7083
7084 /**
7085 * The following apps have access to all private-app directories on secondary volumes:
7086 * * ExternalStorageProvider
7087 * * DownloadProvider
7088 * * Signature apps with ACCESS_MTP permission granted
7089 * (Note: For Android R we also allow privileged apps with ACCESS_MTP to access all
7090 * private-app directories, this additional access is removed for Android S+).
7091 *
7092 * Installer apps can only access private-app directories on Android/obb.
7093 *
7094 * @param relativePath the relative path of the file to access
7095 */
7096 private boolean isCallingIdentityAllowedSpecialPrivatePathAccess(String relativePath) {
7097 if (isCallingIdentityDownloadProvider() ||
7098 isCallingIdentityExternalStorageProvider() || isCallingIdentityMtp()) {
7099 return true;
7100 }
7101 return (isObbOrChildRelativePath(relativePath) &&
7102 isCallingIdentityAllowedInstallerAccess());
7103 }
7104
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07007105 private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) {
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06007106 // System internals can work with all media
Jeff Sharkey8411c402020-04-29 22:12:36 -06007107 if (isCallingPackageSelf() || isCallingPackageShell()) {
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07007108 return true;
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06007109 }
7110
shafik66f10222020-02-17 18:06:19 +00007111 // Apps that have permission to manage external storage can work with all files
Jeff Sharkey8411c402020-04-29 22:12:36 -06007112 if (isCallingPackageManager()) {
shafik66f10222020-02-17 18:06:19 +00007113 return true;
7114 }
7115
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06007116 // Check if caller is known to be owner of this item, to speed up
7117 // performance of our permission checks
7118 final int table = matchUri(uri, true);
7119 switch (table) {
7120 case AUDIO_MEDIA_ID:
7121 case VIDEO_MEDIA_ID:
7122 case IMAGES_MEDIA_ID:
7123 case FILES_ID:
7124 case DOWNLOADS_ID:
7125 final long id = ContentUris.parseId(uri);
7126 if (mCallingIdentity.get().isOwned(id)) {
7127 return true;
7128 }
Ivan Chiangee937df2021-11-16 08:56:59 +00007129 break;
7130 default:
7131 // continue below
7132 }
7133
7134 // Check whether the uri is a specific table or not. Don't allow the global access to these
7135 // table uris
7136 switch (table) {
7137 case AUDIO_MEDIA:
7138 case IMAGES_MEDIA:
7139 case VIDEO_MEDIA:
7140 case DOWNLOADS:
7141 case FILES:
7142 case AUDIO_ALBUMS:
7143 case AUDIO_ARTISTS:
7144 case AUDIO_GENRES:
7145 case AUDIO_PLAYLISTS:
7146 return false;
7147 default:
7148 // continue below
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06007149 }
7150
Jeff Sharkey0bf693f2018-10-27 19:47:17 -06007151 // Outstanding grant means they get access
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007152 if (getContext().checkUriPermission(uri, mCallingIdentity.get().pid,
7153 mCallingIdentity.get().uid, forWrite
7154 ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION
7155 : Intent.FLAG_GRANT_READ_URI_PERMISSION) == PERMISSION_GRANTED) {
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07007156 return true;
Jeff Sharkey0bf693f2018-10-27 19:47:17 -06007157 }
7158
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07007159 return false;
7160 }
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06007161
Jeff Sharkey4ef05632020-04-06 17:21:36 -06007162 @VisibleForTesting
7163 public boolean isFuseThread() {
Sahana Raoc22c85a2020-03-16 10:23:48 +00007164 return FuseDaemon.native_is_fuse_thread();
7165 }
7166
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007167 @Deprecated
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07007168 private boolean checkCallingPermissionAudio(boolean forWrite, String callingPackage) {
Jeff Sharkeydd5db182019-03-25 11:44:15 -06007169 if (forWrite) {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007170 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_AUDIO);
Jeff Sharkeydd5db182019-03-25 11:44:15 -06007171 } else {
shafik260c1402020-02-18 17:40:05 +00007172 // write permission should be enough for reading as well
7173 return mCallingIdentity.get().hasPermission(PERMISSION_READ_AUDIO)
7174 || mCallingIdentity.get().hasPermission(PERMISSION_WRITE_AUDIO);
Jeff Sharkeydd5db182019-03-25 11:44:15 -06007175 }
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07007176 }
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06007177
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007178 @Deprecated
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07007179 private boolean checkCallingPermissionVideo(boolean forWrite, String callingPackage) {
Jeff Sharkeydd5db182019-03-25 11:44:15 -06007180 if (forWrite) {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007181 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO);
Jeff Sharkeydd5db182019-03-25 11:44:15 -06007182 } else {
shafik260c1402020-02-18 17:40:05 +00007183 // write permission should be enough for reading as well
7184 return mCallingIdentity.get().hasPermission(PERMISSION_READ_VIDEO)
7185 || mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO);
Jeff Sharkeydd5db182019-03-25 11:44:15 -06007186 }
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07007187 }
7188
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007189 @Deprecated
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07007190 private boolean checkCallingPermissionImages(boolean forWrite, String callingPackage) {
Jeff Sharkeydd5db182019-03-25 11:44:15 -06007191 if (forWrite) {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007192 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES);
Jeff Sharkeydd5db182019-03-25 11:44:15 -06007193 } else {
shafik260c1402020-02-18 17:40:05 +00007194 // write permission should be enough for reading as well
7195 return mCallingIdentity.get().hasPermission(PERMISSION_READ_IMAGES)
7196 || mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES);
Jeff Sharkeydd5db182019-03-25 11:44:15 -06007197 }
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06007198 }
7199
7200 /**
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07007201 * Enforce that caller has access to the given {@link Uri}.
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06007202 *
7203 * @throws SecurityException if access isn't allowed.
7204 */
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07007205 private void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras,
7206 boolean forWrite) {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06007207 Trace.beginSection("enforceCallingPermission");
Jeff Sharkey031af8d2019-04-28 11:11:30 -06007208 try {
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07007209 enforceCallingPermissionInternal(uri, extras, forWrite);
Jeff Sharkey031af8d2019-04-28 11:11:30 -06007210 } finally {
Jeff Sharkey0b801a52019-08-08 11:19:51 -06007211 Trace.endSection();
Jeff Sharkey031af8d2019-04-28 11:11:30 -06007212 }
7213 }
7214
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07007215 private void enforceCallingPermissionInternal(@NonNull Uri uri, @NonNull Bundle extras,
7216 boolean forWrite) {
Jeff Sharkey61378cb2019-11-23 16:11:09 -07007217 Objects.requireNonNull(uri);
7218 Objects.requireNonNull(extras);
7219
Jeff Sharkey3388f6e2018-11-19 12:11:38 -07007220 // Try a simple global check first before falling back to performing a
7221 // simple query to probe for access.
7222 if (checkCallingPermissionGlobal(uri, forWrite)) {
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06007223 // Access allowed, yay!
7224 return;
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007225 }
Jeff Sharkeyb057c7b2018-12-05 19:18:23 -07007226
Jeff Sharkey5ed33602019-01-23 14:31:30 -07007227 final DatabaseHelper helper;
Jeff Sharkey5ed33602019-01-23 14:31:30 -07007228 try {
7229 helper = getDatabaseForUri(uri);
Jeff Sharkey5ed33602019-01-23 14:31:30 -07007230 } catch (VolumeNotFoundException e) {
7231 throw e.rethrowAsIllegalArgumentException();
7232 }
Jeff Sharkeyb057c7b2018-12-05 19:18:23 -07007233
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007234 final boolean allowHidden = isCallingPackageAllowedHidden();
7235 final int table = matchUri(uri, allowHidden);
7236
7237 // First, check to see if caller has direct write access
7238 if (forWrite) {
Jeff Sharkey61378cb2019-11-23 16:11:09 -07007239 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, null);
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07007240 try (Cursor c = qb.query(helper, new String[0],
7241 null, null, null, null, null, null, null)) {
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06007242 if (c.moveToFirst()) {
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007243 // Direct write access granted, yay!
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06007244 return;
7245 }
7246 }
7247 }
7248
Jeff Sharkey8f9ca6a2018-12-20 12:48:55 -07007249 // We only allow the user to grant access to specific media items in
7250 // strongly typed collections; never to broad collections
7251 boolean allowUserGrant = false;
7252 final int matchUri = matchUri(uri, true);
7253 switch (matchUri) {
7254 case IMAGES_MEDIA_ID:
7255 case AUDIO_MEDIA_ID:
7256 case VIDEO_MEDIA_ID:
7257 allowUserGrant = true;
7258 break;
7259 }
7260
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007261 // Second, check to see if caller has direct read access
Jeff Sharkey61378cb2019-11-23 16:11:09 -07007262 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null);
Jeff Sharkey88d84fb2020-01-13 21:38:46 -07007263 try (Cursor c = qb.query(helper, new String[0],
7264 null, null, null, null, null, null, null)) {
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007265 if (c.moveToFirst()) {
Jeff Sharkey8f9ca6a2018-12-20 12:48:55 -07007266 if (!forWrite) {
7267 // Direct read access granted, yay!
7268 return;
7269 } else if (allowUserGrant) {
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007270 // Caller has read access, but they wanted to write, and
7271 // they'll need to get the user to grant that access
7272 final Context context = getContext();
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07007273 final Collection<Uri> uris = Arrays.asList(uri);
7274 final PendingIntent intent = MediaStore
7275 .createWriteRequest(ContentResolver.wrap(this), uris);
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007276
7277 final Icon icon = getCollectionIcon(uri);
7278 final RemoteAction action = new RemoteAction(icon,
Jeff Sharkey6ebf9962019-02-15 17:43:37 -07007279 context.getText(R.string.permission_required_action),
7280 context.getText(R.string.permission_required_action),
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007281 intent);
7282
7283 throw new RecoverableSecurityException(new SecurityException(
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007284 getCallingPackageOrSelf() + " has no access to " + uri),
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007285 context.getText(R.string.permission_required), action);
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007286 }
7287 }
7288 }
7289
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007290 throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri);
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007291 }
7292
7293 private Icon getCollectionIcon(Uri uri) {
7294 final PackageManager pm = getContext().getPackageManager();
7295 final String type = uri.getPathSegments().get(1);
7296 final String groupName;
7297 switch (type) {
Jeff Sharkeyc3088d82018-12-11 17:32:51 -07007298 default: groupName = android.Manifest.permission_group.STORAGE; break;
7299 }
7300 try {
7301 final PermissionGroupInfo perm = pm.getPermissionGroupInfo(groupName, 0);
7302 return Icon.createWithResource(perm.packageName, perm.icon);
7303 } catch (NameNotFoundException e) {
7304 throw new RuntimeException(e);
7305 }
Jeff Sharkey5a8bb562018-08-10 18:04:10 -06007306 }
7307
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07007308 private void checkAccess(@NonNull Uri uri, @NonNull Bundle extras, @NonNull File file,
7309 boolean isWrite) throws FileNotFoundException {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007310 // First, does caller have the needed row-level access?
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07007311 enforceCallingPermission(uri, extras, isWrite);
Jeff Sharkey55d5bd92018-12-01 18:26:52 -07007312
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007313 // Second, does the path look sane?
7314 if (!FileUtils.contains(Environment.getStorageDirectory(), file)) {
7315 checkWorldReadAccess(file.getAbsolutePath());
Jeff Sharkey007645e2012-03-08 17:45:12 -08007316 }
Jeff Sharkey007645e2012-03-08 17:45:12 -08007317 }
7318
Marco Nelissen70eadbf2013-07-12 12:44:36 -07007319 /**
7320 * Check whether the path is a world-readable file
7321 */
Jeff Sharkeyf06febd2020-04-07 13:03:30 -06007322 @VisibleForTesting
7323 public static void checkWorldReadAccess(String path) throws FileNotFoundException {
Jeff Sharkey55f76902015-07-24 15:22:08 -07007324 // Path has already been canonicalized, and we relax the check to look
7325 // at groups to support runtime storage permissions.
7326 final int accessBits = path.startsWith("/storage/") ? OsConstants.S_IRGRP
7327 : OsConstants.S_IROTH;
Marco Nelissen70eadbf2013-07-12 12:44:36 -07007328 try {
Elliott Hughesf3b67d52014-04-28 11:42:08 -07007329 StructStat stat = Os.stat(path);
Marco Nelissen70eadbf2013-07-12 12:44:36 -07007330 if (OsConstants.S_ISREG(stat.st_mode) &&
7331 ((stat.st_mode & accessBits) == accessBits)) {
7332 checkLeadingPathComponentsWorldExecutable(path);
7333 return;
7334 }
7335 } catch (ErrnoException e) {
7336 // couldn't stat the file, either it doesn't exist or isn't
7337 // accessible to us
7338 }
7339
7340 throw new FileNotFoundException("Can't access " + path);
7341 }
7342
Jeff Sharkey55f76902015-07-24 15:22:08 -07007343 private static void checkLeadingPathComponentsWorldExecutable(String filePath)
Marco Nelissen70eadbf2013-07-12 12:44:36 -07007344 throws FileNotFoundException {
7345 File parent = new File(filePath).getParentFile();
7346
Jeff Sharkey55f76902015-07-24 15:22:08 -07007347 // Path has already been canonicalized, and we relax the check to look
7348 // at groups to support runtime storage permissions.
7349 final int accessBits = filePath.startsWith("/storage/") ? OsConstants.S_IXGRP
7350 : OsConstants.S_IXOTH;
Marco Nelissen70eadbf2013-07-12 12:44:36 -07007351
7352 while (parent != null) {
7353 if (! parent.exists()) {
7354 // parent dir doesn't exist, give up
7355 throw new FileNotFoundException("access denied");
7356 }
7357 try {
Elliott Hughesf3b67d52014-04-28 11:42:08 -07007358 StructStat stat = Os.stat(parent.getPath());
Marco Nelissen70eadbf2013-07-12 12:44:36 -07007359 if ((stat.st_mode & accessBits) != accessBits) {
7360 // the parent dir doesn't have the appropriate access
7361 throw new FileNotFoundException("Can't access " + filePath);
7362 }
7363 } catch (ErrnoException e1) {
7364 // couldn't stat() parent
7365 throw new FileNotFoundException("Can't access " + filePath);
7366 }
7367 parent = parent.getParentFile();
7368 }
7369 }
7370
Jeff Sharkeyf06febd2020-04-07 13:03:30 -06007371 @VisibleForTesting
7372 static class FallbackException extends Exception {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06007373 private final int mThrowSdkVersion;
7374
7375 public FallbackException(String message, int throwSdkVersion) {
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07007376 super(message);
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06007377 mThrowSdkVersion = throwSdkVersion;
Jeff Sharkey5ed33602019-01-23 14:31:30 -07007378 }
7379
Jeff Sharkeyd6697822020-03-22 20:59:47 -06007380 public FallbackException(String message, Throwable cause, int throwSdkVersion) {
7381 super(message, cause);
7382 mThrowSdkVersion = throwSdkVersion;
7383 }
7384
7385 @Override
7386 public String getMessage() {
7387 if (getCause() != null) {
7388 return super.getMessage() + ": " + getCause().getMessage();
7389 } else {
7390 return super.getMessage();
7391 }
7392 }
7393
Jeff Sharkey5ed33602019-01-23 14:31:30 -07007394 public IllegalArgumentException rethrowAsIllegalArgumentException() {
7395 throw new IllegalArgumentException(getMessage());
7396 }
7397
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07007398 public Cursor translateForQuery(int targetSdkVersion) {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06007399 if (targetSdkVersion >= mThrowSdkVersion) {
Jeff Sharkey5ed33602019-01-23 14:31:30 -07007400 throw new IllegalArgumentException(getMessage());
7401 } else {
7402 Log.w(TAG, getMessage());
7403 return null;
7404 }
7405 }
7406
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07007407 public Uri translateForInsert(int targetSdkVersion) {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06007408 if (targetSdkVersion >= mThrowSdkVersion) {
Jeff Sharkey5ed33602019-01-23 14:31:30 -07007409 throw new IllegalArgumentException(getMessage());
7410 } else {
7411 Log.w(TAG, getMessage());
7412 return null;
7413 }
7414 }
7415
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07007416 public int translateForUpdateDelete(int targetSdkVersion) {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06007417 if (targetSdkVersion >= mThrowSdkVersion) {
Jeff Sharkey5ed33602019-01-23 14:31:30 -07007418 throw new IllegalArgumentException(getMessage());
7419 } else {
7420 Log.w(TAG, getMessage());
7421 return 0;
7422 }
7423 }
7424 }
The Android Open Source Project70215272009-03-03 19:32:43 -08007425
Jeff Sharkeyf06febd2020-04-07 13:03:30 -06007426 @VisibleForTesting
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07007427 static class VolumeNotFoundException extends FallbackException {
7428 public VolumeNotFoundException(String volumeName) {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06007429 super("Volume " + volumeName + " not found", Build.VERSION_CODES.Q);
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07007430 }
7431 }
7432
Jeff Sharkeyf06febd2020-04-07 13:03:30 -06007433 @VisibleForTesting
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07007434 static class VolumeArgumentException extends FallbackException {
7435 public VolumeArgumentException(File actual, Collection<File> allowed) {
Jeff Sharkeycc5c31d2019-10-08 16:10:53 -06007436 super("Requested path " + actual + " doesn't appear under " + allowed,
7437 Build.VERSION_CODES.Q);
Jeff Sharkey4fc388d2019-03-08 18:24:21 -07007438 }
7439 }
7440
Jeff Sharkey5ed33602019-01-23 14:31:30 -07007441 private @NonNull DatabaseHelper getDatabaseForUri(Uri uri) throws VolumeNotFoundException {
Jeff Sharkey71437302019-04-09 23:46:52 -06007442 final String volumeName = resolveVolumeName(uri);
7443 synchronized (mAttachedVolumeNames) {
7444 if (!mAttachedVolumeNames.contains(volumeName)) {
Jeff Sharkey5ed33602019-01-23 14:31:30 -07007445 throw new VolumeNotFoundException(volumeName);
Jeff Sharkey077b71e2019-01-22 13:19:51 -07007446 }
The Android Open Source Project70215272009-03-03 19:32:43 -08007447 }
Jeff Sharkey4dbbdfd2019-05-21 10:37:02 -06007448 if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
7449 return mInternalDatabase;
7450 } else {
7451 return mExternalDatabase;
7452 }
Jeff Sharkey6cf27b92019-03-24 13:03:02 -06007453 }
7454
Dianne Hackbornfd8402c2011-08-18 19:46:51 -07007455 static boolean isMediaDatabaseName(String name) {
7456 if (INTERNAL_DATABASE_NAME.equals(name)) {
7457 return true;
7458 }
7459 if (EXTERNAL_DATABASE_NAME.equals(name)) {
7460 return true;
7461 }
kwangjung.kim168d49e2013-04-03 21:08:05 +09007462 if (name.startsWith("external-") && name.endsWith(".db")) {
Dianne Hackbornfd8402c2011-08-18 19:46:51 -07007463 return true;
7464 }
7465 return false;
7466 }
7467
7468 static boolean isInternalMediaDatabaseName(String name) {
7469 if (INTERNAL_DATABASE_NAME.equals(name)) {
7470 return true;
7471 }
7472 return false;
7473 }
7474
Jeff Sharkey5cff16b2020-01-17 19:00:50 -07007475 private @NonNull Uri getBaseContentUri(@NonNull String volumeName) {
7476 return MediaStore.AUTHORITY_URI.buildUpon().appendPath(volumeName).build();
7477 }
7478
Zim604f4522020-06-05 15:30:09 +01007479 public Uri attachVolume(String volume, boolean validate) {
Jeff Sharkey3650e852019-05-29 10:11:38 -06007480 if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
The Android Open Source Project70215272009-03-03 19:32:43 -08007481 throw new SecurityException(
7482 "Opening and closing databases not allowed.");
7483 }
7484
Jeff Sharkey5fdbd982019-01-20 11:03:28 -07007485 // Quick sanity check for shady volume names
7486 MediaStore.checkArgumentVolumeName(volume);
7487
Jeff Sharkey7eed0eb2019-01-19 17:27:46 -07007488 // Quick sanity check that volume actually exists
Zim604f4522020-06-05 15:30:09 +01007489 if (!MediaStore.VOLUME_INTERNAL.equals(volume) && validate) {
Jeff Sharkey077b71e2019-01-22 13:19:51 -07007490 try {
Jeff Sharkeyeeda7ba2019-05-17 18:48:04 -06007491 getVolumePath(volume);
Jeff Sharkey077b71e2019-01-22 13:19:51 -07007492 } catch (IOException e) {
7493 throw new IllegalArgumentException(
7494 "Volume " + volume + " currently unavailable", e);
7495 }
Jeff Sharkey7eed0eb2019-01-19 17:27:46 -07007496 }
7497
Jeff Sharkey71437302019-04-09 23:46:52 -06007498 synchronized (mAttachedVolumeNames) {
7499 mAttachedVolumeNames.add(volume);
The Android Open Source Project70215272009-03-03 19:32:43 -08007500 }
7501
Jeff Sharkey5cff16b2020-01-17 19:00:50 -07007502 final ContentResolver resolver = getContext().getContentResolver();
7503 final Uri uri = getBaseContentUri(volume);
7504 resolver.notifyChange(getBaseContentUri(volume), null);
7505
Jeff Sharkeyc55994b2019-12-20 19:43:59 -07007506 if (LOGV) Log.v(TAG, "Attached volume: " + volume);
Jeff Sharkey71437302019-04-09 23:46:52 -06007507 if (!MediaStore.VOLUME_INTERNAL.equals(volume)) {
Jeff Sharkey5cff16b2020-01-17 19:00:50 -07007508 // Also notify on synthetic view of all devices
7509 resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
7510
Jeff Sharkey22988642020-03-05 17:09:39 -07007511 ForegroundThread.getExecutor().execute(() -> {
Jeff Sharkey5cff16b2020-01-17 19:00:50 -07007512 final DatabaseHelper helper = MediaStore.VOLUME_INTERNAL.equals(volume)
7513 ? mInternalDatabase : mExternalDatabase;
Jeff Sharkeya44a7ba2020-03-31 19:13:24 -06007514 helper.runWithTransaction((db) -> {
7515 ensureDefaultFolders(volume, db);
7516 ensureThumbnailsValid(volume, db);
7517 return null;
7518 });
Jeff Sharkey9aca51f2020-04-29 11:28:08 -06007519
7520 // We just finished the database operation above, we know that
7521 // it's ready to answer queries, so notify our DocumentProvider
7522 // so it can answer queries without risking ANR
7523 MediaDocumentsProvider.onMediaStoreReady(getContext(), volume);
Jeff Sharkeyf0ebe932019-11-13 16:09:48 -07007524 });
Jeff Sharkey72613f72015-08-19 14:18:19 -07007525 }
Jeff Sharkey71437302019-04-09 23:46:52 -06007526 return uri;
The Android Open Source Project70215272009-03-03 19:32:43 -08007527 }
7528
Jeff Sharkey7eed0eb2019-01-19 17:27:46 -07007529 private void detachVolume(Uri uri) {
7530 detachVolume(MediaStore.getVolumeName(uri));
7531 }
7532
Jeff Sharkey5fdbd982019-01-20 11:03:28 -07007533 public void detachVolume(String volume) {
Jeff Sharkey3650e852019-05-29 10:11:38 -06007534 if (mCallingIdentity.get().pid != android.os.Process.myPid()) {
The Android Open Source Project70215272009-03-03 19:32:43 -08007535 throw new SecurityException(
7536 "Opening and closing databases not allowed.");
7537 }
7538
Jeff Sharkey5fdbd982019-01-20 11:03:28 -07007539 // Quick sanity check for shady volume names
7540 MediaStore.checkArgumentVolumeName(volume);
7541
Jeff Sharkey71437302019-04-09 23:46:52 -06007542 if (MediaStore.VOLUME_INTERNAL.equals(volume)) {
The Android Open Source Project70215272009-03-03 19:32:43 -08007543 throw new UnsupportedOperationException(
7544 "Deleting the internal volume is not allowed");
The Android Open Source Project70215272009-03-03 19:32:43 -08007545 }
7546
Jeff Sharkey68b2f062019-03-22 16:54:30 -06007547 // Signal any scanning to shut down
Jeff Sharkey85acbbe2019-10-15 17:10:30 -06007548 mMediaScanner.onDetachVolume(volume);
Jeff Sharkey68b2f062019-03-22 16:54:30 -06007549
Jeff Sharkey71437302019-04-09 23:46:52 -06007550 synchronized (mAttachedVolumeNames) {
7551 mAttachedVolumeNames.remove(volume);
The Android Open Source Project70215272009-03-03 19:32:43 -08007552 }
7553
Jeff Sharkey5cff16b2020-01-17 19:00:50 -07007554 final ContentResolver resolver = getContext().getContentResolver();
7555 final Uri uri = getBaseContentUri(volume);
7556 resolver.notifyChange(getBaseContentUri(volume), null);
7557
7558 if (!MediaStore.VOLUME_INTERNAL.equals(volume)) {
7559 // Also notify on synthetic view of all devices
7560 resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null);
7561 }
7562
Jeff Sharkeyc55994b2019-12-20 19:43:59 -07007563 if (LOGV) Log.v(TAG, "Detached volume: " + volume);
The Android Open Source Project70215272009-03-03 19:32:43 -08007564 }
7565
Jeff Sharkey71437302019-04-09 23:46:52 -06007566 @GuardedBy("mAttachedVolumeNames")
7567 private final ArraySet<String> mAttachedVolumeNames = new ArraySet<>();
Jeff Sharkey66881302019-10-05 10:50:06 -06007568 @GuardedBy("mCustomCollators")
7569 private final ArraySet<String> mCustomCollators = new ArraySet<>();
Jeff Sharkey71437302019-04-09 23:46:52 -06007570
Jeff Sharkey85acbbe2019-10-15 17:10:30 -06007571 private MediaScanner mMediaScanner;
7572
Jeff Sharkey4dbbdfd2019-05-21 10:37:02 -06007573 private DatabaseHelper mInternalDatabase;
7574 private DatabaseHelper mExternalDatabase;
The Android Open Source Project70215272009-03-03 19:32:43 -08007575
The Android Open Source Project70215272009-03-03 19:32:43 -08007576 // name of the volume currently being scanned by the media scanner (or null)
7577 private String mMediaScannerVolume;
7578
Marco Nelissen00270192010-01-08 08:35:20 -08007579 // current FAT volume ID
Mike Lockwood993b6f02011-01-19 14:20:51 -08007580 private int mVolumeId = -1;
Marco Nelissen00270192010-01-08 08:35:20 -08007581
Mike Lockwood17179552010-07-09 10:46:58 -04007582 // WARNING: the values of IMAGES_MEDIA, AUDIO_MEDIA, and VIDEO_MEDIA and AUDIO_PLAYLISTS
Mike Lockwood16dc0fd2010-09-08 12:52:17 -04007583 // are stored in the "files" table, so do not renumber them unless you also add
Mike Lockwood17179552010-07-09 10:46:58 -04007584 // a corresponding database upgrade step for it.
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07007585 static final int IMAGES_MEDIA = 1;
7586 static final int IMAGES_MEDIA_ID = 2;
7587 static final int IMAGES_MEDIA_ID_THUMBNAIL = 3;
7588 static final int IMAGES_THUMBNAILS = 4;
7589 static final int IMAGES_THUMBNAILS_ID = 5;
The Android Open Source Project70215272009-03-03 19:32:43 -08007590
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07007591 static final int AUDIO_MEDIA = 100;
7592 static final int AUDIO_MEDIA_ID = 101;
7593 static final int AUDIO_MEDIA_ID_GENRES = 102;
7594 static final int AUDIO_MEDIA_ID_GENRES_ID = 103;
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07007595 static final int AUDIO_GENRES = 106;
7596 static final int AUDIO_GENRES_ID = 107;
7597 static final int AUDIO_GENRES_ID_MEMBERS = 108;
7598 static final int AUDIO_GENRES_ALL_MEMBERS = 109;
7599 static final int AUDIO_PLAYLISTS = 110;
7600 static final int AUDIO_PLAYLISTS_ID = 111;
7601 static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112;
7602 static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113;
7603 static final int AUDIO_ARTISTS = 114;
7604 static final int AUDIO_ARTISTS_ID = 115;
7605 static final int AUDIO_ALBUMS = 116;
7606 static final int AUDIO_ALBUMS_ID = 117;
7607 static final int AUDIO_ARTISTS_ID_ALBUMS = 118;
7608 static final int AUDIO_ALBUMART = 119;
7609 static final int AUDIO_ALBUMART_ID = 120;
7610 static final int AUDIO_ALBUMART_FILE_ID = 121;
The Android Open Source Project70215272009-03-03 19:32:43 -08007611
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07007612 static final int VIDEO_MEDIA = 200;
7613 static final int VIDEO_MEDIA_ID = 201;
7614 static final int VIDEO_MEDIA_ID_THUMBNAIL = 202;
7615 static final int VIDEO_THUMBNAILS = 203;
7616 static final int VIDEO_THUMBNAILS_ID = 204;
The Android Open Source Project70215272009-03-03 19:32:43 -08007617
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07007618 static final int VOLUMES = 300;
7619 static final int VOLUMES_ID = 301;
The Android Open Source Project70215272009-03-03 19:32:43 -08007620
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07007621 static final int MEDIA_SCANNER = 500;
The Android Open Source Project70215272009-03-03 19:32:43 -08007622
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07007623 static final int FS_ID = 600;
7624 static final int VERSION = 601;
Marco Nelissen00270192010-01-08 08:35:20 -08007625
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07007626 static final int FILES = 700;
7627 static final int FILES_ID = 701;
Brian Muramatsua36cfae2010-11-30 13:46:02 -08007628
Jeff Sharkeyeea49d32019-12-11 17:45:38 -07007629 static final int DOWNLOADS = 800;
7630 static final int DOWNLOADS_ID = 801;
Sudheer Shankaaa626512018-11-15 20:29:28 -08007631
Jeff Sharkey74f73732019-11-12 15:36:32 -07007632 private LocalUriMatcher mUriMatcher;
The Android Open Source Project70215272009-03-03 19:32:43 -08007633
Mike Lockwood1d4a47c2010-10-12 14:24:00 -04007634 private static final String[] PATH_PROJECTION = new String[] {
7635 MediaStore.MediaColumns._ID,
7636 MediaStore.MediaColumns.DATA,
7637 };
7638
Jeff Sharkey74f73732019-11-12 15:36:32 -07007639 private int matchUri(Uri uri, boolean allowHidden) {
7640 return mUriMatcher.matchUri(uri, allowHidden);
Jeff Sharkey94461582018-07-12 14:34:47 -06007641 }
The Android Open Source Project70215272009-03-03 19:32:43 -08007642
Jeff Sharkey74f73732019-11-12 15:36:32 -07007643 static class LocalUriMatcher {
7644 private final UriMatcher mPublic = new UriMatcher(UriMatcher.NO_MATCH);
7645 private final UriMatcher mHidden = new UriMatcher(UriMatcher.NO_MATCH);
The Android Open Source Project70215272009-03-03 19:32:43 -08007646
Jeff Sharkey74f73732019-11-12 15:36:32 -07007647 public int matchUri(Uri uri, boolean allowHidden) {
7648 final int publicMatch = mPublic.match(uri);
7649 if (publicMatch != UriMatcher.NO_MATCH) {
7650 return publicMatch;
7651 }
Marco Nelissen00270192010-01-08 08:35:20 -08007652
Jeff Sharkey74f73732019-11-12 15:36:32 -07007653 final int hiddenMatch = mHidden.match(uri);
7654 if (hiddenMatch != UriMatcher.NO_MATCH) {
7655 // Detect callers asking about hidden behavior by looking closer when
7656 // the matchers diverge; we only care about apps that are explicitly
7657 // targeting a specific public API level.
7658 if (!allowHidden) {
7659 throw new IllegalStateException("Unknown URL: " + uri + " is hidden API");
7660 }
7661 return hiddenMatch;
7662 }
Mike Lockwood819cafd2011-01-21 17:05:41 -08007663
Jeff Sharkey74f73732019-11-12 15:36:32 -07007664 return UriMatcher.NO_MATCH;
7665 }
Jeff Sharkey94461582018-07-12 14:34:47 -06007666
Jeff Sharkey74f73732019-11-12 15:36:32 -07007667 public LocalUriMatcher(String auth) {
7668 mPublic.addURI(auth, "*/images/media", IMAGES_MEDIA);
7669 mPublic.addURI(auth, "*/images/media/#", IMAGES_MEDIA_ID);
7670 mPublic.addURI(auth, "*/images/media/#/thumbnail", IMAGES_MEDIA_ID_THUMBNAIL);
7671 mPublic.addURI(auth, "*/images/thumbnails", IMAGES_THUMBNAILS);
7672 mPublic.addURI(auth, "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID);
Jeff Sharkey94461582018-07-12 14:34:47 -06007673
Jeff Sharkey74f73732019-11-12 15:36:32 -07007674 mPublic.addURI(auth, "*/audio/media", AUDIO_MEDIA);
7675 mPublic.addURI(auth, "*/audio/media/#", AUDIO_MEDIA_ID);
7676 mPublic.addURI(auth, "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES);
7677 mPublic.addURI(auth, "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID);
Jeff Sharkey74f73732019-11-12 15:36:32 -07007678 mPublic.addURI(auth, "*/audio/genres", AUDIO_GENRES);
7679 mPublic.addURI(auth, "*/audio/genres/#", AUDIO_GENRES_ID);
7680 mPublic.addURI(auth, "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS);
7681 // TODO: not actually defined in API, but CTS tested
7682 mPublic.addURI(auth, "*/audio/genres/all/members", AUDIO_GENRES_ALL_MEMBERS);
7683 mPublic.addURI(auth, "*/audio/playlists", AUDIO_PLAYLISTS);
7684 mPublic.addURI(auth, "*/audio/playlists/#", AUDIO_PLAYLISTS_ID);
7685 mPublic.addURI(auth, "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS);
7686 mPublic.addURI(auth, "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID);
7687 mPublic.addURI(auth, "*/audio/artists", AUDIO_ARTISTS);
7688 mPublic.addURI(auth, "*/audio/artists/#", AUDIO_ARTISTS_ID);
7689 mPublic.addURI(auth, "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS);
7690 mPublic.addURI(auth, "*/audio/albums", AUDIO_ALBUMS);
7691 mPublic.addURI(auth, "*/audio/albums/#", AUDIO_ALBUMS_ID);
7692 // TODO: not actually defined in API, but CTS tested
7693 mPublic.addURI(auth, "*/audio/albumart", AUDIO_ALBUMART);
7694 // TODO: not actually defined in API, but CTS tested
7695 mPublic.addURI(auth, "*/audio/albumart/#", AUDIO_ALBUMART_ID);
7696 // TODO: not actually defined in API, but CTS tested
7697 mPublic.addURI(auth, "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID);
Jeff Sharkey94461582018-07-12 14:34:47 -06007698
Jeff Sharkey74f73732019-11-12 15:36:32 -07007699 mPublic.addURI(auth, "*/video/media", VIDEO_MEDIA);
7700 mPublic.addURI(auth, "*/video/media/#", VIDEO_MEDIA_ID);
7701 mPublic.addURI(auth, "*/video/media/#/thumbnail", VIDEO_MEDIA_ID_THUMBNAIL);
7702 mPublic.addURI(auth, "*/video/thumbnails", VIDEO_THUMBNAILS);
7703 mPublic.addURI(auth, "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID);
The Android Open Source Project70215272009-03-03 19:32:43 -08007704
Jeff Sharkey74f73732019-11-12 15:36:32 -07007705 mPublic.addURI(auth, "*/media_scanner", MEDIA_SCANNER);
Mike Lockwoodb78ad0d2010-07-03 00:45:10 -04007706
Jeff Sharkey74f73732019-11-12 15:36:32 -07007707 // NOTE: technically hidden, since Uri is never exposed
7708 mPublic.addURI(auth, "*/fs_id", FS_ID);
7709 // NOTE: technically hidden, since Uri is never exposed
7710 mPublic.addURI(auth, "*/version", VERSION);
Sudheer Shankaaa626512018-11-15 20:29:28 -08007711
Jeff Sharkey74f73732019-11-12 15:36:32 -07007712 mHidden.addURI(auth, "*", VOLUMES_ID);
7713 mHidden.addURI(auth, null, VOLUMES);
7714
Jeff Sharkey74f73732019-11-12 15:36:32 -07007715 mPublic.addURI(auth, "*/file", FILES);
7716 mPublic.addURI(auth, "*/file/#", FILES_ID);
Jeff Sharkey74f73732019-11-12 15:36:32 -07007717
7718 mPublic.addURI(auth, "*/downloads", DOWNLOADS);
7719 mPublic.addURI(auth, "*/downloads/#", DOWNLOADS_ID);
7720 }
The Android Open Source Project70215272009-03-03 19:32:43 -08007721 }
Marco Nelissen10af34f2011-12-16 17:59:52 -08007722
Jeff Sharkey0e880712019-02-11 11:01:31 -07007723 /**
7724 * Set of columns that can be safely mutated by external callers; all other
7725 * columns are treated as read-only, since they reflect what the media
7726 * scanner found on disk, and any mutations would be overwritten the next
7727 * time the media was scanned.
7728 */
Jeff Sharkeybac84e22018-12-20 15:11:17 -07007729 private static final ArraySet<String> sMutableColumns = new ArraySet<>();
7730
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06007731 {
Jeff Sharkeybac84e22018-12-20 15:11:17 -07007732 sMutableColumns.add(MediaStore.MediaColumns.DATA);
Jeff Sharkey2b13ebe2019-03-20 20:19:35 -06007733 sMutableColumns.add(MediaStore.MediaColumns.RELATIVE_PATH);
Jeff Sharkey0e880712019-02-11 11:01:31 -07007734 sMutableColumns.add(MediaStore.MediaColumns.DISPLAY_NAME);
Jeff Sharkeybac84e22018-12-20 15:11:17 -07007735 sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING);
Jeff Sharkey711d10f2019-01-04 16:09:52 -07007736 sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED);
Jeff Sharkeyd4070ec2019-11-23 10:08:38 -07007737 sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE);
Jeff Sharkey8411c402020-04-29 22:12:36 -06007738 sMutableColumns.add(MediaStore.MediaColumns.OWNER_PACKAGE_NAME);
Jeff Sharkey0e880712019-02-11 11:01:31 -07007739
Jeff Sharkeya57867a2019-02-14 13:27:35 -07007740 sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK);
7741
7742 sMutableColumns.add(MediaStore.Video.VideoColumns.TAGS);
7743 sMutableColumns.add(MediaStore.Video.VideoColumns.CATEGORY);
7744 sMutableColumns.add(MediaStore.Video.VideoColumns.BOOKMARK);
7745
Winsonb653af22019-06-05 12:14:13 -07007746 sMutableColumns.add(MediaStore.Audio.Playlists.NAME);
Jeff Sharkeya57867a2019-02-14 13:27:35 -07007747 sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID);
7748 sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER);
7749
Jeff Sharkey033b1ca2020-06-23 07:12:19 -06007750 sMutableColumns.add(MediaStore.DownloadColumns.DOWNLOAD_URI);
7751 sMutableColumns.add(MediaStore.DownloadColumns.REFERER_URI);
7752
Jeff Sharkeya57867a2019-02-14 13:27:35 -07007753 sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE);
7754 sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE);
7755 }
7756
7757 /**
7758 * Set of columns that affect placement of files on disk.
7759 */
7760 private static final ArraySet<String> sPlacementColumns = new ArraySet<>();
7761
7762 {
Jeff Sharkey0e880712019-02-11 11:01:31 -07007763 sPlacementColumns.add(MediaStore.MediaColumns.DATA);
Jeff Sharkey2b13ebe2019-03-20 20:19:35 -06007764 sPlacementColumns.add(MediaStore.MediaColumns.RELATIVE_PATH);
Jeff Sharkey0e880712019-02-11 11:01:31 -07007765 sPlacementColumns.add(MediaStore.MediaColumns.DISPLAY_NAME);
7766 sPlacementColumns.add(MediaStore.MediaColumns.MIME_TYPE);
Jeff Sharkey89149b62020-03-29 22:03:44 -06007767 sPlacementColumns.add(MediaStore.MediaColumns.IS_PENDING);
7768 sPlacementColumns.add(MediaStore.MediaColumns.IS_TRASHED);
7769 sPlacementColumns.add(MediaStore.MediaColumns.DATE_EXPIRES);
Jeff Sharkey0c48d9e2018-08-04 20:03:34 -06007770 }
7771
Jeff Sharkey4230e192018-09-13 17:07:41 -06007772 /**
7773 * List of abusive custom columns that we're willing to allow via
7774 * {@link SQLiteQueryBuilder#setProjectionGreylist(List)}.
7775 */
7776 static final ArrayList<Pattern> sGreylist = new ArrayList<>();
7777
Anton Hansson459d3c72019-04-02 20:20:59 +01007778 private static void addGreylistPattern(String pattern) {
7779 sGreylist.add(Pattern.compile(" *" + pattern + " *"));
7780 }
7781
7782 static {
Jeff Sharkey8cfbb232018-11-18 16:23:35 -07007783 final String maybeAs = "( (as )?[_a-z0-9]+)?";
Anton Hansson459d3c72019-04-02 20:20:59 +01007784 addGreylistPattern("(?i)[_a-z0-9]+" + maybeAs);
Anton Hansson00036fc2019-04-04 19:43:08 +01007785 addGreylistPattern("audio\\._id AS _id");
Anton Hansson459d3c72019-04-02 20:20:59 +01007786 addGreylistPattern("(?i)(min|max|sum|avg|total|count|cast)\\(([_a-z0-9]+" + maybeAs + "|\\*)\\)" + maybeAs);
7787 addGreylistPattern("case when case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added \\* \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added / \\d+ else \\d+ end > case when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified \\* \\d+ when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified / \\d+ else \\d+ end then case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added \\* \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added / \\d+ else \\d+ end else case when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified \\* \\d+ when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified / \\d+ else \\d+ end end as corrected_added_modified");
7788 addGreylistPattern("MAX\\(case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\* \\d+ when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else \\d+ end\\)");
7789 addGreylistPattern("MAX\\(case when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added \\* \\d+ when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added when \\(date_added >= \\d+ and date_added < \\d+\\) then date_added / \\d+ else \\d+ end\\)");
7790 addGreylistPattern("MAX\\(case when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified \\* \\d+ when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified when \\(date_modified >= \\d+ and date_modified < \\d+\\) then date_modified / \\d+ else \\d+ end\\)");
7791 addGreylistPattern("\"content://media/[a-z]+/audio/media\"");
7792 addGreylistPattern("substr\\(_data, length\\(_data\\)-length\\(_display_name\\), 1\\) as filename_prevchar");
7793 addGreylistPattern("\\*" + maybeAs);
Jeff Sharkey14b28872019-04-03 13:00:50 -06007794 addGreylistPattern("case when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken \\* \\d+ when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken when \\(datetaken >= \\d+ and datetaken < \\d+\\) then datetaken / \\d+ else \\d+ end");
Jeff Sharkey4230e192018-09-13 17:07:41 -06007795 }
7796
Jeff Sharkeyc55994b2019-12-20 19:43:59 -07007797 public ArrayMap<String, String> getProjectionMap(Class<?>... clazzes) {
7798 return mExternalDatabase.getProjectionMap(clazzes);
Jeff Sharkeya57867a2019-02-14 13:27:35 -07007799 }
7800
Jeff Sharkey0e880712019-02-11 11:01:31 -07007801 static <T> boolean containsAny(Set<T> a, Set<T> b) {
7802 for (T i : b) {
7803 if (a.contains(i)) {
7804 return true;
7805 }
7806 }
7807 return false;
7808 }
7809
Jeff Sharkey66881302019-10-05 10:50:06 -06007810 @VisibleForTesting
Jeff Sharkey199f8c82019-03-23 11:54:21 -06007811 static @Nullable Uri computeCommonPrefix(@NonNull List<Uri> uris) {
7812 if (uris.isEmpty()) return null;
7813
7814 final Uri base = uris.get(0);
7815 final List<String> basePath = new ArrayList<>(base.getPathSegments());
7816 for (int i = 1; i < uris.size(); i++) {
7817 final List<String> probePath = uris.get(i).getPathSegments();
7818 for (int j = 0; j < basePath.size() && j < probePath.size(); j++) {
7819 if (!Objects.equals(basePath.get(j), probePath.get(j))) {
7820 // Trim away all remaining common elements
7821 while (basePath.size() > j) {
7822 basePath.remove(j);
7823 }
7824 }
7825 }
7826
7827 final int probeSize = probePath.size();
7828 while (basePath.size() > probeSize) {
7829 basePath.remove(probeSize);
7830 }
7831 }
7832
7833 final Uri.Builder builder = base.buildUpon().path(null);
7834 for (int i = 0; i < basePath.size(); i++) {
7835 builder.appendPath(basePath.get(i));
7836 }
7837 return builder.build();
7838 }
7839
Abhijeet Kaur3bc15772021-11-17 08:40:34 +00007840 private int getCallingUidOrSelf() {
7841 return mCallingIdentity.get().uid;
7842 }
7843
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007844 @Deprecated
Jeff Sharkey4b0fb0c2015-07-27 09:50:25 -07007845 private String getCallingPackageOrSelf() {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007846 return mCallingIdentity.get().getPackageName();
Jeff Sharkey4b0fb0c2015-07-27 09:50:25 -07007847 }
7848
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007849 @Deprecated
Sahana Raob105c222020-06-17 20:18:48 +01007850 @VisibleForTesting
7851 public int getCallingPackageTargetSdkVersion() {
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007852 return mCallingIdentity.get().getTargetSdkVersion();
Jeff Sharkey94461582018-07-12 14:34:47 -06007853 }
7854
Jeff Sharkey0218c142018-10-19 15:37:00 -06007855 @Deprecated
Jeff Sharkey94461582018-07-12 14:34:47 -06007856 private boolean isCallingPackageAllowedHidden() {
Jeff Sharkey8411c402020-04-29 22:12:36 -06007857 return isCallingPackageSelf();
Jeff Sharkey0218c142018-10-19 15:37:00 -06007858 }
7859
Jeff Sharkey2b4e4bd2019-05-15 18:43:37 -06007860 @Deprecated
Jeff Sharkey8411c402020-04-29 22:12:36 -06007861 private boolean isCallingPackageSelf() {
7862 return mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF);
Jeff Sharkeyb0cab582019-04-16 12:42:42 -06007863 }
7864
Jeff Sharkey031af8d2019-04-28 11:11:30 -06007865 @Deprecated
Jeff Sharkey8411c402020-04-29 22:12:36 -06007866 private boolean isCallingPackageShell() {
7867 return mCallingIdentity.get().hasPermission(PERMISSION_IS_SHELL);
Jeff Sharkeybd262742019-12-17 16:40:29 -07007868 }
7869
7870 @Deprecated
Jeff Sharkey8411c402020-04-29 22:12:36 -06007871 private boolean isCallingPackageManager() {
7872 return mCallingIdentity.get().hasPermission(PERMISSION_IS_MANAGER);
7873 }
7874
7875 @Deprecated
7876 private boolean isCallingPackageDelegator() {
7877 return mCallingIdentity.get().hasPermission(PERMISSION_IS_DELEGATOR);
shafik575d0742019-11-25 17:02:57 +00007878 }
7879
7880 @Deprecated
7881 private boolean isCallingPackageLegacyRead() {
7882 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_READ);
Jeff Sharkey0218c142018-10-19 15:37:00 -06007883 }
7884
Jeff Sharkey8411c402020-04-29 22:12:36 -06007885 @Deprecated
7886 private boolean isCallingPackageLegacyWrite() {
7887 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE);
Sahana Rao406cf6d2020-04-08 21:52:59 +01007888 }
7889
Marco Nelissen10af34f2011-12-16 17:59:52 -08007890 @Override
7891 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
Jeff Sharkeyf05c4e72019-08-20 10:35:50 -06007892 writer.println("mThumbSize=" + mThumbSize);
Jeff Sharkeyb3e66032020-05-03 11:34:41 -06007893 synchronized (mAttachedVolumeNames) {
7894 writer.println("mAttachedVolumeNames=" + mAttachedVolumeNames);
7895 }
Jeff Sharkey5278ead2020-01-07 16:40:18 -07007896 writer.println();
Marco Nelissen988280a2012-05-15 14:19:24 -07007897
Jeff Sharkey5278ead2020-01-07 16:40:18 -07007898 Logging.dumpPersistent(writer);
Marco Nelissen10af34f2011-12-16 17:59:52 -08007899 }
The Android Open Source Project70215272009-03-03 19:32:43 -08007900}