The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2006 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.providers.media; |
| 18 | |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 19 | import static android.Manifest.permission.ACCESS_MEDIA_LOCATION; |
| 20 | import static android.app.AppOpsManager.permissionToOp; |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 21 | import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; |
| 22 | import static android.app.PendingIntent.FLAG_IMMUTABLE; |
| 23 | import static android.app.PendingIntent.FLAG_ONE_SHOT; |
Jeff Sharkey | 9c1bb3d | 2019-11-14 13:59:41 -0700 | [diff] [blame] | 24 | import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION; |
| 25 | import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS; |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 26 | import static android.content.pm.PackageManager.PERMISSION_GRANTED; |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 27 | import static android.provider.MediaStore.MATCH_DEFAULT; |
| 28 | import static android.provider.MediaStore.MATCH_EXCLUDE; |
| 29 | import static android.provider.MediaStore.MATCH_INCLUDE; |
| 30 | import static android.provider.MediaStore.MATCH_ONLY; |
| 31 | import static android.provider.MediaStore.QUERY_ARG_MATCH_FAVORITE; |
| 32 | import static android.provider.MediaStore.QUERY_ARG_MATCH_PENDING; |
| 33 | import static android.provider.MediaStore.QUERY_ARG_MATCH_TRASHED; |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 34 | import static android.provider.MediaStore.QUERY_ARG_RELATED_URI; |
Jeff Sharkey | b6781bc | 2019-07-18 18:45:52 -0600 | [diff] [blame] | 35 | import static android.provider.MediaStore.getVolumeName; |
Jeff Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 36 | |
Jeff Sharkey | c55994b | 2019-12-20 19:43:59 -0700 | [diff] [blame] | 37 | import static com.android.providers.media.DatabaseHelper.EXTERNAL_DATABASE_NAME; |
| 38 | import static com.android.providers.media.DatabaseHelper.INTERNAL_DATABASE_NAME; |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 39 | import static com.android.providers.media.LocalCallingIdentity.APPOP_REQUEST_INSTALL_PACKAGES_FOR_SHARED_UID; |
| 40 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_ACCESS_MTP; |
| 41 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_INSTALL_PACKAGES; |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 42 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_DELEGATOR; |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 43 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_GRANTED; |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 44 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_READ; |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 45 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE; |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 46 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_MANAGER; |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 47 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED; |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 48 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SELF; |
| 49 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SHELL; |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 50 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_AUDIO; |
| 51 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES; |
| 52 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO; |
| 53 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_AUDIO; |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 54 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_EXTERNAL_STORAGE; |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 55 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_IMAGES; |
| 56 | import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_VIDEO; |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 57 | import static com.android.providers.media.scan.MediaScanner.REASON_DEMAND; |
| 58 | import static com.android.providers.media.scan.MediaScanner.REASON_IDLE; |
Jeff Sharkey | e59ed6b | 2020-01-11 16:20:00 -0700 | [diff] [blame] | 59 | import static com.android.providers.media.util.DatabaseUtils.bindList; |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 60 | import static com.android.providers.media.util.FileUtils.PATTERN_PENDING_FILEPATH_FOR_SQL; |
Jeff Sharkey | e152d576 | 2019-10-11 17:14:51 -0600 | [diff] [blame] | 61 | import static com.android.providers.media.util.FileUtils.extractDisplayName; |
| 62 | import static com.android.providers.media.util.FileUtils.extractFileName; |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 63 | import static com.android.providers.media.util.FileUtils.extractOwnerPackageNameFromRelativePath; |
Jeff Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 64 | import static com.android.providers.media.util.FileUtils.extractPathOwnerPackageName; |
| 65 | import static com.android.providers.media.util.FileUtils.extractRelativePath; |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 66 | import static com.android.providers.media.util.FileUtils.extractRelativePathWithDisplayName; |
Jeff Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 67 | import static com.android.providers.media.util.FileUtils.extractTopLevelDir; |
| 68 | import static com.android.providers.media.util.FileUtils.extractVolumeName; |
Sahana Rao | f21671d | 2020-03-09 16:49:26 +0000 | [diff] [blame] | 69 | import static com.android.providers.media.util.FileUtils.getAbsoluteSanitizedPath; |
Ricky Wai | feb9d9b | 2020-04-06 19:14:46 +0100 | [diff] [blame] | 70 | import static com.android.providers.media.util.FileUtils.isDataOrObbPath; |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 71 | import static com.android.providers.media.util.FileUtils.isDataOrObbRelativePath; |
Jeff Sharkey | 5ea5c28 | 2019-12-18 14:06:28 -0700 | [diff] [blame] | 72 | import static com.android.providers.media.util.FileUtils.isDownload; |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 73 | import static com.android.providers.media.util.FileUtils.isObbOrChildRelativePath; |
Sahana Rao | f21671d | 2020-03-09 16:49:26 +0000 | [diff] [blame] | 74 | import static com.android.providers.media.util.FileUtils.sanitizePath; |
Jeff Sharkey | c55994b | 2019-12-20 19:43:59 -0700 | [diff] [blame] | 75 | import static com.android.providers.media.util.Logging.LOGV; |
| 76 | import static com.android.providers.media.util.Logging.TAG; |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 77 | |
Jeff Sharkey | 55f7690 | 2015-07-24 15:22:08 -0700 | [diff] [blame] | 78 | import android.app.AppOpsManager; |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 79 | import android.app.AppOpsManager.OnOpActiveChangedListener; |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 80 | import android.app.AppOpsManager.OnOpChangedListener; |
Jeff Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 81 | import android.app.DownloadManager; |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 82 | import android.app.PendingIntent; |
| 83 | import android.app.RecoverableSecurityException; |
| 84 | import android.app.RemoteAction; |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 85 | import android.app.compat.CompatChanges; |
| 86 | import android.compat.annotation.ChangeId; |
| 87 | import android.compat.annotation.EnabledAfter; |
Owen Lin | bdd3b83 | 2010-07-13 17:53:41 +0800 | [diff] [blame] | 88 | import android.content.BroadcastReceiver; |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 89 | import android.content.ClipData; |
Jeff Sharkey | 7ea24f2 | 2019-08-22 10:14:18 -0600 | [diff] [blame] | 90 | import android.content.ClipDescription; |
Owen Lin | bdd3b83 | 2010-07-13 17:53:41 +0800 | [diff] [blame] | 91 | import android.content.ContentProvider; |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 92 | import android.content.ContentProviderClient; |
Owen Lin | bdd3b83 | 2010-07-13 17:53:41 +0800 | [diff] [blame] | 93 | import android.content.ContentProviderOperation; |
| 94 | import android.content.ContentProviderResult; |
| 95 | import android.content.ContentResolver; |
| 96 | import android.content.ContentUris; |
| 97 | import android.content.ContentValues; |
| 98 | import android.content.Context; |
| 99 | import android.content.Intent; |
| 100 | import android.content.IntentFilter; |
| 101 | import android.content.OperationApplicationException; |
Mike Lockwood | ed9bbc4 | 2011-01-12 19:32:44 -0500 | [diff] [blame] | 102 | import android.content.SharedPreferences; |
Owen Lin | bdd3b83 | 2010-07-13 17:53:41 +0800 | [diff] [blame] | 103 | import android.content.UriMatcher; |
shafik | 77ed67b | 2020-02-06 18:27:35 +0000 | [diff] [blame] | 104 | import android.content.pm.ApplicationInfo; |
Jeff Sharkey | bd26274 | 2019-12-17 16:40:29 -0700 | [diff] [blame] | 105 | import android.content.pm.PackageInstaller.SessionInfo; |
Marco Nelissen | 3e6a428 | 2013-08-27 13:49:24 -0700 | [diff] [blame] | 106 | import android.content.pm.PackageManager; |
Marco Nelissen | 90c7da0 | 2012-02-17 09:25:39 -0800 | [diff] [blame] | 107 | import android.content.pm.PackageManager.NameNotFoundException; |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 108 | import android.content.pm.PermissionGroupInfo; |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 109 | import android.content.pm.ProviderInfo; |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 110 | import android.content.res.AssetFileDescriptor; |
Sean Stout | 247d918 | 2018-01-23 11:00:37 -0800 | [diff] [blame] | 111 | import android.content.res.Configuration; |
Daniel Lehmann | 7067650 | 2011-01-12 14:45:49 -0800 | [diff] [blame] | 112 | import android.content.res.Resources; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 113 | import android.database.Cursor; |
Marco Nelissen | 0027019 | 2010-01-08 08:35:20 -0800 | [diff] [blame] | 114 | import android.database.MatrixCursor; |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 115 | import android.database.sqlite.SQLiteConstraintException; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 116 | import android.database.sqlite.SQLiteDatabase; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 117 | import android.graphics.Bitmap; |
| 118 | import android.graphics.BitmapFactory; |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 119 | import android.graphics.drawable.Icon; |
Jeff Sharkey | 6688130 | 2019-10-05 10:50:06 -0600 | [diff] [blame] | 120 | import android.icu.util.ULocale; |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 121 | import android.media.ExifInterface; |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 122 | import android.media.ThumbnailUtils; |
Mike Lockwood | 9034578 | 2010-12-30 11:52:26 -0500 | [diff] [blame] | 123 | import android.mtp.MtpConstants; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 124 | import android.net.Uri; |
| 125 | import android.os.Binder; |
Jeff Sharkey | 4391332 | 2019-12-16 16:28:02 -0700 | [diff] [blame] | 126 | import android.os.Binder.ProxyTransactListener; |
Jean-Michel Trivi | 880dce9 | 2016-09-14 12:58:11 -0700 | [diff] [blame] | 127 | import android.os.Build; |
Marco Nelissen | 38b4364 | 2012-01-27 09:40:07 -0800 | [diff] [blame] | 128 | import android.os.Bundle; |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 129 | import android.os.CancellationSignal; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 130 | import android.os.Environment; |
Jeff Sharkey | f63882a | 2018-01-04 15:08:09 -0700 | [diff] [blame] | 131 | import android.os.IBinder; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 132 | import android.os.ParcelFileDescriptor; |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 133 | import android.os.ParcelFileDescriptor.OnCloseListener; |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 134 | import android.os.RemoteException; |
Marco Nelissen | 10af34f | 2011-12-16 17:59:52 -0800 | [diff] [blame] | 135 | import android.os.SystemClock; |
Nandana Dutt | 4f5e15a | 2019-11-29 10:45:58 +0000 | [diff] [blame] | 136 | import android.os.SystemProperties; |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 137 | import android.os.Trace; |
Jeff Sharkey | f63882a | 2018-01-04 15:08:09 -0700 | [diff] [blame] | 138 | import android.os.UserHandle; |
Mike Lockwood | c47e4f2 | 2011-05-09 19:08:21 -0700 | [diff] [blame] | 139 | import android.os.storage.StorageManager; |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 140 | import android.os.storage.StorageManager.StorageVolumeCallback; |
Mike Lockwood | 1f3014a | 2011-05-23 19:42:01 -0400 | [diff] [blame] | 141 | import android.os.storage.StorageVolume; |
Mike Lockwood | ed9bbc4 | 2011-01-12 19:32:44 -0500 | [diff] [blame] | 142 | import android.preference.PreferenceManager; |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 143 | import android.provider.BaseColumns; |
| 144 | import android.provider.Column; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 145 | import android.provider.MediaStore; |
| 146 | import android.provider.MediaStore.Audio; |
Jeff Sharkey | 6378ccb | 2019-03-20 13:47:36 -0600 | [diff] [blame] | 147 | import android.provider.MediaStore.Audio.AudioColumns; |
Marco Nelissen | 4eff7fe | 2012-04-06 12:41:41 -0700 | [diff] [blame] | 148 | import android.provider.MediaStore.Audio.Playlists; |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 149 | import android.provider.MediaStore.Downloads; |
Owen Lin | a2466a7 | 2012-04-16 17:59:00 +0800 | [diff] [blame] | 150 | import android.provider.MediaStore.Files; |
Daniel Lehmann | 7067650 | 2011-01-12 14:45:49 -0800 | [diff] [blame] | 151 | import android.provider.MediaStore.Files.FileColumns; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 152 | import android.provider.MediaStore.Images; |
Daniel Lehmann | 7067650 | 2011-01-12 14:45:49 -0800 | [diff] [blame] | 153 | import android.provider.MediaStore.Images.ImageColumns; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 154 | import android.provider.MediaStore.MediaColumns; |
| 155 | import android.provider.MediaStore.Video; |
Elliott Hughes | f3b67d5 | 2014-04-28 11:42:08 -0700 | [diff] [blame] | 156 | import android.system.ErrnoException; |
| 157 | import android.system.Os; |
| 158 | import android.system.OsConstants; |
| 159 | import android.system.StructStat; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 160 | import android.text.TextUtils; |
Marco Nelissen | 10af34f | 2011-12-16 17:59:52 -0800 | [diff] [blame] | 161 | import android.text.format.DateUtils; |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 162 | import android.util.ArrayMap; |
Jeff Sharkey | 0a0caad | 2018-08-10 14:07:57 -0600 | [diff] [blame] | 163 | import android.util.ArraySet; |
Jeff Sharkey | 5f9c079 | 2019-01-26 13:52:03 -0700 | [diff] [blame] | 164 | import android.util.DisplayMetrics; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 165 | import android.util.Log; |
Sudheer Shanka | 1a79ac0 | 2019-01-17 13:14:52 -0800 | [diff] [blame] | 166 | import android.util.LongSparseArray; |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 167 | import android.util.Size; |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 168 | import android.util.SparseArray; |
Sahana Rao | b105c22 | 2020-06-17 20:18:48 +0100 | [diff] [blame] | 169 | import android.webkit.MimeTypeMap; |
Jeff Sharkey | 556d2d9 | 2018-07-12 19:51:27 -0600 | [diff] [blame] | 170 | |
Jeff Sharkey | b6781bc | 2019-07-18 18:45:52 -0600 | [diff] [blame] | 171 | import androidx.annotation.GuardedBy; |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 172 | import androidx.annotation.Keep; |
Jeff Sharkey | b6781bc | 2019-07-18 18:45:52 -0600 | [diff] [blame] | 173 | import androidx.annotation.NonNull; |
| 174 | import androidx.annotation.Nullable; |
| 175 | import androidx.annotation.VisibleForTesting; |
| 176 | |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 177 | import com.android.providers.media.DatabaseHelper.OnFilesChangeListener; |
Jeff Sharkey | d5a4292 | 2020-03-06 14:42:12 -0700 | [diff] [blame] | 178 | import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener; |
Zim | edbe69e | 2019-12-13 18:49:36 +0000 | [diff] [blame] | 179 | import com.android.providers.media.fuse.ExternalStorageServiceImpl; |
| 180 | import com.android.providers.media.fuse.FuseDaemon; |
Jeff Sharkey | f52c812 | 2020-03-28 10:27:41 -0600 | [diff] [blame] | 181 | import com.android.providers.media.playlist.Playlist; |
Jeff Sharkey | 10b4d8d | 2019-02-04 21:53:22 -0700 | [diff] [blame] | 182 | import com.android.providers.media.scan.MediaScanner; |
Jeff Sharkey | 99a4828 | 2019-03-22 15:11:53 -0600 | [diff] [blame] | 183 | import com.android.providers.media.scan.ModernMediaScanner; |
Jeff Sharkey | f05c4e7 | 2019-08-20 10:35:50 -0600 | [diff] [blame] | 184 | import com.android.providers.media.util.BackgroundThread; |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 185 | import com.android.providers.media.util.CachedSupplier; |
Jeff Sharkey | 7ea24f2 | 2019-08-22 10:14:18 -0600 | [diff] [blame] | 186 | import com.android.providers.media.util.DatabaseUtils; |
| 187 | import com.android.providers.media.util.FileUtils; |
Jeff Sharkey | e04e2c6 | 2020-03-05 10:53:33 -0700 | [diff] [blame] | 188 | import com.android.providers.media.util.ForegroundThread; |
Jeff Sharkey | 60ca298 | 2019-05-11 13:44:09 -0600 | [diff] [blame] | 189 | import com.android.providers.media.util.IsoInterface; |
Jeff Sharkey | 5278ead | 2020-01-07 16:40:18 -0700 | [diff] [blame] | 190 | import com.android.providers.media.util.Logging; |
Jeff Sharkey | f05c4e7 | 2019-08-20 10:35:50 -0600 | [diff] [blame] | 191 | import com.android.providers.media.util.LongArray; |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 192 | import com.android.providers.media.util.Metrics; |
Jeff Sharkey | e152d576 | 2019-10-11 17:14:51 -0600 | [diff] [blame] | 193 | import com.android.providers.media.util.MimeUtils; |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 194 | import com.android.providers.media.util.PermissionUtils; |
Jeff Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 195 | import com.android.providers.media.util.RedactingFileDescriptor; |
Jeff Sharkey | fe66ae3 | 2020-01-11 14:36:53 -0700 | [diff] [blame] | 196 | import com.android.providers.media.util.SQLiteQueryBuilder; |
Anton Hansson | 7efb82f | 2019-05-20 10:37:27 +0100 | [diff] [blame] | 197 | import com.android.providers.media.util.XmpInterface; |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 198 | |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 199 | import com.google.common.hash.Hashing; |
| 200 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 201 | import java.io.File; |
Marco Nelissen | 10af34f | 2011-12-16 17:59:52 -0800 | [diff] [blame] | 202 | import java.io.FileDescriptor; |
Jeff Sharkey | 60ca298 | 2019-05-11 13:44:09 -0600 | [diff] [blame] | 203 | import java.io.FileInputStream; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 204 | import java.io.FileNotFoundException; |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 205 | import java.io.FileOutputStream; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 206 | import java.io.IOException; |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 207 | import java.io.OutputStream; |
Marco Nelissen | 10af34f | 2011-12-16 17:59:52 -0800 | [diff] [blame] | 208 | import java.io.PrintWriter; |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 209 | import java.nio.charset.StandardCharsets; |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 210 | import java.nio.file.Path; |
Marco Nelissen | cb0c5a6 | 2009-12-08 13:44:19 -0800 | [diff] [blame] | 211 | import java.util.ArrayList; |
Jeff Sharkey | 31d03d1 | 2018-08-07 11:26:19 -0600 | [diff] [blame] | 212 | import java.util.Arrays; |
Marco Nelissen | 10af34f | 2011-12-16 17:59:52 -0800 | [diff] [blame] | 213 | import java.util.Collection; |
Marco Nelissen | f5f9eca | 2009-12-09 09:26:15 -0800 | [diff] [blame] | 214 | import java.util.List; |
Marco Nelissen | 38b4364 | 2012-01-27 09:40:07 -0800 | [diff] [blame] | 215 | import java.util.Locale; |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 216 | import java.util.Map; |
Jeff Sharkey | fac4821 | 2018-10-18 09:44:45 -0600 | [diff] [blame] | 217 | import java.util.Objects; |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 218 | import java.util.Optional; |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 219 | import java.util.Set; |
Jeff Sharkey | e987615 | 2018-12-08 11:14:13 -0700 | [diff] [blame] | 220 | import java.util.function.Consumer; |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 221 | import java.util.function.Supplier; |
Sahana Rao | 71403c0 | 2020-03-25 17:13:40 +0000 | [diff] [blame] | 222 | import java.util.function.UnaryOperator; |
Jeff Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 223 | import java.util.regex.Matcher; |
| 224 | import java.util.regex.Pattern; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 225 | |
| 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 | */ |
| 232 | public class MediaProvider extends ContentProvider { |
Jeff Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 233 | /** |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 234 | * 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 Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 241 | * Regex of a selection string that matches a specific ID. |
| 242 | */ |
Jeff Sharkey | c9ae859 | 2019-10-07 11:41:04 -0600 | [diff] [blame] | 243 | static final Pattern PATTERN_SELECTION_ID = Pattern.compile( |
Jeff Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 244 | "(?:image_id|video_id)\\s*=\\s*(\\d+)"); |
| 245 | |
| 246 | /** |
Nandana Dutt | 4f5e15a | 2019-11-29 10:45:58 +0000 | [diff] [blame] | 247 | * Property that indicates whether fuse is enabled. |
| 248 | */ |
| 249 | private static final String PROP_FUSE = "persist.sys.fuse"; |
| 250 | |
| 251 | /** |
shafik | a32e93c | 2019-11-01 12:17:34 +0000 | [diff] [blame] | 252 | * 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 Rao | 7169344 | 2019-11-13 13:48:07 +0000 | [diff] [blame] | 267 | private static final String DIRECTORY_ANDROID = "Android"; |
shafik | f0fea69 | 2020-02-14 15:49:17 +0000 | [diff] [blame] | 268 | |
Sahana Rao | 7169344 | 2019-11-13 13:48:07 +0000 | [diff] [blame] | 269 | private static final String DIRECTORY_MEDIA = "media"; |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 270 | 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"; |
shafik | a32e93c | 2019-11-01 12:17:34 +0000 | [diff] [blame] | 278 | |
| 279 | /** |
shafik | ac34fe9 | 2020-02-25 15:28:55 +0000 | [diff] [blame] | 280 | * 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 Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 289 | * Value indicating that operations should include database rows matching the criteria defined |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 290 | * 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 Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 292 | * <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 Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 296 | private static final int MATCH_VISIBLE_FOR_FILEPATH = 32; |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 297 | |
| 298 | /** |
Sahana Rao | b02e715 | 2020-06-12 17:07:31 +0100 | [diff] [blame] | 299 | * 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 Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 306 | * Set of {@link Cursor} columns that refer to raw filesystem paths. |
Jeff Sharkey | 16fb805 | 2018-10-18 15:22:53 -0600 | [diff] [blame] | 307 | */ |
Jeff Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 308 | private static final ArrayMap<String, Object> sDataColumns = new ArrayMap<>(); |
Jeff Sharkey | 16fb805 | 2018-10-18 15:22:53 -0600 | [diff] [blame] | 309 | |
| 310 | { |
Jeff Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 311 | 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 Sharkey | 16fb805 | 2018-10-18 15:22:53 -0600 | [diff] [blame] | 316 | } |
| 317 | |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 318 | private static final Object sCacheLock = new Object(); |
Jeff Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 319 | |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 320 | @GuardedBy("sCacheLock") |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 321 | private static final Set<String> sCachedExternalVolumeNames = new ArraySet<>(); |
| 322 | @GuardedBy("sCacheLock") |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 323 | private static final Map<String, File> sCachedVolumePaths = new ArrayMap<>(); |
| 324 | @GuardedBy("sCacheLock") |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 325 | private static final Map<String, Collection<File>> sCachedVolumeScanPaths = new ArrayMap<>(); |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 326 | @GuardedBy("sCacheLock") |
| 327 | private static final ArrayMap<File, String> sCachedVolumePathToId = new ArrayMap<>(); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 328 | |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 329 | /** |
| 330 | * Please use {@link getDownloadsProviderAuthority()} instead of using this directly. |
| 331 | */ |
| 332 | private static final String DOWNLOADS_PROVIDER_AUTHORITY = "downloads"; |
| 333 | |
Zim | 7d249ef | 2020-05-26 13:55:56 +0100 | [diff] [blame] | 334 | @GuardedBy("mShouldRedactThreadIds") |
| 335 | private final LongArray mShouldRedactThreadIds = new LongArray(); |
| 336 | |
Abhijeet Kaur | 2598beb | 2020-03-23 14:31:58 +0000 | [diff] [blame] | 337 | public void updateVolumes() { |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 338 | synchronized (sCacheLock) { |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 339 | sCachedExternalVolumeNames.clear(); |
| 340 | sCachedExternalVolumeNames.addAll(MediaStore.getExternalVolumeNames(getContext())); |
Jeff Sharkey | 7447230 | 2020-06-04 17:35:44 -0600 | [diff] [blame] | 341 | Log.v(TAG, "Updated external volumes to: " + sCachedExternalVolumeNames.toString()); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 342 | |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 343 | sCachedVolumePaths.clear(); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 344 | sCachedVolumeScanPaths.clear(); |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 345 | sCachedVolumePathToId.clear(); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 346 | try { |
| 347 | sCachedVolumeScanPaths.put(MediaStore.VOLUME_INTERNAL, |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 348 | FileUtils.getVolumeScanPaths(getContext(), MediaStore.VOLUME_INTERNAL)); |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 349 | } 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 Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 355 | final Uri uri = MediaStore.Files.getContentUri(volumeName); |
| 356 | final StorageVolume volume = mStorageManager.getStorageVolume(uri); |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 357 | sCachedVolumePaths.put(volumeName, volume.getDirectory()); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 358 | sCachedVolumeScanPaths.put(volumeName, |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 359 | FileUtils.getVolumeScanPaths(getContext(), volumeName)); |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 360 | sCachedVolumePathToId.put(volume.getDirectory(), volume.getId()); |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 361 | } catch (IllegalStateException | FileNotFoundException e) { |
| 362 | Log.wtf(TAG, "Failed to update volume " + volumeName, e); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 363 | } |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 364 | } |
| 365 | } |
Jeff Sharkey | e59ed6b | 2020-01-11 16:20:00 -0700 | [diff] [blame] | 366 | |
| 367 | // Update filters to reflect mounted volumes so users don't get |
| 368 | // confused by metadata from ejected volumes |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 369 | ForegroundThread.getExecutor().execute(() -> { |
Jeff Sharkey | e59ed6b | 2020-01-11 16:20:00 -0700 | [diff] [blame] | 370 | mExternalDatabase.setFilterVolumeNames(getExternalVolumeNames()); |
| 371 | }); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 372 | } |
| 373 | |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 374 | public @NonNull File getVolumePath(@NonNull String volumeName) throws FileNotFoundException { |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 375 | // 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 Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 381 | synchronized (sCacheLock) { |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 382 | if (sCachedVolumePaths.containsKey(volumeName)) { |
| 383 | return sCachedVolumePaths.get(volumeName); |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 384 | } |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 385 | |
| 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 Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 389 | return res; |
| 390 | } |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 391 | } |
| 392 | |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 393 | 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 Sharkey | f06febd | 2020-04-07 13:03:30 -0600 | [diff] [blame] | 403 | if (volume == null) { |
| 404 | throw new FileNotFoundException("Missing volume for " + file); |
| 405 | } |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 406 | sCachedVolumePathToId.put(volume.getDirectory(), volume.getId()); |
| 407 | return volume.getId(); |
| 408 | } |
| 409 | } |
| 410 | |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 411 | public @NonNull Set<String> getExternalVolumeNames() { |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 412 | synchronized (sCacheLock) { |
| 413 | return new ArraySet<>(sCachedExternalVolumeNames); |
| 414 | } |
| 415 | } |
| 416 | |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 417 | public @NonNull Collection<File> getVolumeScanPaths(String volumeName) |
| 418 | throws FileNotFoundException { |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 419 | synchronized (sCacheLock) { |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 420 | 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 Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 428 | } |
| 429 | } |
| 430 | |
Jeff Sharkey | 5d36def | 2013-10-16 16:35:29 -0700 | [diff] [blame] | 431 | private StorageManager mStorageManager; |
Jeff Sharkey | 55f7690 | 2015-07-24 15:22:08 -0700 | [diff] [blame] | 432 | private AppOpsManager mAppOpsManager; |
Sean Stout | cceb5e4 | 2017-09-08 11:16:00 -0700 | [diff] [blame] | 433 | private PackageManager mPackageManager; |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 434 | private int mExternalStorageAuthorityAppId; |
| 435 | private int mDownloadsAuthorityAppId; |
Jeff Sharkey | 5d36def | 2013-10-16 16:35:29 -0700 | [diff] [blame] | 436 | |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 437 | private Size mThumbSize; |
| 438 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 439 | /** |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 440 | * Map from UID to cached {@link LocalCallingIdentity}. Values are only |
Zimuzo Ezeozue | ee10fb7 | 2020-03-19 20:26:37 +0000 | [diff] [blame] | 441 | * maintained in this map while the UID is actively working with a |
| 442 | * performance-critical component, such as camera. |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 443 | */ |
| 444 | @GuardedBy("mCachedCallingIdentity") |
| 445 | private final SparseArray<LocalCallingIdentity> mCachedCallingIdentity = new SparseArray<>(); |
| 446 | |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 447 | private final OnOpActiveChangedListener mActiveListener = (code, uid, packageName, active) -> { |
| 448 | synchronized (mCachedCallingIdentity) { |
| 449 | if (active) { |
Philip P. Moltmann | 05b68ea | 2019-09-27 13:33:11 -0700 | [diff] [blame] | 450 | // TODO moltmann: Set correct featureId |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 451 | mCachedCallingIdentity.put(uid, |
Philip P. Moltmann | 05b68ea | 2019-09-27 13:33:11 -0700 | [diff] [blame] | 452 | LocalCallingIdentity.fromExternal(getContext(), uid, packageName, |
| 453 | null)); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 454 | } else { |
| 455 | mCachedCallingIdentity.remove(uid); |
| 456 | } |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 457 | } |
| 458 | }; |
| 459 | |
Zimuzo Ezeozue | ee10fb7 | 2020-03-19 20:26:37 +0000 | [diff] [blame] | 460 | /** |
| 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 | |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 469 | private OnOpChangedListener mModeListener = |
Zimuzo Ezeozue | ee10fb7 | 2020-03-19 20:26:37 +0000 | [diff] [blame] | 470 | (op, packageName) -> invalidateLocalCallingIdentityCache(packageName, "op " + op); |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 471 | |
shafik | 60ed701 | 2020-04-24 20:10:33 +0100 | [diff] [blame] | 472 | /** |
| 473 | * Retrieves a cached calling identity or creates a new one. Also, always sets the app-op |
| 474 | * description for the calling identity. |
| 475 | */ |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 476 | private LocalCallingIdentity getCachedCallingIdentityForFuse(int uid) { |
Zimuzo Ezeozue | ee10fb7 | 2020-03-19 20:26:37 +0000 | [diff] [blame] | 477 | synchronized (mCachedCallingIdentityForFuse) { |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 478 | PermissionUtils.setOpDescription("via FUSE"); |
Zimuzo Ezeozue | ee10fb7 | 2020-03-19 20:26:37 +0000 | [diff] [blame] | 479 | LocalCallingIdentity ident = mCachedCallingIdentityForFuse.get(uid); |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 480 | if (ident == null) { |
Zimuzo Ezeozue | ee10fb7 | 2020-03-19 20:26:37 +0000 | [diff] [blame] | 481 | ident = LocalCallingIdentity.fromExternal(getContext(), uid); |
| 482 | mCachedCallingIdentityForFuse.put(uid, ident); |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 483 | } |
| 484 | return ident; |
| 485 | } |
| 486 | } |
| 487 | |
| 488 | /** |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 489 | * 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 Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 492 | */ |
| 493 | private final ThreadLocal<LocalCallingIdentity> mCallingIdentity = ThreadLocal |
Zimuzo Ezeozue | ee10fb7 | 2020-03-19 20:26:37 +0000 | [diff] [blame] | 494 | .withInitial(() -> { |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 495 | PermissionUtils.setOpDescription("via MediaProvider"); |
Zimuzo Ezeozue | ee10fb7 | 2020-03-19 20:26:37 +0000 | [diff] [blame] | 496 | synchronized (mCachedCallingIdentity) { |
| 497 | final LocalCallingIdentity cached = mCachedCallingIdentity |
| 498 | .get(Binder.getCallingUid()); |
| 499 | return (cached != null) ? cached |
| 500 | : LocalCallingIdentity.fromBinder(getContext(), this); |
| 501 | } |
| 502 | }); |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 503 | |
Jeff Sharkey | 4391332 | 2019-12-16 16:28:02 -0700 | [diff] [blame] | 504 | /** |
| 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 Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 512 | if (LOGV) Trace.beginSection(Thread.currentThread().getStackTrace()[5].getMethodName()); |
| 513 | return Binder.setCallingWorkSourceUid(mCallingIdentity.get().uid); |
Jeff Sharkey | 4391332 | 2019-12-16 16:28:02 -0700 | [diff] [blame] | 514 | } |
| 515 | |
| 516 | @Override |
| 517 | public void onTransactEnded(Object session) { |
| 518 | final long token = (long) session; |
| 519 | Binder.restoreCallingWorkSource(token); |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 520 | if (LOGV) Trace.endSection(); |
Jeff Sharkey | 4391332 | 2019-12-16 16:28:02 -0700 | [diff] [blame] | 521 | } |
| 522 | }; |
| 523 | |
Marco Nelissen | 7f36494 | 2011-12-12 14:32:49 -0800 | [diff] [blame] | 524 | // In memory cache of path<->id mappings, to speed up inserts during media scan |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 525 | @GuardedBy("mDirectoryCache") |
| 526 | private final ArrayMap<String, Long> mDirectoryCache = new ArrayMap<>(); |
Marco Nelissen | 7f36494 | 2011-12-12 14:32:49 -0800 | [diff] [blame] | 527 | |
Marco Nelissen | 1662045 | 2012-02-03 12:45:44 -0800 | [diff] [blame] | 528 | private static final String[] sDataOnlyColumn = new String[] { |
| 529 | FileColumns.DATA |
| 530 | }; |
| 531 | |
Chong Zhang | eb5f7a6 | 2016-08-31 21:25:15 -0700 | [diff] [blame] | 532 | private static final String ID_NOT_PARENT_CLAUSE = |
Jeff Sharkey | acc3b58 | 2019-12-07 11:52:32 -0700 | [diff] [blame] | 533 | "_id NOT IN (SELECT parent FROM files WHERE parent IS NOT NULL)"; |
Chong Zhang | eb5f7a6 | 2016-08-31 21:25:15 -0700 | [diff] [blame] | 534 | |
Marco Nelissen | 01e706a | 2013-09-12 15:38:42 -0700 | [diff] [blame] | 535 | private static final String CANONICAL = "canonical"; |
| 536 | |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 537 | 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 Ezeozue | ee10fb7 | 2020-03-19 20:26:37 +0000 | [diff] [blame] | 546 | invalidateLocalCallingIdentityCache(pkg, "package " + intent.getAction()); |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 547 | } else { |
| 548 | Log.w(TAG, "Failed to retrieve package from intent: " + intent.getAction()); |
| 549 | } |
| 550 | break; |
| 551 | } |
| 552 | } |
| 553 | }; |
| 554 | |
Zimuzo Ezeozue | ee10fb7 | 2020-03-19 20:26:37 +0000 | [diff] [blame] | 555 | 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) { |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 563 | } |
| 564 | } |
| 565 | } |
| 566 | |
Martijn Coenen | 49971b1 | 2020-02-18 08:59:29 +0100 | [diff] [blame] | 567 | private final void updateQuotaTypeForUri(@NonNull Uri uri, int mediaType) { |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 568 | Trace.beginSection("updateQuotaTypeForUri"); |
Martijn Coenen | 49971b1 | 2020-02-18 08:59:29 +0100 | [diff] [blame] | 569 | File file; |
| 570 | try { |
| 571 | file = queryForDataFile(uri, null); |
Martijn Coenen | c55e107 | 2020-06-10 14:48:00 +0200 | [diff] [blame] | 572 | if (!file.exists()) { |
| 573 | // This can happen if an item is inserted in MediaStore before it is created |
| 574 | return; |
| 575 | } |
Martijn Coenen | af2d34d | 2020-06-19 12:52:20 +0200 | [diff] [blame] | 576 | |
| 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 Coenen | 49971b1 | 2020-02-18 08:59:29 +0100 | [diff] [blame] | 585 | } catch (FileNotFoundException e) { |
| 586 | // Ignore |
| 587 | return; |
Martijn Coenen | af2d34d | 2020-06-19 12:52:20 +0200 | [diff] [blame] | 588 | } finally { |
| 589 | Trace.endSection(); |
Martijn Coenen | 49971b1 | 2020-02-18 08:59:29 +0100 | [diff] [blame] | 590 | } |
Martijn Coenen | af2d34d | 2020-06-19 12:52:20 +0200 | [diff] [blame] | 591 | } |
| 592 | |
| 593 | private final void updateQuotaTypeForFileInternal(File file, int mediaType) { |
Martijn Coenen | 49971b1 | 2020-02-18 08:59:29 +0100 | [diff] [blame] | 594 | 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 Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 618 | /** |
| 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 Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 624 | 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 Rao | a211c4e | 2020-03-23 02:59:33 +0000 | [diff] [blame] | 628 | handleInsertedRowForFuse(id); |
Jeff Sharkey | e04e2c6 | 2020-03-05 10:53:33 -0700 | [diff] [blame] | 629 | acceptWithExpansion(helper::notifyInsert, volumeName, id, mediaType, isDownload); |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 630 | |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 631 | 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 Coenen | 49971b1 | 2020-02-18 08:59:29 +0100 | [diff] [blame] | 637 | |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 638 | // Tell our SAF provider so it knows when views are no longer empty |
| 639 | MediaDocumentsProvider.onMediaStoreInsert(getContext(), volumeName, mediaType, id); |
| 640 | }); |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 641 | } |
| 642 | |
| 643 | @Override |
Sahana Rao | a211c4e | 2020-03-23 02:59:33 +0000 | [diff] [blame] | 644 | public void onUpdate(@NonNull DatabaseHelper helper, @NonNull String volumeName, |
| 645 | long oldId, int oldMediaType, boolean oldIsDownload, |
| 646 | long newId, int newMediaType, boolean newIsDownload, |
Sahana Rao | 1e8271b | 2020-04-03 14:01:08 +0100 | [diff] [blame] | 647 | String oldOwnerPackage, String newOwnerPackage, String oldPath) { |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 648 | final boolean isDownload = oldIsDownload || newIsDownload; |
Martijn Coenen | c55e107 | 2020-06-10 14:48:00 +0200 | [diff] [blame] | 649 | final Uri fileUri = MediaStore.Files.getContentUri(volumeName, oldId); |
Sahana Rao | 1e8271b | 2020-04-03 14:01:08 +0100 | [diff] [blame] | 650 | handleUpdatedRowForFuse(oldPath, oldOwnerPackage, oldId, newId); |
| 651 | handleOwnerPackageNameChange(oldPath, oldOwnerPackage, newOwnerPackage); |
Sahana Rao | a211c4e | 2020-03-23 02:59:33 +0000 | [diff] [blame] | 652 | acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, oldMediaType, isDownload); |
| 653 | |
Martijn Coenen | c55e107 | 2020-06-10 14:48:00 +0200 | [diff] [blame] | 654 | helper.postBackground(() -> { |
| 655 | if (helper.isExternal()) { |
| 656 | // Update the quota type on the filesystem |
| 657 | updateQuotaTypeForUri(fileUri, newMediaType); |
| 658 | } |
| 659 | }); |
| 660 | |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 661 | if (newMediaType != oldMediaType) { |
Sahana Rao | a211c4e | 2020-03-23 02:59:33 +0000 | [diff] [blame] | 662 | acceptWithExpansion(helper::notifyUpdate, volumeName, oldId, newMediaType, |
| 663 | isDownload); |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 664 | |
| 665 | helper.postBackground(() -> { |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 666 | // Invalidate any thumbnails when the media type changes |
| 667 | invalidateThumbnails(fileUri); |
| 668 | }); |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 669 | } |
| 670 | } |
| 671 | |
| 672 | @Override |
| 673 | public void onDelete(@NonNull DatabaseHelper helper, @NonNull String volumeName, long id, |
Sahana Rao | a211c4e | 2020-03-23 02:59:33 +0000 | [diff] [blame] | 674 | int mediaType, boolean isDownload, String ownerPackageName, String path) { |
| 675 | handleDeletedRowForFuse(path, ownerPackageName, id); |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 676 | acceptWithExpansion(helper::notifyDelete, volumeName, id, mediaType, isDownload); |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 677 | |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 678 | 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 Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 688 | |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 689 | // 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 Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 695 | } |
| 696 | }; |
| 697 | |
Sahana Rao | 71403c0 | 2020-03-25 17:13:40 +0000 | [diff] [blame] | 698 | 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 Sharkey | d5a4292 | 2020-03-06 14:42:12 -0700 | [diff] [blame] | 706 | 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 Sharkey | 9aca51f | 2020-04-29 11:28:08 -0600 | [diff] [blame] | 713 | 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 Sharkey | d5a4292 | 2020-03-06 14:42:12 -0700 | [diff] [blame] | 719 | public void onFinished(ContentProviderClient client, String volumeName) { |
| 720 | MediaStore.finishLegacyMigration(ContentResolver.wrap(client), volumeName); |
| 721 | } |
| 722 | }; |
| 723 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 724 | /** |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 725 | * Apply {@link Consumer#accept} to the given item. |
Jeff Sharkey | d630264 | 2019-04-28 13:30:23 -0600 | [diff] [blame] | 726 | * <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 Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 731 | 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 Sharkey | d630264 | 2019-04-28 13:30:23 -0600 | [diff] [blame] | 736 | |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 737 | // Any changing audio items mean we probably need to invalidate all |
| 738 | // indexed views built from that media |
Jeff Sharkey | d630264 | 2019-04-28 13:30:23 -0600 | [diff] [blame] | 739 | 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 Sharkey | 1168535 | 2019-05-14 10:48:07 -0600 | [diff] [blame] | 743 | break; |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 744 | |
| 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 Sharkey | d630264 | 2019-04-28 13:30:23 -0600 | [diff] [blame] | 770 | } |
| 771 | } |
| 772 | |
Mike Lockwood | ed9bbc4 | 2011-01-12 19:32:44 -0500 | [diff] [blame] | 773 | private static final String[] sDefaultFolderNames = { |
shafik | f0fea69 | 2020-02-14 15:49:17 +0000 | [diff] [blame] | 774 | 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, |
shafik | 0382d04 | 2020-03-03 20:16:24 +0000 | [diff] [blame] | 783 | Environment.DIRECTORY_AUDIOBOOKS, |
| 784 | Environment.DIRECTORY_DOCUMENTS, |
Mike Lockwood | ed9bbc4 | 2011-01-12 19:32:44 -0500 | [diff] [blame] | 785 | }; |
| 786 | |
shafik | f0fea69 | 2020-02-14 15:49:17 +0000 | [diff] [blame] | 787 | 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 Sharkey | 72613f7 | 2015-08-19 14:18:19 -0700 | [diff] [blame] | 796 | /** |
| 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 Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 801 | private void ensureDefaultFolders(@NonNull String volumeName, @NonNull SQLiteDatabase db) { |
Jeff Sharkey | 7eed0eb | 2019-01-19 17:27:46 -0700 | [diff] [blame] | 802 | try { |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 803 | final File path = getVolumePath(volumeName); |
Jeff Sharkey | 7eed0eb | 2019-01-19 17:27:46 -0700 | [diff] [blame] | 804 | final StorageVolume vol = mStorageManager.getStorageVolume(path); |
| 805 | final String key; |
Zim | 2697d47 | 2020-02-19 14:27:11 +0000 | [diff] [blame] | 806 | if (vol == null) { |
| 807 | Log.w(TAG, "Failed to ensure default folders for " + volumeName); |
| 808 | return; |
| 809 | } |
| 810 | |
| 811 | if (vol.isPrimary()) { |
Jeff Sharkey | 7eed0eb | 2019-01-19 17:27:46 -0700 | [diff] [blame] | 812 | key = "created_default_folders"; |
| 813 | } else { |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 814 | key = "created_default_folders_" + vol.getMediaStoreVolumeName(); |
Mike Lockwood | ed9bbc4 | 2011-01-12 19:32:44 -0500 | [diff] [blame] | 815 | } |
| 816 | |
Jeff Sharkey | 7eed0eb | 2019-01-19 17:27:46 -0700 | [diff] [blame] | 817 | final SharedPreferences prefs = PreferenceManager |
| 818 | .getDefaultSharedPreferences(getContext()); |
| 819 | if (prefs.getInt(key, 0) == 0) { |
| 820 | for (String folderName : sDefaultFolderNames) { |
Jeff Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 821 | final File folder = new File(vol.getDirectory(), folderName); |
Jeff Sharkey | 7eed0eb | 2019-01-19 17:27:46 -0700 | [diff] [blame] | 822 | if (!folder.exists()) { |
| 823 | folder.mkdirs(); |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 824 | insertDirectory(db, folder.getAbsolutePath()); |
Jeff Sharkey | 7eed0eb | 2019-01-19 17:27:46 -0700 | [diff] [blame] | 825 | } |
| 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 Lockwood | ed9bbc4 | 2011-01-12 19:32:44 -0500 | [diff] [blame] | 834 | } |
| 835 | } |
| 836 | |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 837 | /** |
| 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 Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 843 | private void ensureThumbnailsValid(@NonNull String volumeName, @NonNull SQLiteDatabase db) { |
| 844 | final String uuidFromDatabase = DatabaseHelper.getOrCreateUuid(db); |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 845 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 878 | @Override |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 879 | public void attachInfo(Context context, ProviderInfo info) { |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 880 | Log.v(TAG, "Attached " + info.authority + " from " + info.applicationInfo.packageName); |
| 881 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 882 | mUriMatcher = new LocalUriMatcher(info.authority); |
Jeff Sharkey | 56c34e8 | 2019-11-21 15:56:37 -0700 | [diff] [blame] | 883 | |
| 884 | super.attachInfo(context, info); |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 885 | } |
| 886 | |
| 887 | @Override |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 888 | public boolean onCreate() { |
Mike Lockwood | d186c64 | 2010-07-14 15:37:42 -0400 | [diff] [blame] | 889 | final Context context = getContext(); |
| 890 | |
Jeff Sharkey | 7873f54 | 2019-05-30 12:18:38 -0600 | [diff] [blame] | 891 | // Shift call statistics back to the original caller |
Jeff Sharkey | 4391332 | 2019-12-16 16:28:02 -0700 | [diff] [blame] | 892 | Binder.setProxyTransactListener(mTransactListener); |
Jeff Sharkey | 7873f54 | 2019-05-30 12:18:38 -0600 | [diff] [blame] | 893 | |
Jeff Sharkey | 55f7690 | 2015-07-24 15:22:08 -0700 | [diff] [blame] | 894 | mStorageManager = context.getSystemService(StorageManager.class); |
| 895 | mAppOpsManager = context.getSystemService(AppOpsManager.class); |
Sean Stout | cceb5e4 | 2017-09-08 11:16:00 -0700 | [diff] [blame] | 896 | mPackageManager = context.getPackageManager(); |
Jeff Sharkey | 5d36def | 2013-10-16 16:35:29 -0700 | [diff] [blame] | 897 | |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 898 | // Reasonable thumbnail size is half of the smallest screen edge width |
Jeff Sharkey | 5f9c079 | 2019-01-26 13:52:03 -0700 | [diff] [blame] | 899 | final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); |
| 900 | final int thumbSize = Math.min(metrics.widthPixels, metrics.heightPixels) / 2; |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 901 | mThumbSize = new Size(thumbSize, thumbSize); |
| 902 | |
Jeff Sharkey | e365064 | 2020-04-03 18:50:03 -0600 | [diff] [blame] | 903 | mMediaScanner = new ModernMediaScanner(context); |
Jeff Sharkey | 85acbbe | 2019-10-15 17:10:30 -0600 | [diff] [blame] | 904 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 905 | mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, |
Jeff Sharkey | e365064 | 2020-04-03 18:50:03 -0600 | [diff] [blame] | 906 | true, false, false, Column.class, |
Sahana Rao | 71403c0 | 2020-03-25 17:13:40 +0000 | [diff] [blame] | 907 | Metrics::logSchemaChange, mFilesListener, mMigrationListener, mIdGenerator); |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 908 | mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, |
Jeff Sharkey | e365064 | 2020-04-03 18:50:03 -0600 | [diff] [blame] | 909 | false, false, false, Column.class, |
Sahana Rao | 71403c0 | 2020-03-25 17:13:40 +0000 | [diff] [blame] | 910 | Metrics::logSchemaChange, mFilesListener, mMigrationListener, mIdGenerator); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 911 | |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 912 | final IntentFilter packageFilter = new IntentFilter(); |
| 913 | packageFilter.setPriority(10); |
Abhijeet Kaur | 2598beb | 2020-03-23 14:31:58 +0000 | [diff] [blame] | 914 | packageFilter.addDataScheme("package"); |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 915 | packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); |
| 916 | packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); |
| 917 | context.registerReceiver(mPackageReceiver, packageFilter); |
| 918 | |
Zim | 604f452 | 2020-06-05 15:30:09 +0100 | [diff] [blame] | 919 | // 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 | |
Zim | 4935428 | 2019-09-09 13:52:39 +0100 | [diff] [blame] | 928 | updateVolumes(); |
Zim | 604f452 | 2020-06-05 15:30:09 +0100 | [diff] [blame] | 929 | attachVolume(MediaStore.VOLUME_INTERNAL, /* validate */ false); |
Zim | 4935428 | 2019-09-09 13:52:39 +0100 | [diff] [blame] | 930 | for (String volumeName : getExternalVolumeNames()) { |
Zim | 604f452 | 2020-06-05 15:30:09 +0100 | [diff] [blame] | 931 | attachVolume(volumeName, /* validate */ false); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 932 | } |
| 933 | |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 934 | // Watch for performance-sensitive activity |
Jeff Sharkey | 7ea24f2 | 2019-08-22 10:14:18 -0600 | [diff] [blame] | 935 | mAppOpsManager.startWatchingActive(new String[] { |
| 936 | AppOpsManager.OPSTR_CAMERA |
| 937 | }, context.getMainExecutor(), mActiveListener); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 938 | |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 939 | 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 Ioffe | a9551ab | 2020-06-22 14:22:23 +0100 | [diff] [blame] | 956 | 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 Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 967 | |
| 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 980 | return true; |
| 981 | } |
| 982 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 983 | @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 Sharkey | 667618e | 2019-08-20 08:46:35 -0600 | [diff] [blame] | 990 | return clearLocalCallingIdentity(LocalCallingIdentity.fromSelf(getContext())); |
Jeff Sharkey | 0ee9741 | 2019-05-20 14:00:12 -0600 | [diff] [blame] | 991 | } |
| 992 | |
| 993 | public LocalCallingIdentity clearLocalCallingIdentity(LocalCallingIdentity replacement) { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 994 | final LocalCallingIdentity token = mCallingIdentity.get(); |
Jeff Sharkey | 0ee9741 | 2019-05-20 14:00:12 -0600 | [diff] [blame] | 995 | mCallingIdentity.set(replacement); |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 996 | return token; |
| 997 | } |
| 998 | |
| 999 | public void restoreLocalCallingIdentity(LocalCallingIdentity token) { |
| 1000 | mCallingIdentity.set(token); |
| 1001 | } |
| 1002 | |
Jeff Sharkey | bd26274 | 2019-12-17 16:40:29 -0700 | [diff] [blame] | 1003 | 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 Sharkey | 99a4828 | 2019-03-22 15:11:53 -0600 | [diff] [blame] | 1024 | public void onIdleMaintenance(@NonNull CancellationSignal signal) { |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 1025 | final long startTime = SystemClock.elapsedRealtime(); |
| 1026 | |
Jeff Sharkey | 5278ead | 2020-01-07 16:40:18 -0700 | [diff] [blame] | 1027 | // Trim any stale log files before we emit new events below |
| 1028 | Logging.trimPersistent(); |
| 1029 | |
Jeff Sharkey | a0c3c3b | 2019-04-15 16:25:07 -0600 | [diff] [blame] | 1030 | // Scan all volumes to resolve any staleness |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 1031 | for (String volumeName : getExternalVolumeNames()) { |
Jeff Sharkey | a0c3c3b | 2019-04-15 16:25:07 -0600 | [diff] [blame] | 1032 | // Possibly bail before digging into each volume |
| 1033 | signal.throwIfCanceled(); |
| 1034 | |
| 1035 | try { |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 1036 | MediaService.onScanVolume(getContext(), volumeName, REASON_IDLE); |
Jeff Sharkey | a0c3c3b | 2019-04-15 16:25:07 -0600 | [diff] [blame] | 1037 | } catch (IOException e) { |
| 1038 | Log.w(TAG, e); |
| 1039 | } |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 1040 | |
| 1041 | // Ensure that our thumbnails are valid |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1042 | mExternalDatabase.runWithTransaction((db) -> { |
| 1043 | ensureThumbnailsValid(volumeName, db); |
| 1044 | return null; |
| 1045 | }); |
Jeff Sharkey | a0c3c3b | 2019-04-15 16:25:07 -0600 | [diff] [blame] | 1046 | } |
| 1047 | |
Jeff Sharkey | 99a4828 | 2019-03-22 15:11:53 -0600 | [diff] [blame] | 1048 | // Delete any stale thumbnails |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1049 | final int staleThumbnails = mExternalDatabase.runWithTransaction((db) -> { |
| 1050 | return pruneThumbnails(db, signal); |
| 1051 | }); |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 1052 | Log.d(TAG, "Pruned " + staleThumbnails + " unknown thumbnails"); |
Jeff Sharkey | 99a4828 | 2019-03-22 15:11:53 -0600 | [diff] [blame] | 1053 | |
Jeff Sharkey | 7320f37 | 2018-09-12 15:14:24 -0600 | [diff] [blame] | 1054 | // Finished orphaning any content whose package no longer exists |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1055 | 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 Sharkey | bd26274 | 2019-12-17 16:40:29 -0700 | [diff] [blame] | 1062 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1063 | if (!isPackageKnown(packageName)) { |
| 1064 | unknownPackages.add(packageName); |
| 1065 | } |
Jeff Sharkey | 7320f37 | 2018-09-12 15:14:24 -0600 | [diff] [blame] | 1066 | } |
| 1067 | } |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1068 | for (String packageName : unknownPackages) { |
| 1069 | onPackageOrphaned(db, packageName); |
| 1070 | } |
| 1071 | return unknownPackages.size(); |
| 1072 | }); |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 1073 | Log.d(TAG, "Pruned " + stalePackages + " unknown packages"); |
Jeff Sharkey | 711d10f | 2019-01-04 16:09:52 -0700 | [diff] [blame] | 1074 | |
Jeff Sharkey | d4babd8 | 2019-05-14 12:37:07 -0600 | [diff] [blame] | 1075 | // Delete any expired content; we're paranoid about wildly changing |
| 1076 | // clocks, so only delete items within the last week |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 1077 | final long from = ((System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS) / 1000); |
| 1078 | final long to = (System.currentTimeMillis() / 1000); |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1079 | 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 Sharkey | 711d10f | 2019-01-04 16:09:52 -0700 | [diff] [blame] | 1089 | } |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1090 | }); |
| 1091 | Log.d(TAG, "Deleted " + expiredMedia + " expired items"); |
Jeff Sharkey | d4babd8 | 2019-05-14 12:37:07 -0600 | [diff] [blame] | 1092 | |
| 1093 | // Forget any stale volumes |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1094 | 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 Sharkey | d4babd8 | 2019-05-14 12:37:07 -0600 | [diff] [blame] | 1103 | } |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1104 | 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 Sharkey | c579312 | 2019-08-19 15:58:35 -0600 | [diff] [blame] | 1114 | |
| 1115 | synchronized (mDirectoryCache) { |
| 1116 | mDirectoryCache.clear(); |
| 1117 | } |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 1118 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1119 | final long itemCount = mExternalDatabase.runWithTransaction((db) -> { |
| 1120 | return DatabaseHelper.getItemCount(db); |
| 1121 | }); |
| 1122 | |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 1123 | final long durationMillis = (SystemClock.elapsedRealtime() - startTime); |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1124 | Metrics.logIdleMaintenance(MediaStore.VOLUME_EXTERNAL, itemCount, |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 1125 | durationMillis, staleThumbnails, expiredMedia); |
Jeff Sharkey | 7320f37 | 2018-09-12 15:14:24 -0600 | [diff] [blame] | 1126 | } |
| 1127 | |
| 1128 | public void onPackageOrphaned(String packageName) { |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1129 | mExternalDatabase.runWithTransaction((db) -> { |
| 1130 | onPackageOrphaned(db, packageName); |
| 1131 | return null; |
| 1132 | }); |
| 1133 | } |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 1134 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1135 | public void onPackageOrphaned(@NonNull SQLiteDatabase db, @NonNull String packageName) { |
Jeff Sharkey | 7320f37 | 2018-09-12 15:14:24 -0600 | [diff] [blame] | 1136 | final ContentValues values = new ContentValues(); |
| 1137 | values.putNull(FileColumns.OWNER_PACKAGE_NAME); |
| 1138 | |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 1139 | 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 Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 1143 | + packageName + " on " + db.getPath()); |
Jeff Sharkey | 7320f37 | 2018-09-12 15:14:24 -0600 | [diff] [blame] | 1144 | } |
| 1145 | } |
| 1146 | |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 1147 | public void scanDirectory(File file, int reason) { |
| 1148 | mMediaScanner.scanDirectory(file, reason); |
Jeff Sharkey | 85acbbe | 2019-10-15 17:10:30 -0600 | [diff] [blame] | 1149 | } |
| 1150 | |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 1151 | public Uri scanFile(File file, int reason) { |
| 1152 | return mMediaScanner.scanFile(file, reason); |
Jeff Sharkey | 85acbbe | 2019-10-15 17:10:30 -0600 | [diff] [blame] | 1153 | } |
| 1154 | |
Sahana Rao | 0ccfdd8 | 2020-02-17 10:34:42 +0000 | [diff] [blame] | 1155 | public Uri scanFile(File file, int reason, String ownerPackage) { |
| 1156 | return mMediaScanner.scanFile(file, reason, ownerPackage); |
| 1157 | } |
| 1158 | |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 1159 | /** |
| 1160 | * Makes MediaScanner scan the given file. |
| 1161 | * @param file path of the file to be scanned |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 1162 | * |
| 1163 | * Called from JNI in jni/MediaProviderWrapper.cpp |
| 1164 | */ |
| 1165 | @Keep |
Sahana Rao | ee32136 | 2020-03-05 19:27:37 +0000 | [diff] [blame] | 1166 | public void scanFileForFuse(String file) { |
| 1167 | scanFile(new File(file), REASON_DEMAND); |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 1168 | } |
| 1169 | |
shafik | 77ed67b | 2020-02-06 18:27:35 +0000 | [diff] [blame] | 1170 | /** |
Martijn Coenen | af2d34d | 2020-06-19 12:52:20 +0200 | [diff] [blame] | 1171 | * 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 | /** |
shafik | 77ed67b | 2020-02-06 18:27:35 +0000 | [diff] [blame] | 1188 | * 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) { |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 1192 | PermissionUtils.setOpDescription("clear app cache"); |
| 1193 | try { |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 1194 | return PermissionUtils.checkPermissionManager(context, /* pid */ -1, ai.uid, |
| 1195 | ai.packageName, /* attributionTag */ null); |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 1196 | } finally { |
| 1197 | PermissionUtils.clearOpDescription(); |
| 1198 | } |
shafik | 77ed67b | 2020-02-06 18:27:35 +0000 | [diff] [blame] | 1199 | } |
| 1200 | |
Jeff Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 1201 | @VisibleForTesting |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 1202 | 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 Sharkey | b3e6603 | 2020-05-03 11:34:41 -0600 | [diff] [blame] | 1254 | .hashString(key, StandardCharsets.UTF_8).asLong() & ~(1L << 63); |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 1255 | values.put(focusId, id); |
| 1256 | } |
| 1257 | } |
| 1258 | |
Marco Nelissen | 01e706a | 2013-09-12 15:38:42 -0700 | [diff] [blame] | 1259 | @Override |
| 1260 | public Uri canonicalize(Uri uri) { |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 1261 | final boolean allowHidden = isCallingPackageAllowedHidden(); |
| 1262 | final int match = matchUri(uri, allowHidden); |
Jeff Sharkey | 313eec8 | 2019-05-12 12:25:57 -0600 | [diff] [blame] | 1263 | |
| 1264 | // Skip when we have nothing to canonicalize |
| 1265 | if ("1".equals(uri.getQueryParameter(CANONICAL))) { |
| 1266 | return uri; |
| 1267 | } |
| 1268 | |
Jeff Sharkey | 6378ccb | 2019-03-20 13:47:36 -0600 | [diff] [blame] | 1269 | 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 Sharkey | b3e6603 | 2020-05-03 11:34:41 -0600 | [diff] [blame] | 1279 | break; |
Jeff Sharkey | 6378ccb | 2019-03-20 13:47:36 -0600 | [diff] [blame] | 1280 | } |
| 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 Sharkey | b3e6603 | 2020-05-03 11:34:41 -0600 | [diff] [blame] | 1291 | break; |
Jeff Sharkey | 4b1921d | 2018-12-11 12:24:46 -0700 | [diff] [blame] | 1292 | } |
| 1293 | } |
Jeff Sharkey | 6378ccb | 2019-03-20 13:47:36 -0600 | [diff] [blame] | 1294 | } catch (FileNotFoundException e) { |
| 1295 | Log.w(TAG, e.getMessage()); |
Mattias Nilsson | a79fcf1 | 2014-03-26 17:18:35 +0100 | [diff] [blame] | 1296 | } |
Jeff Sharkey | 4b1921d | 2018-12-11 12:24:46 -0700 | [diff] [blame] | 1297 | return null; |
Marco Nelissen | 01e706a | 2013-09-12 15:38:42 -0700 | [diff] [blame] | 1298 | } |
| 1299 | |
| 1300 | @Override |
| 1301 | public Uri uncanonicalize(Uri uri) { |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 1302 | final boolean allowHidden = isCallingPackageAllowedHidden(); |
| 1303 | final int match = matchUri(uri, allowHidden); |
| 1304 | |
Jeff Sharkey | 6378ccb | 2019-03-20 13:47:36 -0600 | [diff] [blame] | 1305 | // Skip when we have nothing to uncanonicalize |
| 1306 | if (!"1".equals(uri.getQueryParameter(CANONICAL))) { |
| 1307 | return uri; |
| 1308 | } |
Marco Nelissen | 01e706a | 2013-09-12 15:38:42 -0700 | [diff] [blame] | 1309 | |
Jeff Sharkey | 6378ccb | 2019-03-20 13:47:36 -0600 | [diff] [blame] | 1310 | // 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 Nilsson | a79fcf1 | 2014-03-26 17:18:35 +0100 | [diff] [blame] | 1324 | } |
Marco Nelissen | 01e706a | 2013-09-12 15:38:42 -0700 | [diff] [blame] | 1325 | |
Jeff Sharkey | 6378ccb | 2019-03-20 13:47:36 -0600 | [diff] [blame] | 1326 | // 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 Nilsson | a79fcf1 | 2014-03-26 17:18:35 +0100 | [diff] [blame] | 1334 | return null; |
| 1335 | } |
Jeff Sharkey | 6378ccb | 2019-03-20 13:47:36 -0600 | [diff] [blame] | 1336 | } |
| 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 Nilsson | a79fcf1 | 2014-03-26 17:18:35 +0100 | [diff] [blame] | 1356 | return null; |
| 1357 | } |
Marco Nelissen | 01e706a | 2013-09-12 15:38:42 -0700 | [diff] [blame] | 1358 | } |
Marco Nelissen | 01e706a | 2013-09-12 15:38:42 -0700 | [diff] [blame] | 1359 | } |
Jeff Sharkey | 6378ccb | 2019-03-20 13:47:36 -0600 | [diff] [blame] | 1360 | |
Marco Nelissen | 01e706a | 2013-09-12 15:38:42 -0700 | [diff] [blame] | 1361 | 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 Rao | a82bd6a | 2019-10-10 18:10:37 +0100 | [diff] [blame] | 1372 | /** |
Sahana Rao | b02e715 | 2020-06-12 17:07:31 +0100 | [diff] [blame] | 1373 | * @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 Rao | 0bbd3e1 | 2020-06-06 15:56:44 +0100 | [diff] [blame] | 1384 | + getSharedPackages(); |
Sahana Rao | b02e715 | 2020-06-12 17:07:31 +0100 | [diff] [blame] | 1385 | // 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 Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 1393 | * @return where clause to include database rows where |
| 1394 | * <ul> |
| 1395 | * <li> {@code column} is not set or |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 1396 | * <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 Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 1398 | * </ul> |
| 1399 | * The method is used to match db rows corresponding to writable pending and trashed files. |
| 1400 | */ |
| 1401 | @Nullable |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 1402 | private String getWhereClauseForMatchableVisibleFromFilePath(@NonNull Uri uri, |
| 1403 | @NonNull String column) { |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 1404 | 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 Rao | 0bbd3e1 | 2020-06-06 15:56:44 +0100 | [diff] [blame] | 1474 | + getSharedPackages(); |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 1475 | options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause)); |
| 1476 | |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 1477 | if (column.equalsIgnoreCase(MediaColumns.IS_PENDING)) { |
| 1478 | // Include all pending files from Fuse |
Sahana Rao | b02e715 | 2020-06-12 17:07:31 +0100 | [diff] [blame] | 1479 | options.add(MATCH_PENDING_FROM_FUSE); |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 1480 | } |
| 1481 | |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 1482 | 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 Rao | 8a588e7 | 2019-12-06 11:32:56 +0000 | [diff] [blame] | 1488 | * Gets list of files in {@code path} from media provider database. |
Sahana Rao | a82bd6a | 2019-10-10 18:10:37 +0100 | [diff] [blame] | 1489 | * |
Sahana Rao | 8a588e7 | 2019-12-06 11:32:56 +0000 | [diff] [blame] | 1490 | * @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 Rao | 81ceaf0 | 2019-12-23 12:37:06 +0000 | [diff] [blame] | 1495 | * 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. |
shafik | 63abf8b | 2020-03-02 15:44:37 +0000 | [diff] [blame] | 1498 | * 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 Rao | a82bd6a | 2019-10-10 18:10:37 +0100 | [diff] [blame] | 1502 | * |
| 1503 | * Called from JNI in jni/MediaProviderWrapper.cpp |
| 1504 | */ |
| 1505 | @Keep |
Sahana Rao | 8a588e7 | 2019-12-06 11:32:56 +0000 | [diff] [blame] | 1506 | public String[] getFilesInDirectoryForFuse(String path, int uid) { |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 1507 | final LocalCallingIdentity token = |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 1508 | clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 1509 | |
Sahana Rao | a82bd6a | 2019-10-10 18:10:37 +0100 | [diff] [blame] | 1510 | try { |
Martijn Coenen | 9a1f679 | 2020-03-17 07:13:47 +0100 | [diff] [blame] | 1511 | if (isPrivatePackagePathNotOwnedByCaller(path)) { |
| 1512 | return new String[] {""}; |
Sahana Rao | 81ceaf0 | 2019-12-23 12:37:06 +0000 | [diff] [blame] | 1513 | } |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 1514 | |
Ricky Wai | feb9d9b | 2020-04-06 19:14:46 +0100 | [diff] [blame] | 1515 | // 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 | |
shafik | 63abf8b | 2020-03-02 15:44:37 +0000 | [diff] [blame] | 1522 | 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 Rao | 7169344 | 2019-11-13 13:48:07 +0000 | [diff] [blame] | 1531 | // Get relative path for the contents of given directory. |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 1532 | String relativePath = extractRelativePathWithDisplayName(path); |
Sahana Rao | a82bd6a | 2019-10-10 18:10:37 +0100 | [diff] [blame] | 1533 | |
Sahana Rao | 7169344 | 2019-11-13 13:48:07 +0000 | [diff] [blame] | 1534 | if (relativePath == null) { |
Sahana Rao | 8a588e7 | 2019-12-06 11:32:56 +0000 | [diff] [blame] | 1535 | // 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 Rao | 7169344 | 2019-11-13 13:48:07 +0000 | [diff] [blame] | 1538 | return new String[] {"/"}; |
Sahana Rao | 7169344 | 2019-11-13 13:48:07 +0000 | [diff] [blame] | 1539 | } |
Sahana Rao | 8a588e7 | 2019-12-06 11:32:56 +0000 | [diff] [blame] | 1540 | |
| 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 Rao | 8a588e7 | 2019-12-06 11:32:56 +0000 | [diff] [blame] | 1544 | |
Sahana Rao | 44c1d6f | 2020-05-14 18:42:00 +0100 | [diff] [blame] | 1545 | // Only FileColumns.DATA contains actual name of the file. |
| 1546 | String[] projection = {MediaColumns.DATA}; |
| 1547 | |
Sahana Rao | 8a588e7 | 2019-12-06 11:32:56 +0000 | [diff] [blame] | 1548 | 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 Rao | 44c1d6f | 2020-05-14 18:42:00 +0100 | [diff] [blame] | 1552 | // Get database entries for files from MediaProvider database with |
| 1553 | // MediaColumns.RELATIVE_PATH as the given path. |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 1554 | try (final Cursor cursor = query(FileUtils.getContentUriForPath(path), projection, |
Sahana Rao | 8a588e7 | 2019-12-06 11:32:56 +0000 | [diff] [blame] | 1555 | queryArgs, null)) { |
| 1556 | while(cursor.moveToNext()) { |
Sahana Rao | 44c1d6f | 2020-05-14 18:42:00 +0100 | [diff] [blame] | 1557 | fileNamesList.add(extractDisplayName(cursor.getString(0))); |
Sahana Rao | 8a588e7 | 2019-12-06 11:32:56 +0000 | [diff] [blame] | 1558 | } |
| 1559 | } |
| 1560 | return fileNamesList.toArray(new String[fileNamesList.size()]); |
Sahana Rao | a82bd6a | 2019-10-10 18:10:37 +0100 | [diff] [blame] | 1561 | } finally { |
| 1562 | restoreLocalCallingIdentity(token); |
| 1563 | } |
Sahana Rao | a82bd6a | 2019-10-10 18:10:37 +0100 | [diff] [blame] | 1564 | } |
| 1565 | |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 1566 | /** |
Martijn Coenen | 070bce1 | 2020-06-08 21:18:24 +0200 | [diff] [blame] | 1567 | * Scan files during directory renames for the following reasons: |
Sahana Rao | aeded75 | 2020-04-29 17:28:15 +0100 | [diff] [blame] | 1568 | * <ul> |
Sahana Rao | 8ff51bc | 2020-05-14 23:53:50 +0100 | [diff] [blame] | 1569 | * <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 Coenen | 070bce1 | 2020-06-08 21:18:24 +0200 | [diff] [blame] | 1571 | * <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 Rao | aeded75 | 2020-04-29 17:28:15 +0100 | [diff] [blame] | 1573 | * </ul> |
| 1574 | */ |
Martijn Coenen | 070bce1 | 2020-06-08 21:18:24 +0200 | [diff] [blame] | 1575 | private void scanRenamedDirectoryForFuse(@NonNull String oldPath, @NonNull String newPath) { |
Sahana Rao | aeded75 | 2020-04-29 17:28:15 +0100 | [diff] [blame] | 1576 | final LocalCallingIdentity token = clearLocalCallingIdentity(); |
| 1577 | try { |
Martijn Coenen | 070bce1 | 2020-06-08 21:18:24 +0200 | [diff] [blame] | 1578 | scanFile(new File(oldPath), REASON_DEMAND); |
| 1579 | scanFile(new File(newPath), REASON_DEMAND); |
Sahana Rao | aeded75 | 2020-04-29 17:28:15 +0100 | [diff] [blame] | 1580 | } finally { |
| 1581 | restoreLocalCallingIdentity(token); |
| 1582 | } |
| 1583 | } |
| 1584 | |
| 1585 | /** |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1586 | * Checks if given {@code mimeType} is supported in {@code path}. |
| 1587 | */ |
| 1588 | private boolean isMimeTypeSupportedInPath(String path, String mimeType) { |
| 1589 | final String supportedPrimaryMimeType; |
Jeff Sharkey | c4a5f81 | 2020-05-03 21:07:14 -0600 | [diff] [blame] | 1590 | final int match = matchUri(getContentUriForFile(path, mimeType), true); |
| 1591 | switch (match) { |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1592 | 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 Sharkey | c4a5f81 | 2020-05-03 21:07:14 -0600 | [diff] [blame] | 1604 | return (supportedPrimaryMimeType.equalsIgnoreCase(ClipDescription.MIMETYPE_UNKNOWN) || |
| 1605 | MimeUtils.startsWithIgnoreCase(mimeType, supportedPrimaryMimeType)); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1606 | } |
| 1607 | |
Sahana Rao | 1e8271b | 2020-04-03 14:01:08 +0100 | [diff] [blame] | 1608 | /** |
| 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 | |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 1626 | final Uri uri = FileUtils.getContentUriForPath(path); |
Sahana Rao | 1e8271b | 2020-04-03 14:01:08 +0100 | [diff] [blame] | 1627 | 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 | |
shafik | ac34fe9 | 2020-02-25 15:28:55 +0000 | [diff] [blame] | 1657 | 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 Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1662 | /** |
| 1663 | * Updates database entry for given {@code path} with {@code values} |
| 1664 | */ |
Jeff Sharkey | 021e68f | 2020-01-14 18:21:50 -0700 | [diff] [blame] | 1665 | private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper, |
shafik | ac34fe9 | 2020-02-25 15:28:55 +0000 | [diff] [blame] | 1666 | @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values, |
| 1667 | @NonNull Bundle qbExtras) { |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 1668 | final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1669 | boolean allowHidden = isCallingPackageAllowedHidden(); |
| 1670 | final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE, |
shafik | ac34fe9 | 2020-02-25 15:28:55 +0000 | [diff] [blame] | 1671 | matchUri(uriOldPath, allowHidden), uriOldPath, qbExtras, null); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1672 | final String selection = MediaColumns.DATA + " =? "; |
| 1673 | int count = 0; |
| 1674 | boolean retryUpdateWithReplace = false; |
| 1675 | |
| 1676 | try { |
Sahana Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 1677 | // 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 Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1680 | count = qbForUpdate.update(helper, values, selection, new String[]{oldPath}); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1681 | } 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. |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 1689 | final Uri uriNewPath = FileUtils.getContentUriForPath(oldPath); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1690 | final SQLiteQueryBuilder qbForDelete = getQueryBuilder(TYPE_DELETE, |
shafik | ac34fe9 | 2020-02-25 15:28:55 +0000 | [diff] [blame] | 1691 | matchUri(uriNewPath, allowHidden), uriNewPath, qbExtras, null); |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1692 | if (qbForDelete.delete(helper, selection, new String[] {newPath}) == 1) { |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1693 | Log.i(TAG, "Retrying database update after deleting conflicting entry"); |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1694 | count = qbForUpdate.update(helper, values, selection, new String[]{oldPath}); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1695 | } 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 Coenen | 070bce1 | 2020-06-08 21:18:24 +0200 | [diff] [blame] | 1705 | private ContentValues getContentValuesForFuseRename(String path, String newMimeType, |
| 1706 | boolean checkHidden) { |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1707 | ContentValues values = new ContentValues(); |
| 1708 | values.put(MediaColumns.MIME_TYPE, newMimeType); |
| 1709 | values.put(MediaColumns.DATA, path); |
| 1710 | |
Martijn Coenen | 070bce1 | 2020-06-08 21:18:24 +0200 | [diff] [blame] | 1711 | if (checkHidden && shouldFileBeHidden(new File(path))) { |
| 1712 | values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE); |
| 1713 | } else { |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1714 | int mediaType = MimeUtils.resolveMediaType(newMimeType); |
| 1715 | values.put(FileColumns.MEDIA_TYPE, mediaType); |
| 1716 | } |
| 1717 | final boolean allowHidden = isCallingPackageAllowedHidden(); |
Jeff Sharkey | c4a5f81 | 2020-05-03 21:07:14 -0600 | [diff] [blame] | 1718 | if (!newMimeType.equalsIgnoreCase("null") && |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1719 | matchUri(getContentUriForFile(path, newMimeType), allowHidden) == AUDIO_MEDIA) { |
| 1720 | computeAudioLocalizedValues(values); |
| 1721 | computeAudioKeyValues(values); |
| 1722 | } |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 1723 | FileUtils.computeValuesFromData(values, isFuseThread()); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1724 | return values; |
| 1725 | } |
| 1726 | |
shafik | ac34fe9 | 2020-02-25 15:28:55 +0000 | [diff] [blame] | 1727 | 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 Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1740 | /** |
Sahana Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 1741 | * 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 Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 1745 | extractRelativePathWithDisplayName(oldPath) + "/?.*' and mime_type not like 'null'"; |
Sahana Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 1746 | ArrayList<String> fileList = new ArrayList<>(); |
| 1747 | |
| 1748 | final LocalCallingIdentity token = clearLocalCallingIdentity(); |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 1749 | try (final Cursor c = query(FileUtils.getContentUriForPath(oldPath), |
Sahana Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 1750 | 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 Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1762 | * 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 Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 1769 | private ArrayList<String> getWritableFilesForRenameDirectory(String oldPath, String newPath) |
| 1770 | throws IllegalArgumentException { |
shafik | ac34fe9 | 2020-02-25 15:28:55 +0000 | [diff] [blame] | 1771 | // 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 Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 1773 | final String oldRelativePath = extractRelativePathWithDisplayName(oldPath); |
| 1774 | final String newRelativePath = extractRelativePathWithDisplayName(newPath); |
shafik | ac34fe9 | 2020-02-25 15:28:55 +0000 | [diff] [blame] | 1775 | 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 Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1785 | final int countAllFilesInDirectory; |
| 1786 | final String selection = MediaColumns.RELATIVE_PATH + " REGEXP '^" + |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 1787 | extractRelativePathWithDisplayName(oldPath) + "/?.*' and mime_type not like 'null'"; |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 1788 | final Uri uriOldPath = FileUtils.getContentUriForPath(oldPath); |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1789 | |
| 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 Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1812 | try (Cursor c = qb.query(helper, projection, selection, null, |
| 1813 | null, null, null, null, null)) { |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1814 | // 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 Rao | ddc8582 | 2020-01-06 20:22:46 +0000 | [diff] [blame] | 1834 | 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); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 1841 | return e.errno; |
Sahana Rao | ddc8582 | 2020-01-06 20:22:46 +0000 | [diff] [blame] | 1842 | } |
| 1843 | } |
| 1844 | |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1845 | /** |
| 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 Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 1866 | private int renameDirectoryCheckedForFuse(String oldPath, String newPath) { |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1867 | final ArrayList<String> fileList; |
| 1868 | try { |
| 1869 | fileList = getWritableFilesForRenameDirectory(oldPath, newPath); |
Sahana Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 1870 | } catch (IllegalArgumentException e) { |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1871 | final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. "; |
| 1872 | Log.e(TAG, errorMessage, e); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 1873 | return OsConstants.EPERM; |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1874 | } |
| 1875 | |
Sahana Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 1876 | return renameDirectoryUncheckedForFuse(oldPath, newPath, fileList); |
| 1877 | } |
| 1878 | |
| 1879 | private int renameDirectoryUncheckedForFuse(String oldPath, String newPath, |
| 1880 | ArrayList<String> fileList) { |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1881 | final DatabaseHelper helper; |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1882 | try { |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 1883 | helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath)); |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1884 | } catch (VolumeNotFoundException e) { |
| 1885 | throw new IllegalStateException("Volume not found while trying to update database for " |
| 1886 | + oldPath, e); |
| 1887 | } |
| 1888 | |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1889 | helper.beginTransaction(); |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1890 | try { |
shafik | ac34fe9 | 2020-02-25 15:28:55 +0000 | [diff] [blame] | 1891 | final Bundle qbExtras = new Bundle(); |
| 1892 | qbExtras.putStringArrayList(INCLUDED_DEFAULT_DIRECTORIES, |
| 1893 | getIncludedDefaultDirectories()); |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1894 | for (String filePath : fileList) { |
| 1895 | final String newFilePath = newPath + "/" + filePath; |
| 1896 | final String mimeType = MimeUtils.resolveMimeType(new File(newFilePath)); |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1897 | if(!updateDatabaseForFuseRename(helper, oldPath + "/" + filePath, newFilePath, |
Martijn Coenen | 070bce1 | 2020-06-08 21:18:24 +0200 | [diff] [blame] | 1898 | getContentValuesForFuseRename(newFilePath, mimeType, |
| 1899 | false /* checkHidden - will be fixed up below */), qbExtras)) { |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1900 | Log.e(TAG, "Calling package doesn't have write permission to rename file."); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 1901 | return OsConstants.EPERM; |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1902 | } |
| 1903 | } |
| 1904 | |
| 1905 | // Rename the directory in lower file system. |
Sahana Rao | ddc8582 | 2020-01-06 20:22:46 +0000 | [diff] [blame] | 1906 | int errno = renameInLowerFs(oldPath, newPath); |
| 1907 | if (errno == 0) { |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1908 | helper.setTransactionSuccessful(); |
Sahana Rao | ddc8582 | 2020-01-06 20:22:46 +0000 | [diff] [blame] | 1909 | } else { |
| 1910 | return errno; |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1911 | } |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1912 | } finally { |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1913 | helper.endTransaction(); |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1914 | } |
Martijn Coenen | 070bce1 | 2020-06-08 21:18:24 +0200 | [diff] [blame] | 1915 | // Directory movement might have made new/old path hidden. |
| 1916 | scanRenamedDirectoryForFuse(oldPath, newPath); |
Sahana Rao | 182ec6b | 2020-01-03 15:00:46 +0000 | [diff] [blame] | 1917 | return 0; |
| 1918 | } |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1919 | |
| 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 Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 1937 | private int renameFileCheckedForFuse(String oldPath, String newPath) { |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1938 | // Check if new mime type is supported in new path. |
| 1939 | final String newMimeType = MimeUtils.resolveMimeType(new File(newPath)); |
| 1940 | if (!isMimeTypeSupportedInPath(newPath, newMimeType)) { |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 1941 | return OsConstants.EPERM; |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1942 | } |
Sahana Rao | 1e8271b | 2020-04-03 14:01:08 +0100 | [diff] [blame] | 1943 | return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ false) ; |
Sahana Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 1944 | } |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1945 | |
Sahana Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 1946 | private int renameFileUncheckedForFuse(String oldPath, String newPath) { |
Sahana Rao | 1e8271b | 2020-04-03 14:01:08 +0100 | [diff] [blame] | 1947 | return renameFileForFuse(oldPath, newPath, /* bypassRestrictions */ true) ; |
| 1948 | } |
| 1949 | |
Martijn Coenen | 070bce1 | 2020-06-08 21:18:24 +0200 | [diff] [blame] | 1950 | 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 Rao | 1e8271b | 2020-04-03 14:01:08 +0100 | [diff] [blame] | 1965 | private int renameFileForFuse(String oldPath, String newPath, boolean bypassRestrictions) { |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1966 | final DatabaseHelper helper; |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1967 | try { |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 1968 | helper = getDatabaseForUri(FileUtils.getContentUriForPath(oldPath)); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1969 | } catch (VolumeNotFoundException e) { |
Sahana Rao | 1e8271b | 2020-04-03 14:01:08 +0100 | [diff] [blame] | 1970 | throw new IllegalStateException("Failed to update database row with " + oldPath, e); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1971 | } |
| 1972 | |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1973 | helper.beginTransaction(); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1974 | try { |
Sahana Rao | 1e8271b | 2020-04-03 14:01:08 +0100 | [diff] [blame] | 1975 | final String newMimeType = MimeUtils.resolveMimeType(new File(newPath)); |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1976 | if (!updateDatabaseForFuseRename(helper, oldPath, newPath, |
Martijn Coenen | 070bce1 | 2020-06-08 21:18:24 +0200 | [diff] [blame] | 1977 | getContentValuesForFuseRename(newPath, newMimeType, true /* checkHidden */))) { |
Sahana Rao | 1e8271b | 2020-04-03 14:01:08 +0100 | [diff] [blame] | 1978 | 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 Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1985 | } |
| 1986 | |
| 1987 | // Try renaming oldPath to newPath in lower file system. |
Sahana Rao | ddc8582 | 2020-01-06 20:22:46 +0000 | [diff] [blame] | 1988 | int errno = renameInLowerFs(oldPath, newPath); |
| 1989 | if (errno == 0) { |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1990 | helper.setTransactionSuccessful(); |
Sahana Rao | ddc8582 | 2020-01-06 20:22:46 +0000 | [diff] [blame] | 1991 | } else { |
| 1992 | return errno; |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1993 | } |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1994 | } finally { |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 1995 | helper.endTransaction(); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 1996 | } |
Martijn Coenen | 070bce1 | 2020-06-08 21:18:24 +0200 | [diff] [blame] | 1997 | // 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 Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 2013 | return 0; |
| 2014 | } |
| 2015 | |
| 2016 | /** |
Sahana Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 2017 | * 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 Rao | 92e2726 | 2020-02-19 14:11:56 +0000 | [diff] [blame] | 2021 | */ |
| 2022 | private int renameUncheckedForFuse(String oldPath, String newPath) { |
Sahana Rao | 7448453 | 2020-04-07 14:58:29 +0100 | [diff] [blame] | 2023 | if (new File(oldPath).isFile()) { |
| 2024 | return renameFileUncheckedForFuse(oldPath, newPath); |
| 2025 | } else { |
| 2026 | return renameDirectoryUncheckedForFuse(oldPath, newPath, |
| 2027 | getAllFilesForRenameDirectory(oldPath)); |
| 2028 | } |
Sahana Rao | 92e2726 | 2020-02-19 14:11:56 +0000 | [diff] [blame] | 2029 | } |
| 2030 | |
| 2031 | /** |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2032 | * 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. |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 2037 | * @return 0 on successful rename, appropriate errno value if the rename is not allowed. |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2038 | * <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 Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 2041 | * <li>{@link OsConstants#EPERM} Renaming a default directory or renaming a file to a file type |
| 2042 | * not supported by new path. |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2043 | * This method can also return errno returned from {@code Os.rename} function. |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2044 | * |
| 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. "; |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 2050 | final LocalCallingIdentity token = |
| 2051 | clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 2052 | |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2053 | try { |
Martijn Coenen | 9a1f679 | 2020-03-17 07:13:47 +0100 | [diff] [blame] | 2054 | if (isPrivatePackagePathNotOwnedByCaller(oldPath) |
| 2055 | || isPrivatePackagePathNotOwnedByCaller(newPath)) { |
| 2056 | return OsConstants.EACCES; |
Sahana Rao | ddc8582 | 2020-01-06 20:22:46 +0000 | [diff] [blame] | 2057 | } |
| 2058 | |
Sahana Rao | 6b7baf4 | 2020-04-17 20:42:23 +0100 | [diff] [blame] | 2059 | if (!newPath.equals(getAbsoluteSanitizedPath(newPath))) { |
| 2060 | Log.e(TAG, "New path name contains invalid characters."); |
| 2061 | return OsConstants.EPERM; |
| 2062 | } |
| 2063 | |
shafik | 63abf8b | 2020-03-02 15:44:37 +0000 | [diff] [blame] | 2064 | 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 Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2074 | 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."); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 2079 | return OsConstants.EPERM; |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2080 | } 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."); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 2087 | return OsConstants.EPERM; |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2088 | } |
| 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"); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 2093 | return OsConstants.EPERM; |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2094 | } |
| 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."); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 2104 | return OsConstants.EPERM; |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2105 | } 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."); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 2112 | return OsConstants.EPERM; |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2113 | } 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"); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 2120 | return OsConstants.EPERM; |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2121 | } |
| 2122 | } |
| 2123 | |
| 2124 | // Continue renaming files/directories if rename of oldPath to newPath is allowed. |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 2125 | if (new File(oldPath).isFile()) { |
Sahana Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 2126 | return renameFileCheckedForFuse(oldPath, newPath); |
Sahana Rao | 5b0b965 | 2019-12-31 17:49:25 +0000 | [diff] [blame] | 2127 | } else { |
Sahana Rao | 5032657 | 2020-02-25 13:07:38 +0000 | [diff] [blame] | 2128 | return renameDirectoryCheckedForFuse(oldPath, newPath); |
Sahana Rao | 2c41603 | 2019-12-31 13:41:00 +0000 | [diff] [blame] | 2129 | } |
| 2130 | } finally { |
| 2131 | restoreLocalCallingIdentity(token); |
| 2132 | } |
| 2133 | } |
| 2134 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2135 | @Override |
Jeff Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 2136 | public int checkUriPermission(@NonNull Uri uri, int uid, |
| 2137 | /* @Intent.AccessUriMode */ int modeFlags) { |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 2138 | final LocalCallingIdentity token = clearLocalCallingIdentity( |
| 2139 | LocalCallingIdentity.fromExternal(getContext(), uid)); |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 2140 | |
Jeff Sharkey | 0ee9741 | 2019-05-20 14:00:12 -0600 | [diff] [blame] | 2141 | try { |
| 2142 | final boolean allowHidden = isCallingPackageAllowedHidden(); |
| 2143 | final int table = matchUri(uri, allowHidden); |
| 2144 | |
| 2145 | final DatabaseHelper helper; |
Jeff Sharkey | 0ee9741 | 2019-05-20 14:00:12 -0600 | [diff] [blame] | 2146 | try { |
| 2147 | helper = getDatabaseForUri(uri); |
Jeff Sharkey | 0ee9741 | 2019-05-20 14:00:12 -0600 | [diff] [blame] | 2148 | } 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 Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2159 | final SQLiteQueryBuilder qb = getQueryBuilder(type, table, uri, Bundle.EMPTY, null); |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 2160 | try (Cursor c = qb.query(helper, |
| 2161 | new String[] { BaseColumns._ID }, null, null, null, null, null, null, null)) { |
Jeff Sharkey | 0ee9741 | 2019-05-20 14:00:12 -0600 | [diff] [blame] | 2162 | 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 Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2173 | public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, |
| 2174 | String sortOrder) { |
| 2175 | return query(uri, projection, |
Jeff Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 2176 | DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, sortOrder), null); |
Jeff Sharkey | ca52427 | 2018-07-30 13:33:48 -0600 | [diff] [blame] | 2177 | } |
| 2178 | |
| 2179 | @Override |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2180 | public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal) { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 2181 | Trace.beginSection("query"); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 2182 | try { |
| 2183 | return queryInternal(uri, projection, queryArgs, signal); |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 2184 | } catch (FallbackException e) { |
| 2185 | return e.translateForQuery(getCallingPackageTargetSdkVersion()); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 2186 | } finally { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 2187 | Trace.endSection(); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 2188 | } |
| 2189 | } |
| 2190 | |
| 2191 | private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs, |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 2192 | CancellationSignal signal) throws FallbackException { |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 2193 | queryArgs = (queryArgs != null) ? queryArgs : new Bundle(); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2194 | |
Nikita Ioffe | 710787b | 2020-06-11 14:35:14 +0100 | [diff] [blame] | 2195 | // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. |
| 2196 | queryArgs.remove(INCLUDED_DEFAULT_DIRECTORIES); |
| 2197 | |
Jeff Sharkey | 9c1bb3d | 2019-11-14 13:59:41 -0700 | [diff] [blame] | 2198 | final ArraySet<String> honoredArgs = new ArraySet<>(); |
| 2199 | DatabaseUtils.resolveQueryArgs(queryArgs, honoredArgs::add, this::ensureCustomCollator); |
| 2200 | |
Marco Nelissen | 01e706a | 2013-09-12 15:38:42 -0700 | [diff] [blame] | 2201 | uri = safeUncanonicalize(uri); |
| 2202 | |
Jeff Sharkey | 27bf696 | 2018-10-20 15:12:28 -0600 | [diff] [blame] | 2203 | final String volumeName = getVolumeName(uri); |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 2204 | final int targetSdkVersion = getCallingPackageTargetSdkVersion(); |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 2205 | final boolean allowHidden = isCallingPackageAllowedHidden(); |
| 2206 | final int table = matchUri(uri, allowHidden); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2207 | |
Marco Nelissen | 8a1db2e | 2017-11-30 12:46:54 -0800 | [diff] [blame] | 2208 | //Log.v(TAG, "query: uri="+uri+", selection="+selection); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2209 | // handle MEDIA_SCANNER before calling getDatabaseForUri() |
| 2210 | if (table == MEDIA_SCANNER) { |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 2211 | // 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2215 | } |
| 2216 | |
Marco Nelissen | 0027019 | 2010-01-08 08:35:20 -0800 | [diff] [blame] | 2217 | // 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 Nelissen | 704a8b5 | 2011-02-03 10:43:06 -0800 | [diff] [blame] | 2226 | if (table == VERSION) { |
| 2227 | MatrixCursor c = new MatrixCursor(new String[] {"version"}); |
Jeff Sharkey | c9ae859 | 2019-10-07 11:41:04 -0600 | [diff] [blame] | 2228 | c.addRow(new Integer[] {DatabaseHelper.getDatabaseVersion(getContext())}); |
Marco Nelissen | 704a8b5 | 2011-02-03 10:43:06 -0800 | [diff] [blame] | 2229 | return c; |
| 2230 | } |
| 2231 | |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 2232 | final DatabaseHelper helper = getDatabaseForUri(uri); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2233 | final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, queryArgs, |
| 2234 | honoredArgs::add); |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 2235 | |
Jeff Sharkey | 9c1bb3d | 2019-11-14 13:59:41 -0700 | [diff] [blame] | 2236 | if (targetSdkVersion < Build.VERSION_CODES.R) { |
Jeff Sharkey | b855772 | 2019-12-18 17:57:50 -0700 | [diff] [blame] | 2237 | // Some apps are abusing "ORDER BY" clauses to inject "LIMIT" |
| 2238 | // clauses; gracefully lift them out. |
| 2239 | DatabaseUtils.recoverAbusiveSortOrder(queryArgs); |
| 2240 | |
Jeff Sharkey | 9c1bb3d | 2019-11-14 13:59:41 -0700 | [diff] [blame] | 2241 | // 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 Sharkey | 1ed7727 | 2018-10-03 13:47:33 -0600 | [diff] [blame] | 2247 | // Some apps are abusing the "WHERE" clause by injecting "GROUP BY" |
| 2248 | // clauses; gracefully lift them out. |
Jeff Sharkey | 9c1bb3d | 2019-11-14 13:59:41 -0700 | [diff] [blame] | 2249 | DatabaseUtils.recoverAbusiveSelection(queryArgs); |
Jeff Sharkey | 1ed7727 | 2018-10-03 13:47:33 -0600 | [diff] [blame] | 2250 | |
| 2251 | // Some apps are abusing the first column to inject "DISTINCT"; |
| 2252 | // gracefully lift them out. |
Jeff Sharkey | f05c4e7 | 2019-08-20 10:35:50 -0600 | [diff] [blame] | 2253 | if ((projection != null) && (projection.length > 0) |
| 2254 | && projection[0].startsWith("DISTINCT ")) { |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2255 | projection[0] = projection[0].substring("DISTINCT ".length()); |
Jeff Sharkey | 1ed7727 | 2018-10-03 13:47:33 -0600 | [diff] [blame] | 2256 | qb.setDistinct(true); |
| 2257 | } |
Jeff Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 2258 | |
| 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 Sharkey | 9c1bb3d | 2019-11-14 13:59:41 -0700 | [diff] [blame] | 2262 | final String selection = queryArgs.getString(QUERY_ARG_SQL_SELECTION); |
Jeff Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 2263 | 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 Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 2281 | final File file = ContentResolver.encodeToFile( |
Jeff Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 2282 | fullUri.buildUpon().appendPath("thumbnail").build()); |
Jeff Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 2283 | final String data = file.getAbsolutePath(); |
Jeff Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 2284 | 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 Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 2288 | return cursor; |
| 2289 | } |
| 2290 | } |
Jeff Sharkey | 3a1265b | 2018-08-06 11:36:08 -0600 | [diff] [blame] | 2291 | } |
| 2292 | |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 2293 | final Cursor c = qb.query(helper, projection, queryArgs, signal); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2294 | if (c != null) { |
Jeff Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 2295 | // As a performance optimization, only configure notifications when |
| 2296 | // resulting cursor will leave our process |
Jeff Sharkey | 75b8bd4 | 2020-06-19 08:51:14 -0600 | [diff] [blame] | 2297 | final boolean callerIsRemote = mCallingIdentity.get().pid != android.os.Process.myPid(); |
| 2298 | if (callerIsRemote && !isFuseThread()) { |
Jeff Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 2299 | c.setNotificationUri(getContext().getContentResolver(), uri); |
| 2300 | } |
Jeff Sharkey | 9c1bb3d | 2019-11-14 13:59:41 -0700 | [diff] [blame] | 2301 | |
| 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2306 | } |
| 2307 | return c; |
| 2308 | } |
| 2309 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2310 | @Override |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 2311 | public String getType(Uri url) { |
Jeff Sharkey | 7285f26 | 2019-05-21 08:36:27 -0600 | [diff] [blame] | 2312 | final int match = matchUri(url, true); |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 2313 | switch (match) { |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2314 | case IMAGES_MEDIA_ID: |
| 2315 | case AUDIO_MEDIA_ID: |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 2316 | case AUDIO_PLAYLISTS_ID: |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2317 | case AUDIO_PLAYLISTS_ID_MEMBERS_ID: |
| 2318 | case VIDEO_MEDIA_ID: |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 2319 | case DOWNLOADS_ID: |
Mike Lockwood | c198bd9 | 2010-09-10 14:55:20 -0400 | [diff] [blame] | 2320 | case FILES_ID: |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 2321 | final LocalCallingIdentity token = clearLocalCallingIdentity(); |
Jeff Sharkey | b7d359b | 2019-03-18 20:07:12 -0600 | [diff] [blame] | 2322 | 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 Chen | 26f297a | 2010-04-06 14:52:01 -0700 | [diff] [blame] | 2327 | } finally { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 2328 | restoreLocalCallingIdentity(token); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2329 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2330 | |
| 2331 | case IMAGES_MEDIA: |
| 2332 | case IMAGES_THUMBNAILS: |
| 2333 | return Images.Media.CONTENT_TYPE; |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 2334 | |
Marco Nelissen | 804f5fe | 2010-08-13 16:15:54 -0700 | [diff] [blame] | 2335 | case AUDIO_ALBUMART_ID: |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 2336 | case AUDIO_ALBUMART_FILE_ID: |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2337 | case IMAGES_THUMBNAILS_ID: |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 2338 | case VIDEO_THUMBNAILS_ID: |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2339 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2353 | return Audio.Playlists.CONTENT_TYPE; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2354 | |
| 2355 | case VIDEO_MEDIA: |
| 2356 | return Video.Media.CONTENT_TYPE; |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 2357 | case DOWNLOADS: |
| 2358 | return Downloads.CONTENT_TYPE; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2359 | } |
Marco Nelissen | 804f5fe | 2010-08-13 16:15:54 -0700 | [diff] [blame] | 2360 | throw new IllegalStateException("Unknown URL : " + url); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2361 | } |
| 2362 | |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2363 | @VisibleForTesting |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2364 | void ensureFileColumns(@NonNull Uri uri, @NonNull ContentValues values) |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 2365 | throws VolumeArgumentException, VolumeNotFoundException { |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 2366 | final LocalUriMatcher matcher = new LocalUriMatcher(MediaStore.AUTHORITY); |
| 2367 | final int match = matcher.matchUri(uri, true); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2368 | ensureNonUniqueFileColumns(match, uri, Bundle.EMPTY, values, null /* currentPath */); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2369 | } |
| 2370 | |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2371 | private void ensureUniqueFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, |
Jeff Sharkey | ab27f02 | 2020-04-29 20:58:55 -0600 | [diff] [blame] | 2372 | @NonNull ContentValues values, @Nullable String currentPath) |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 2373 | throws VolumeArgumentException, VolumeNotFoundException { |
Jeff Sharkey | ab27f02 | 2020-04-29 20:58:55 -0600 | [diff] [blame] | 2374 | ensureFileColumns(match, uri, extras, values, true, currentPath); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2375 | } |
| 2376 | |
| 2377 | private void ensureNonUniqueFileColumns(int match, @NonNull Uri uri, |
| 2378 | @NonNull Bundle extras, @NonNull ContentValues values, @Nullable String currentPath) |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 2379 | throws VolumeArgumentException, VolumeNotFoundException { |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2380 | ensureFileColumns(match, uri, extras, values, false, currentPath); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2381 | } |
| 2382 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2383 | /** |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2384 | * 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2389 | */ |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2390 | private void ensureFileColumns(int match, @NonNull Uri uri, @NonNull Bundle extras, |
| 2391 | @NonNull ContentValues values, boolean makeUnique, @Nullable String currentPath) |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 2392 | throws VolumeArgumentException, VolumeNotFoundException { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 2393 | Trace.beginSection("ensureFileColumns"); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 2394 | |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2395 | Objects.requireNonNull(uri); |
| 2396 | Objects.requireNonNull(extras); |
| 2397 | Objects.requireNonNull(values); |
| 2398 | |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2399 | // Figure out defaults based on Uri being modified |
Jeff Sharkey | 7ea24f2 | 2019-08-22 10:14:18 -0600 | [diff] [blame] | 2400 | String defaultMimeType = ClipDescription.MIMETYPE_UNKNOWN; |
Jeff Sharkey | ca4a71e | 2020-01-10 17:18:37 -0700 | [diff] [blame] | 2401 | int defaultMediaType = FileColumns.MEDIA_TYPE_NONE; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2402 | 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 Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2409 | case AUDIO_MEDIA_ID: |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2410 | defaultMimeType = "audio/mpeg"; |
Jeff Sharkey | ca4a71e | 2020-01-10 17:18:37 -0700 | [diff] [blame] | 2411 | defaultMediaType = FileColumns.MEDIA_TYPE_AUDIO; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2412 | defaultPrimary = Environment.DIRECTORY_MUSIC; |
| 2413 | allowedPrimary = Arrays.asList( |
| 2414 | Environment.DIRECTORY_ALARMS, |
shafik | 5870b20 | 2020-03-03 20:21:16 +0000 | [diff] [blame] | 2415 | Environment.DIRECTORY_AUDIOBOOKS, |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2416 | Environment.DIRECTORY_MUSIC, |
| 2417 | Environment.DIRECTORY_NOTIFICATIONS, |
| 2418 | Environment.DIRECTORY_PODCASTS, |
| 2419 | Environment.DIRECTORY_RINGTONES); |
| 2420 | break; |
| 2421 | case VIDEO_MEDIA: |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2422 | case VIDEO_MEDIA_ID: |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2423 | defaultMimeType = "video/mp4"; |
Jeff Sharkey | ca4a71e | 2020-01-10 17:18:37 -0700 | [diff] [blame] | 2424 | defaultMediaType = FileColumns.MEDIA_TYPE_VIDEO; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2425 | defaultPrimary = Environment.DIRECTORY_MOVIES; |
| 2426 | allowedPrimary = Arrays.asList( |
| 2427 | Environment.DIRECTORY_DCIM, |
shafik | b95e024 | 2020-02-12 17:41:32 +0000 | [diff] [blame] | 2428 | Environment.DIRECTORY_MOVIES, |
| 2429 | Environment.DIRECTORY_PICTURES); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2430 | break; |
| 2431 | case IMAGES_MEDIA: |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2432 | case IMAGES_MEDIA_ID: |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2433 | defaultMimeType = "image/jpeg"; |
Jeff Sharkey | ca4a71e | 2020-01-10 17:18:37 -0700 | [diff] [blame] | 2434 | defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2435 | defaultPrimary = Environment.DIRECTORY_PICTURES; |
| 2436 | allowedPrimary = Arrays.asList( |
| 2437 | Environment.DIRECTORY_DCIM, |
| 2438 | Environment.DIRECTORY_PICTURES); |
| 2439 | break; |
| 2440 | case AUDIO_ALBUMART: |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2441 | case AUDIO_ALBUMART_ID: |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2442 | defaultMimeType = "image/jpeg"; |
Jeff Sharkey | ca4a71e | 2020-01-10 17:18:37 -0700 | [diff] [blame] | 2443 | defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2444 | defaultPrimary = Environment.DIRECTORY_MUSIC; |
| 2445 | allowedPrimary = Arrays.asList(defaultPrimary); |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 2446 | defaultSecondary = DIRECTORY_THUMBNAILS; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2447 | break; |
| 2448 | case VIDEO_THUMBNAILS: |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2449 | case VIDEO_THUMBNAILS_ID: |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2450 | defaultMimeType = "image/jpeg"; |
Jeff Sharkey | ca4a71e | 2020-01-10 17:18:37 -0700 | [diff] [blame] | 2451 | defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2452 | defaultPrimary = Environment.DIRECTORY_MOVIES; |
| 2453 | allowedPrimary = Arrays.asList(defaultPrimary); |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 2454 | defaultSecondary = DIRECTORY_THUMBNAILS; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2455 | break; |
| 2456 | case IMAGES_THUMBNAILS: |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2457 | case IMAGES_THUMBNAILS_ID: |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2458 | defaultMimeType = "image/jpeg"; |
Jeff Sharkey | ca4a71e | 2020-01-10 17:18:37 -0700 | [diff] [blame] | 2459 | defaultMediaType = FileColumns.MEDIA_TYPE_IMAGE; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2460 | defaultPrimary = Environment.DIRECTORY_PICTURES; |
| 2461 | allowedPrimary = Arrays.asList(defaultPrimary); |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 2462 | defaultSecondary = DIRECTORY_THUMBNAILS; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2463 | break; |
| 2464 | case AUDIO_PLAYLISTS: |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2465 | case AUDIO_PLAYLISTS_ID: |
Jeff Sharkey | 21e297e | 2019-12-06 18:14:32 -0700 | [diff] [blame] | 2466 | defaultMimeType = "audio/mpegurl"; |
Jeff Sharkey | ca4a71e | 2020-01-10 17:18:37 -0700 | [diff] [blame] | 2467 | defaultMediaType = FileColumns.MEDIA_TYPE_PLAYLIST; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2468 | defaultPrimary = Environment.DIRECTORY_MUSIC; |
Jeff Sharkey | ca4a71e | 2020-01-10 17:18:37 -0700 | [diff] [blame] | 2469 | allowedPrimary = Arrays.asList( |
| 2470 | Environment.DIRECTORY_MUSIC, |
| 2471 | Environment.DIRECTORY_MOVIES); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2472 | break; |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 2473 | case DOWNLOADS: |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2474 | case DOWNLOADS_ID: |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 2475 | defaultPrimary = Environment.DIRECTORY_DOWNLOADS; |
| 2476 | allowedPrimary = Arrays.asList(defaultPrimary); |
| 2477 | break; |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2478 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2485 | } |
| 2486 | |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 2487 | final String resolvedVolumeName = resolveVolumeName(uri); |
| 2488 | |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 2489 | if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA)) |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 2490 | && MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)) { |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2491 | // 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 Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 2497 | if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 2498 | FileUtils.computeValuesFromData(values, isFuseThread()); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2499 | } |
Bram Bonné | 2c46f52 | 2020-02-27 13:35:06 +0100 | [diff] [blame] | 2500 | |
Sahana Rao | b105c22 | 2020-06-17 20:18:48 +0100 | [diff] [blame] | 2501 | 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é | 2c46f52 | 2020-02-27 13:35:06 +0100 | [diff] [blame] | 2549 | } |
| 2550 | } |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2551 | |
| 2552 | // Give ourselves sane defaults when missing |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 2553 | if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) { |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2554 | values.put(MediaColumns.DISPLAY_NAME, |
| 2555 | String.valueOf(System.currentTimeMillis())); |
| 2556 | } |
Sudheer Shanka | 2b79a5c | 2019-04-12 10:04:20 -0700 | [diff] [blame] | 2557 | 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 Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2561 | } |
| 2562 | |
Sahana Rao | b105c22 | 2020-06-17 20:18:48 +0100 | [diff] [blame] | 2563 | mimeType = values.getAsString(MediaColumns.MIME_TYPE); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2564 | // Sanity check MIME type against table |
Jeff Sharkey | ca4a71e | 2020-01-10 17:18:37 -0700 | [diff] [blame] | 2565 | 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 Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2590 | throw new IllegalArgumentException( |
| 2591 | "MIME type " + mimeType + " cannot be inserted into " + uri |
| 2592 | + "; expected MIME type under " + split[0] + "/*"); |
| 2593 | } |
| 2594 | } |
| 2595 | |
Jeff Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 2596 | // 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 Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2607 | // Generate path when undefined |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 2608 | if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { |
Jeff Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 2609 | File volumePath; |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2610 | try { |
Jeff Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 2611 | volumePath = getVolumePath(resolvedVolumeName); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2612 | } catch (FileNotFoundException e) { |
| 2613 | throw new IllegalArgumentException(e); |
| 2614 | } |
Jeff Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 2615 | |
Sahana Rao | 76b8b5b | 2020-04-17 20:21:59 +0100 | [diff] [blame] | 2616 | FileUtils.sanitizeValues(values, /*rewriteHiddenFileName*/ !isFuseThread()); |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 2617 | FileUtils.computeDataFromValues(values, volumePath, isFuseThread()); |
Jeff Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 2618 | |
| 2619 | // Create result file |
| 2620 | File res = new File(values.getAsString(MediaColumns.DATA)); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2621 | try { |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2622 | if (makeUnique) { |
Jeff Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 2623 | res = FileUtils.buildUniqueFile(res.getParentFile(), |
| 2624 | mimeType, res.getName()); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2625 | } else { |
Jeff Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 2626 | res = FileUtils.buildNonUniqueFile(res.getParentFile(), |
| 2627 | mimeType, res.getName()); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 2628 | } |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2629 | } catch (FileNotFoundException e) { |
| 2630 | throw new IllegalStateException( |
Jeff Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 2631 | "Failed to build unique file: " + res + " " + values); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2632 | } |
Nikita Ioffe | b5650d1 | 2019-05-22 13:24:41 +0100 | [diff] [blame] | 2633 | |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2634 | // Require that content lives under well-defined directories to help |
| 2635 | // keep the user's content organized |
Nikita Ioffe | b5650d1 | 2019-05-22 13:24:41 +0100 | [diff] [blame] | 2636 | |
Jeff Sharkey | ab27f02 | 2020-04-29 20:58:55 -0600 | [diff] [blame] | 2637 | // 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 Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2641 | |
| 2642 | // Next, consider allowing based on allowed primary directory |
Jeff Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 2643 | final String[] relativePath = values.getAsString(MediaColumns.RELATIVE_PATH).split("/"); |
| 2644 | final String primary = (relativePath.length > 0) ? relativePath[0] : null; |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2645 | 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 Rao | 80ecfba | 2020-04-03 10:48:01 +0100 | [diff] [blame] | 2682 | // 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 Rao | 406cf6d | 2020-04-08 21:52:59 +0100 | [diff] [blame] | 2691 | // Allow apps with MANAGE_EXTERNAL_STORAGE to create files anywhere |
| 2692 | if (!validPath) { |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 2693 | validPath = isCallingPackageManager(); |
Sahana Rao | 406cf6d | 2020-04-08 21:52:59 +0100 | [diff] [blame] | 2694 | } |
| 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 Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 2707 | // Nothing left to check; caller can't use this path |
| 2708 | if (!validPath) { |
Nikita Ioffe | b5650d1 | 2019-05-22 13:24:41 +0100 | [diff] [blame] | 2709 | 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 Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2719 | values.put(MediaColumns.DATA, res.getAbsolutePath()); |
Sahana Rao | b105c22 | 2020-06-17 20:18:48 +0100 | [diff] [blame] | 2720 | // 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 Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 2724 | } else { |
| 2725 | assertFileColumnsSane(match, uri, values); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2726 | } |
| 2727 | |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 2728 | assertPrivatePathNotInValues(values); |
| 2729 | |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2730 | // 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 Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 2735 | 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 Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 2742 | break; |
| 2743 | } |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 2744 | |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 2745 | Trace.endSection(); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2746 | } |
| 2747 | |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 2748 | /** |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 2749 | * 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 Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 2791 | * Sanity check that any requested {@link MediaColumns#DATA} paths actually |
| 2792 | * live on the storage volume being targeted. |
| 2793 | */ |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 2794 | private void assertFileColumnsSane(int match, Uri uri, ContentValues values) |
| 2795 | throws VolumeArgumentException, VolumeNotFoundException { |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 2796 | if (!values.containsKey(MediaColumns.DATA)) return; |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 2797 | |
| 2798 | final String volumeName = resolveVolumeName(uri); |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 2799 | try { |
| 2800 | // Sanity check that the requested path actually lives on volume |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 2801 | final Collection<File> allowed = getVolumeScanPaths(volumeName); |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 2802 | final File actual = new File(values.getAsString(MediaColumns.DATA)) |
| 2803 | .getCanonicalFile(); |
| 2804 | if (!FileUtils.contains(allowed, actual)) { |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 2805 | throw new VolumeArgumentException(actual, allowed); |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 2806 | } |
| 2807 | } catch (IOException e) { |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 2808 | throw new VolumeNotFoundException(volumeName); |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 2809 | } |
| 2810 | } |
| 2811 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2812 | @Override |
| 2813 | public int bulkInsert(Uri uri, ContentValues values[]) { |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 2814 | final int targetSdkVersion = getCallingPackageTargetSdkVersion(); |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 2815 | final boolean allowHidden = isCallingPackageAllowedHidden(); |
| 2816 | final int match = matchUri(uri, allowHidden); |
| 2817 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2818 | if (match == VOLUMES) { |
| 2819 | return super.bulkInsert(uri, values); |
| 2820 | } |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 2821 | |
| 2822 | final DatabaseHelper helper; |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 2823 | try { |
| 2824 | helper = getDatabaseForUri(uri); |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 2825 | } catch (VolumeNotFoundException e) { |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 2826 | return e.translateForUpdateDelete(targetSdkVersion); |
Chih-Chung Chang | 5fde670 | 2010-12-17 18:11:53 +0800 | [diff] [blame] | 2827 | } |
Marco Nelissen | ccf3e3c | 2010-01-25 15:37:40 -0800 | [diff] [blame] | 2828 | |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 2829 | helper.beginTransaction(); |
| 2830 | try { |
Jeff Sharkey | 7cfeb76 | 2019-03-25 09:56:23 -0600 | [diff] [blame] | 2831 | final int result = super.bulkInsert(uri, values); |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 2832 | helper.setTransactionSuccessful(); |
Jeff Sharkey | 7cfeb76 | 2019-03-25 09:56:23 -0600 | [diff] [blame] | 2833 | return result; |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 2834 | } finally { |
| 2835 | helper.endTransaction(); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2836 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 2837 | } |
| 2838 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 2839 | private long insertDirectory(@NonNull SQLiteDatabase db, @NonNull String path) { |
Jeff Sharkey | c55994b | 2019-12-20 19:43:59 -0700 | [diff] [blame] | 2840 | if (LOGV) Log.v(TAG, "inserting directory " + path); |
Mike Lockwood | ed9bbc4 | 2011-01-12 19:32:44 -0500 | [diff] [blame] | 2841 | ContentValues values = new ContentValues(); |
| 2842 | values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); |
| 2843 | values.put(FileColumns.DATA, path); |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 2844 | values.put(FileColumns.PARENT, getParent(db, path)); |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 2845 | 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 Sharkey | a9473e9 | 2020-04-17 15:54:30 -0600 | [diff] [blame] | 2849 | values.put(FileColumns.IS_DOWNLOAD, isDownload(path) ? 1 : 0); |
Mike Lockwood | f2921d9 | 2011-01-18 09:12:46 -0800 | [diff] [blame] | 2850 | File file = new File(path); |
| 2851 | if (file.exists()) { |
| 2852 | values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); |
| 2853 | } |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 2854 | return db.insert("files", FileColumns.DATE_MODIFIED, values); |
Mike Lockwood | ed9bbc4 | 2011-01-12 19:32:44 -0500 | [diff] [blame] | 2855 | } |
| 2856 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 2857 | private long getParent(@NonNull SQLiteDatabase db, @NonNull String path) { |
Jeff Sharkey | 29c9154 | 2019-04-15 15:39:40 -0600 | [diff] [blame] | 2858 | final String parentPath = new File(path).getParent(); |
| 2859 | if (Objects.equals("/", parentPath)) { |
| 2860 | return -1; |
Mike Lockwood | b78ad0d | 2010-07-03 00:45:10 -0400 | [diff] [blame] | 2861 | } else { |
Jeff Sharkey | 29c9154 | 2019-04-15 15:39:40 -0600 | [diff] [blame] | 2862 | 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 Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 2875 | id = insertDirectory(db, parentPath); |
Jeff Sharkey | 29c9154 | 2019-04-15 15:39:40 -0600 | [diff] [blame] | 2876 | } |
| 2877 | } |
| 2878 | |
| 2879 | synchronized (mDirectoryCache) { |
| 2880 | mDirectoryCache.put(parentPath, id); |
| 2881 | } |
| 2882 | return id; |
Mike Lockwood | b78ad0d | 2010-07-03 00:45:10 -0400 | [diff] [blame] | 2883 | } |
| 2884 | } |
| 2885 | |
Sean Stout | cceb5e4 | 2017-09-08 11:16:00 -0700 | [diff] [blame] | 2886 | /** |
Sean Stout | 247d918 | 2018-01-23 11:00:37 -0800 | [diff] [blame] | 2887 | * @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 Stout | cceb5e4 | 2017-09-08 11:16:00 -0700 | [diff] [blame] | 2944 | * 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 Stout | 247d918 | 2018-01-23 11:00:37 -0800 | [diff] [blame] | 2948 | * Second Path Segment: Resource name of title |
Sean Stout | cceb5e4 | 2017-09-08 11:16:00 -0700 | [diff] [blame] | 2949 | * |
Sean Stout | 247d918 | 2018-01-23 11:00:37 -0800 | [diff] [blame] | 2950 | * @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 Stout | cceb5e4 | 2017-09-08 11:16:00 -0700 | [diff] [blame] | 2953 | * @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 Stout | 247d918 | 2018-01-23 11:00:37 -0800 | [diff] [blame] | 2955 | * installed, or it does not have the resource which needs to be localized |
Sean Stout | cceb5e4 | 2017-09-08 11:16:00 -0700 | [diff] [blame] | 2956 | */ |
Sean Stout | 247d918 | 2018-01-23 11:00:37 -0800 | [diff] [blame] | 2957 | private String getTitleFromResourceUri(String title_resource_uri, boolean localize) |
| 2958 | throws Exception { |
| 2959 | if (TextUtils.isEmpty(title_resource_uri)) { |
| 2960 | return null; |
Sean Stout | cceb5e4 | 2017-09-08 11:16:00 -0700 | [diff] [blame] | 2961 | } |
Sean Stout | 247d918 | 2018-01-23 11:00:37 -0800 | [diff] [blame] | 2962 | 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 Stout | cceb5e4 | 2017-09-08 11:16:00 -0700 | [diff] [blame] | 2992 | } |
| 2993 | |
Jeff Sharkey | 7eed0eb | 2019-01-19 17:27:46 -0700 | [diff] [blame] | 2994 | public void onLocaleChanged() { |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 2995 | mInternalDatabase.runWithTransaction((db) -> { |
| 2996 | localizeTitles(db); |
| 2997 | return null; |
| 2998 | }); |
Jeff Sharkey | 7eed0eb | 2019-01-19 17:27:46 -0700 | [diff] [blame] | 2999 | } |
| 3000 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 3001 | private void localizeTitles(@NonNull SQLiteDatabase db) { |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3002 | 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 Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3009 | values.put(AudioColumns.TITLE_RESOURCE_URI, titleResourceUri); |
| 3010 | computeAudioLocalizedValues(values); |
| 3011 | computeAudioKeyValues(values); |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3012 | 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 Stout | cceb5e4 | 2017-09-08 11:16:00 -0700 | [diff] [blame] | 3016 | } |
| 3017 | } |
| 3018 | } |
| 3019 | } |
| 3020 | |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3021 | private Uri insertFile(@NonNull SQLiteQueryBuilder qb, @NonNull DatabaseHelper helper, |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 3022 | int match, @NonNull Uri uri, @NonNull Bundle extras, @NonNull ContentValues values, |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3023 | int mediaType) throws VolumeArgumentException, VolumeNotFoundException { |
Winson | b653af2 | 2019-06-05 12:14:13 -0700 | [diff] [blame] | 3024 | boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA) |
| 3025 | || TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA)); |
| 3026 | |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 3027 | // Make sure all file-related columns are defined |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3028 | ensureUniqueFileColumns(match, uri, extras, values, null); |
Mike Lockwood | 5d7e71a | 2010-07-12 08:49:32 -0400 | [diff] [blame] | 3029 | |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3030 | switch (mediaType) { |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3031 | case FileColumns.MEDIA_TYPE_AUDIO: { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3032 | computeAudioLocalizedValues(values); |
| 3033 | computeAudioKeyValues(values); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3034 | break; |
| 3035 | } |
| 3036 | } |
| 3037 | |
Mike Lockwood | fb598dd | 2010-11-16 10:23:43 -0500 | [diff] [blame] | 3038 | // compute bucket_id and bucket_display_name for all files |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3039 | String path = values.getAsString(MediaStore.MediaColumns.DATA); |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 3040 | FileUtils.computeValuesFromData(values, isFuseThread()); |
Mike Lockwood | fb598dd | 2010-11-16 10:23:43 -0500 | [diff] [blame] | 3041 | values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3042 | |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3043 | String title = values.getAsString(MediaStore.MediaColumns.TITLE); |
Marco Nelissen | 3572ac7 | 2010-11-05 15:46:31 -0700 | [diff] [blame] | 3044 | if (title == null && path != null) { |
Jeff Sharkey | e152d576 | 2019-10-11 17:14:51 -0600 | [diff] [blame] | 3045 | title = extractFileName(path); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3046 | } |
| 3047 | values.put(FileColumns.TITLE, title); |
| 3048 | |
Sudheer Shanka | 0e03f66 | 2019-04-23 12:10:46 -0700 | [diff] [blame] | 3049 | 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 Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3060 | if (format == 0) { |
Jeff Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 3061 | format = MimeUtils.resolveFormatCode(mimeType); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3062 | } |
Marco Nelissen | 89734e1 | 2016-09-30 15:21:35 -0700 | [diff] [blame] | 3063 | if (path != null && path.endsWith("/")) { |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3064 | // TODO: convert to using FallbackException once VERSION_CODES.S is defined |
Marco Nelissen | 759a9b7 | 2016-09-19 14:02:20 -0700 | [diff] [blame] | 3065 | Log.e(TAG, "directory has trailing slash: " + path); |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3066 | return null; |
Marco Nelissen | 759a9b7 | 2016-09-19 14:02:20 -0700 | [diff] [blame] | 3067 | } |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3068 | if (format != 0) { |
| 3069 | values.put(FileColumns.FORMAT, format); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3070 | } |
| 3071 | |
Marco Nelissen | 8247cc4 | 2016-09-28 13:41:25 -0700 | [diff] [blame] | 3072 | if (mimeType == null && path != null && format != MtpConstants.FORMAT_ASSOCIATION) { |
Jeff Sharkey | e152d576 | 2019-10-11 17:14:51 -0600 | [diff] [blame] | 3073 | mimeType = MimeUtils.resolveMimeType(new File(path)); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3074 | } |
Marco Nelissen | ff93c6c | 2018-04-25 12:25:24 -0700 | [diff] [blame] | 3075 | |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3076 | if (mimeType != null) { |
| 3077 | values.put(FileColumns.MIME_TYPE, mimeType); |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 3078 | if (isCallingPackageSelf() && values.containsKey(FileColumns.MEDIA_TYPE)) { |
Sahana Rao | 663f144 | 2020-04-27 20:13:09 +0100 | [diff] [blame] | 3079 | // Leave FileColumns.MEDIA_TYPE untouched if the caller is ModernMediaScanner and |
| 3080 | // FileColumns.MEDIA_TYPE is already populated. |
Sahana Rao | 549c9c3 | 2020-06-15 14:06:55 +0100 | [diff] [blame] | 3081 | } else if (path != null && shouldFileBeHidden(new File(path))) { |
| 3082 | values.put(FileColumns.MEDIA_TYPE, FileColumns.MEDIA_TYPE_NONE); |
| 3083 | } else { |
Sahana Rao | 663f144 | 2020-04-27 20:13:09 +0100 | [diff] [blame] | 3084 | values.put(FileColumns.MEDIA_TYPE, MimeUtils.resolveMediaType(mimeType)); |
| 3085 | } |
Jeff Sharkey | 21e297e | 2019-12-06 18:14:32 -0700 | [diff] [blame] | 3086 | } else { |
| 3087 | values.put(FileColumns.MEDIA_TYPE, mediaType); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3088 | } |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3089 | |
Jeff Sharkey | 4a1293b | 2019-10-18 18:56:36 -0600 | [diff] [blame] | 3090 | final long rowId; |
| 3091 | { |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3092 | if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { |
Marco Nelissen | 3572ac7 | 2010-11-05 15:46:31 -0700 | [diff] [blame] | 3093 | String name = values.getAsString(Audio.Playlists.NAME); |
Mike Lockwood | 282dc90 | 2010-11-12 10:44:38 -0500 | [diff] [blame] | 3094 | if (name == null && path == null) { |
| 3095 | // MediaScanner will compute the name from the path if we have one |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3096 | throw new IllegalArgumentException( |
Mike Lockwood | 282dc90 | 2010-11-12 10:44:38 -0500 | [diff] [blame] | 3097 | "no name was provided when inserting abstract playlist"); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3098 | } |
| 3099 | } else { |
Brian Muramatsu | a36cfae | 2010-11-30 13:46:02 -0800 | [diff] [blame] | 3100 | if (path == null) { |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3101 | // 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 Lockwood | f2921d9 | 2011-01-18 09:12:46 -0800 | [diff] [blame] | 3108 | // 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 Nelissen | 34d6cfa | 2012-08-02 10:20:53 -0700 | [diff] [blame] | 3113 | if (!values.containsKey(FileColumns.SIZE)) { |
| 3114 | values.put(FileColumns.SIZE, file.length()); |
| 3115 | } |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3116 | } |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3117 | } |
| 3118 | |
Nandana Dutt | a1059a4 | 2019-12-23 12:48:00 +0000 | [diff] [blame] | 3119 | rowId = insertAllowingUpsert(qb, helper, values, path); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3120 | } |
Marco Nelissen | d4e1312 | 2012-05-14 13:23:34 -0700 | [diff] [blame] | 3121 | if (format == MtpConstants.FORMAT_ASSOCIATION) { |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 3122 | synchronized (mDirectoryCache) { |
Marco Nelissen | 971408e | 2016-09-12 16:43:50 -0700 | [diff] [blame] | 3123 | mDirectoryCache.put(path, rowId); |
| 3124 | } |
Marco Nelissen | d4e1312 | 2012-05-14 13:23:34 -0700 | [diff] [blame] | 3125 | } |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3126 | |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3127 | return ContentUris.withAppendedId(uri, rowId); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3128 | } |
| 3129 | |
Nandana Dutt | a1059a4 | 2019-12-23 12:48:00 +0000 | [diff] [blame] | 3130 | /** |
| 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 Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 3137 | return helper.runWithTransaction((db) -> { |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 3138 | 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 Dutt | a1059a4 | 2019-12-23 12:48:00 +0000 | [diff] [blame] | 3146 | try { |
| 3147 | return qb.insert(helper, values); |
| 3148 | } catch (SQLiteConstraintException e) { |
Sahana Rao | 25db649 | 2020-06-06 16:22:23 +0100 | [diff] [blame] | 3149 | final String packages = getAllowedPackagesForUpsert( |
| 3150 | values.getAsString(MediaColumns.OWNER_PACKAGE_NAME)); |
Sahana Rao | bb3d94a | 2020-04-07 13:15:05 +0100 | [diff] [blame] | 3151 | SQLiteQueryBuilder qbForUpsert = getQueryBuilderForUpsert(path); |
Sahana Rao | 25db649 | 2020-06-06 16:22:23 +0100 | [diff] [blame] | 3152 | final long rowId = getIdIfPathOwnedByPackages(qbForUpsert, helper, path, packages); |
Nandana Dutt | a1059a4 | 2019-12-23 12:48:00 +0000 | [diff] [blame] | 3153 | // 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 Rao | bb3d94a | 2020-04-07 13:15:05 +0100 | [diff] [blame] | 3157 | if (rowId != -1 && qbForUpsert.update(helper, values, "_id=?", |
Nandana Dutt | a1059a4 | 2019-12-23 12:48:00 +0000 | [diff] [blame] | 3158 | 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 Rao | 25db649 | 2020-06-06 16:22:23 +0100 | [diff] [blame] | 3168 | * @return row id of the entry with path {@code path} if the owner is one of {@code packages}. |
Nandana Dutt | a1059a4 | 2019-12-23 12:48:00 +0000 | [diff] [blame] | 3169 | */ |
Sahana Rao | 25db649 | 2020-06-06 16:22:23 +0100 | [diff] [blame] | 3170 | 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 Dutt | a1059a4 | 2019-12-23 12:48:00 +0000 | [diff] [blame] | 3176 | |
Sahana Rao | 4a81bbc | 2020-04-07 13:01:21 +0100 | [diff] [blame] | 3177 | try (Cursor c = qb.query(helper, projection, selection, new String[] {path}, null, null, |
| 3178 | null, null, null)) { |
Nandana Dutt | a1059a4 | 2019-12-23 12:48:00 +0000 | [diff] [blame] | 3179 | if (c.moveToFirst()) { |
Sahana Rao | 25db649 | 2020-06-06 16:22:23 +0100 | [diff] [blame] | 3180 | return c.getLong(0); |
Nandana Dutt | a1059a4 | 2019-12-23 12:48:00 +0000 | [diff] [blame] | 3181 | } |
| 3182 | } |
| 3183 | return -1; |
| 3184 | } |
| 3185 | |
Sahana Rao | bb3d94a | 2020-04-07 13:15:05 +0100 | [diff] [blame] | 3186 | /** |
Sahana Rao | 25db649 | 2020-06-06 16:22:23 +0100 | [diff] [blame] | 3187 | * 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 Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 3204 | if (givenOwnerPackage != null && isCallingPackageDelegator() && |
Sahana Rao | 25db649 | 2020-06-06 16:22:23 +0100 | [diff] [blame] | 3205 | !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 Rao | bb3d94a | 2020-04-07 13:15:05 +0100 | [diff] [blame] | 3213 | * @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 Rao | bb3d94a | 2020-04-07 13:15:05 +0100 | [diff] [blame] | 3217 | final boolean allowHidden = isCallingPackageAllowedHidden(); |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 3218 | Bundle extras = new Bundle(); |
| 3219 | extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE); |
| 3220 | extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE); |
| 3221 | |
Sahana Rao | bb3d94a | 2020-04-07 13:15:05 +0100 | [diff] [blame] | 3222 | // 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 Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 3224 | // 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 Rao | bb3d94a | 2020-04-07 13:15:05 +0100 | [diff] [blame] | 3227 | SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, matchUri(uri, allowHidden), uri, |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 3228 | extras, null); |
Sahana Rao | bb3d94a | 2020-04-07 13:15:05 +0100 | [diff] [blame] | 3229 | |
| 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 Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 3238 | 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 Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 3245 | private boolean maybeMarkAsDownload(@NonNull ContentValues values) { |
| 3246 | final String path = values.getAsString(MediaColumns.DATA); |
| 3247 | if (path != null && isDownload(path)) { |
Jeff Sharkey | a9473e9 | 2020-04-17 15:54:30 -0600 | [diff] [blame] | 3248 | values.put(FileColumns.IS_DOWNLOAD, 1); |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 3249 | return true; |
| 3250 | } |
| 3251 | return false; |
| 3252 | } |
| 3253 | |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3254 | 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 Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3263 | /** |
| 3264 | * @deprecated all operations should be routed through the overload that |
| 3265 | * accepts a {@link Bundle} of extras. |
| 3266 | */ |
Jeff Sharkey | 199f8c8 | 2019-03-23 11:54:21 -0600 | [diff] [blame] | 3267 | @Override |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3268 | @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 Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 3276 | Trace.beginSection("insert"); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 3277 | try { |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 3278 | 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 Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3287 | } catch (FallbackException e) { |
| 3288 | return e.translateForInsert(getCallingPackageTargetSdkVersion()); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 3289 | } finally { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 3290 | Trace.endSection(); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 3291 | } |
| 3292 | } |
| 3293 | |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3294 | private @Nullable Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues, |
| 3295 | @Nullable Bundle extras) throws FallbackException { |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 3296 | extras = (extras != null) ? extras : new Bundle(); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3297 | |
Nikita Ioffe | 710787b | 2020-06-11 14:35:14 +0100 | [diff] [blame] | 3298 | // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. |
| 3299 | extras.remove(INCLUDED_DEFAULT_DIRECTORIES); |
| 3300 | |
Jeff Sharkey | 199f8c8 | 2019-03-23 11:54:21 -0600 | [diff] [blame] | 3301 | final boolean allowHidden = isCallingPackageAllowedHidden(); |
| 3302 | final int match = matchUri(uri, allowHidden); |
| 3303 | |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 3304 | final int targetSdkVersion = getCallingPackageTargetSdkVersion(); |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3305 | final String originalVolumeName = getVolumeName(uri); |
| 3306 | final String resolvedVolumeName = resolveVolumeName(uri); |
Jeff Sharkey | b39b32d | 2013-09-26 13:49:07 -0700 | [diff] [blame] | 3307 | |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3308 | // handle MEDIA_SCANNER before calling getDatabaseForUri() |
| 3309 | if (match == MEDIA_SCANNER) { |
Mike Lockwood | bc442ef | 2011-07-20 08:29:28 -0700 | [diff] [blame] | 3310 | mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 3311 | |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3312 | final DatabaseHelper helper = getDatabaseForUri( |
| 3313 | MediaStore.Files.getContentUri(mMediaScannerVolume)); |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 3314 | |
Jeff Sharkey | 7ea24f2 | 2019-08-22 10:14:18 -0600 | [diff] [blame] | 3315 | helper.mScanStartTime = SystemClock.elapsedRealtime(); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3316 | return MediaStore.getMediaScannerUri(); |
| 3317 | } |
| 3318 | |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 3319 | if (match == VOLUMES) { |
| 3320 | String name = initialValues.getAsString("name"); |
Zim | 604f452 | 2020-06-05 15:30:09 +0100 | [diff] [blame] | 3321 | Uri attachedVolume = attachVolume(name, /* validate */ true); |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 3322 | if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3323 | final DatabaseHelper helper = getDatabaseForUri( |
| 3324 | MediaStore.Files.getContentUri(mMediaScannerVolume)); |
Jeff Sharkey | 7ea24f2 | 2019-08-22 10:14:18 -0600 | [diff] [blame] | 3325 | helper.mScanStartTime = SystemClock.elapsedRealtime(); |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 3326 | } |
| 3327 | return attachedVolume; |
| 3328 | } |
| 3329 | |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 3330 | 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 Nelissen | 38b4364 | 2012-01-27 09:40:07 -0800 | [diff] [blame] | 3355 | String path = null; |
Jeff Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 3356 | String ownerPackageName = null; |
Mike Lockwood | b8f9b76 | 2011-07-31 17:51:07 -0400 | [diff] [blame] | 3357 | if (initialValues != null) { |
Jeff Sharkey | 41f1854 | 2019-10-16 13:03:38 -0600 | [diff] [blame] | 3358 | // IDs are forever; nobody should be editing them |
| 3359 | initialValues.remove(MediaColumns._ID); |
| 3360 | |
Jeff Sharkey | 05c3a03 | 2020-04-09 16:57:04 -0600 | [diff] [blame] | 3361 | // Expiration times are hard-coded; let's derive them |
| 3362 | FileUtils.computeDateExpires(initialValues); |
| 3363 | |
Jeff Sharkey | b0cab58 | 2019-04-16 12:42:42 -0600 | [diff] [blame] | 3364 | // Ignore or augment incoming raw filesystem paths |
Jeff Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 3365 | for (String column : sDataColumns.keySet()) { |
| 3366 | if (!initialValues.containsKey(column)) continue; |
Jeff Sharkey | b0cab58 | 2019-04-16 12:42:42 -0600 | [diff] [blame] | 3367 | |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 3368 | if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 3369 | // Mutation allowed |
Jeff Sharkey | b0cab58 | 2019-04-16 12:42:42 -0600 | [diff] [blame] | 3370 | } else { |
| 3371 | Log.w(TAG, "Ignoring mutation of " + column + " from " |
| 3372 | + getCallingPackageOrSelf()); |
| 3373 | initialValues.remove(column); |
| 3374 | } |
Jeff Sharkey | 16fb805 | 2018-10-18 15:22:53 -0600 | [diff] [blame] | 3375 | } |
| 3376 | |
Marco Nelissen | 38b4364 | 2012-01-27 09:40:07 -0800 | [diff] [blame] | 3377 | path = initialValues.getAsString(MediaStore.MediaColumns.DATA); |
Jeff Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 3378 | |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 3379 | if (!isCallingPackageSelf()) { |
Sudheer Shanka | e20b007 | 2018-12-21 10:33:48 -0800 | [diff] [blame] | 3380 | initialValues.remove(FileColumns.IS_DOWNLOAD); |
| 3381 | } |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 3382 | |
Jeff Sharkey | d2a3822 | 2018-12-04 11:23:48 -0700 | [diff] [blame] | 3383 | // 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 Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 3391 | 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 Shanka | ff5328a | 2019-04-22 13:13:03 -0700 | [diff] [blame] | 3395 | ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME); |
| 3396 | if (TextUtils.isEmpty(ownerPackageName)) { |
| 3397 | ownerPackageName = extractPathOwnerPackageName(path); |
| 3398 | } |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 3399 | } 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 Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 3408 | } else { |
Sudheer Shanka | ff5328a | 2019-04-22 13:13:03 -0700 | [diff] [blame] | 3409 | // 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 Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 3412 | ownerPackageName = getCallingPackageOrSelf(); |
Jeff Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 3413 | } |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 3414 | } |
Marco Nelissen | 38b4364 | 2012-01-27 09:40:07 -0800 | [diff] [blame] | 3415 | |
Jeff Sharkey | 199f8c8 | 2019-03-23 11:54:21 -0600 | [diff] [blame] | 3416 | long rowId = -1; |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3417 | Uri newUri = null; |
Marco Nelissen | 10af34f | 2011-12-16 17:59:52 -0800 | [diff] [blame] | 3418 | |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3419 | final DatabaseHelper helper = getDatabaseForUri(uri); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 3420 | final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_INSERT, match, uri, extras, null); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3421 | |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3422 | switch (match) { |
| 3423 | case IMAGES_MEDIA: { |
Jeff Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 3424 | maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 3425 | final boolean isDownload = maybeMarkAsDownload(initialValues); |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3426 | newUri = insertFile(qb, helper, match, uri, extras, initialValues, |
| 3427 | FileColumns.MEDIA_TYPE_IMAGE); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3428 | break; |
| 3429 | } |
| 3430 | |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3431 | case IMAGES_THUMBNAILS: { |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 3432 | 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 Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3440 | MediaStore.Images.Media.getContentUri(resolvedVolumeName), imageId), |
| 3441 | extras, true); |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 3442 | |
Jeff Sharkey | ab27f02 | 2020-04-29 20:58:55 -0600 | [diff] [blame] | 3443 | ensureUniqueFileColumns(match, uri, extras, initialValues, null); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 3444 | |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 3445 | rowId = qb.insert(helper, initialValues); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3446 | if (rowId > 0) { |
| 3447 | newUri = ContentUris.withAppendedId(Images.Thumbnails. |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3448 | getContentUri(originalVolumeName), rowId); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3449 | } |
| 3450 | break; |
| 3451 | } |
| 3452 | |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3453 | case VIDEO_THUMBNAILS: { |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 3454 | 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 Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3462 | MediaStore.Video.Media.getContentUri(resolvedVolumeName), videoId), |
| 3463 | Bundle.EMPTY, true); |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 3464 | |
Jeff Sharkey | ab27f02 | 2020-04-29 20:58:55 -0600 | [diff] [blame] | 3465 | ensureUniqueFileColumns(match, uri, extras, initialValues, null); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 3466 | |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 3467 | rowId = qb.insert(helper, initialValues); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3468 | if (rowId > 0) { |
| 3469 | newUri = ContentUris.withAppendedId(Video.Thumbnails. |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3470 | getContentUri(originalVolumeName), rowId); |
Mike Lockwood | afa157c | 2010-09-14 19:34:41 -0400 | [diff] [blame] | 3471 | } |
| 3472 | break; |
| 3473 | } |
| 3474 | |
| 3475 | case AUDIO_MEDIA: { |
Jeff Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 3476 | maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 3477 | final boolean isDownload = maybeMarkAsDownload(initialValues); |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3478 | newUri = insertFile(qb, helper, match, uri, extras, initialValues, |
| 3479 | FileColumns.MEDIA_TYPE_AUDIO); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3480 | break; |
| 3481 | } |
| 3482 | |
| 3483 | case AUDIO_MEDIA_ID_GENRES: { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3484 | throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3485 | } |
| 3486 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3487 | case AUDIO_GENRES: { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3488 | throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3489 | } |
| 3490 | |
| 3491 | case AUDIO_GENRES_ID_MEMBERS: { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3492 | throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3493 | } |
| 3494 | |
| 3495 | case AUDIO_PLAYLISTS: { |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 3496 | maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 3497 | final boolean isDownload = maybeMarkAsDownload(initialValues); |
Mike Lockwood | bc442ef | 2011-07-20 08:29:28 -0700 | [diff] [blame] | 3498 | ContentValues values = new ContentValues(initialValues); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3499 | values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 3500 | // Playlist names are stored as display names, but leave |
| 3501 | // values untouched if the caller is ModernMediaScanner |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 3502 | if (!isCallingPackageSelf()) { |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 3503 | 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 Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3510 | newUri = insertFile(qb, helper, match, uri, extras, values, |
| 3511 | FileColumns.MEDIA_TYPE_PLAYLIST); |
| 3512 | if (newUri != null) { |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 3513 | // 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3520 | } |
| 3521 | break; |
| 3522 | } |
| 3523 | |
| 3524 | case VIDEO_MEDIA: { |
Jeff Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 3525 | maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 3526 | final boolean isDownload = maybeMarkAsDownload(initialValues); |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3527 | newUri = insertFile(qb, helper, match, uri, extras, initialValues, |
| 3528 | FileColumns.MEDIA_TYPE_VIDEO); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3529 | break; |
| 3530 | } |
| 3531 | |
Mike Lockwood | c198bd9 | 2010-09-10 14:55:20 -0400 | [diff] [blame] | 3532 | case AUDIO_ALBUMART: { |
Marco Nelissen | 10af34f | 2011-12-16 17:59:52 -0800 | [diff] [blame] | 3533 | if (helper.mInternal) { |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3534 | throw new UnsupportedOperationException("no internal album art allowed"); |
| 3535 | } |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 3536 | |
Jeff Sharkey | ab27f02 | 2020-04-29 20:58:55 -0600 | [diff] [blame] | 3537 | ensureUniqueFileColumns(match, uri, extras, initialValues, null); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 3538 | |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 3539 | rowId = qb.insert(helper, initialValues); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3540 | if (rowId > 0) { |
| 3541 | newUri = ContentUris.withAppendedId(uri, rowId); |
| 3542 | } |
| 3543 | break; |
Mike Lockwood | c198bd9 | 2010-09-10 14:55:20 -0400 | [diff] [blame] | 3544 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3545 | |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 3546 | case FILES: { |
Jeff Sharkey | 58f533a | 2018-08-06 18:31:51 -0600 | [diff] [blame] | 3547 | maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 3548 | final boolean isDownload = maybeMarkAsDownload(initialValues); |
Jeff Sharkey | e365064 | 2020-04-03 18:50:03 -0600 | [diff] [blame] | 3549 | final String mimeType = initialValues.getAsString(MediaColumns.MIME_TYPE); |
| 3550 | final int mediaType = MimeUtils.resolveMediaType(mimeType); |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3551 | newUri = insertFile(qb, helper, match, uri, extras, initialValues, |
| 3552 | mediaType); |
Mike Lockwood | fc824ed | 2010-10-26 14:40:48 -0400 | [diff] [blame] | 3553 | break; |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 3554 | } |
Mike Lockwood | fc824ed | 2010-10-26 14:40:48 -0400 | [diff] [blame] | 3555 | |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 3556 | case DOWNLOADS: |
| 3557 | maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); |
Jeff Sharkey | a9473e9 | 2020-04-17 15:54:30 -0600 | [diff] [blame] | 3558 | initialValues.put(FileColumns.IS_DOWNLOAD, 1); |
Jeff Sharkey | f95b06f | 2020-06-02 11:10:35 -0600 | [diff] [blame] | 3559 | newUri = insertFile(qb, helper, match, uri, extras, initialValues, |
| 3560 | FileColumns.MEDIA_TYPE_NONE); |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 3561 | break; |
| 3562 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3563 | default: |
| 3564 | throw new UnsupportedOperationException("Invalid URI " + uri); |
| 3565 | } |
| 3566 | |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 3567 | // 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 Sharkey | 470b97e | 2019-10-15 16:32:04 -0600 | [diff] [blame] | 3571 | if (path != null && path.toLowerCase(Locale.ROOT).endsWith("/.nomedia")) { |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 3572 | mMediaScanner.scanFile(new File(path).getParentFile(), REASON_DEMAND); |
Marco Nelissen | 38b4364 | 2012-01-27 09:40:07 -0800 | [diff] [blame] | 3573 | } |
Jeff Sharkey | 199f8c8 | 2019-03-23 11:54:21 -0600 | [diff] [blame] | 3574 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3575 | return newUri; |
| 3576 | } |
| 3577 | |
Marco Nelissen | cb0c5a6 | 2009-12-08 13:44:19 -0800 | [diff] [blame] | 3578 | @Override |
| 3579 | public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) |
| 3580 | throws OperationApplicationException { |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 3581 | // Open transactions on databases for requested volumes |
Hyoungho Choi | b0ddb73 | 2020-03-25 16:49:02 +0900 | [diff] [blame] | 3582 | final Set<DatabaseHelper> transactions = new ArraySet<>(); |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 3583 | try { |
| 3584 | for (ContentProviderOperation op : operations) { |
Hyoungho Choi | b0ddb73 | 2020-03-25 16:49:02 +0900 | [diff] [blame] | 3585 | final DatabaseHelper helper = getDatabaseForUri(op.getUri()); |
Jeff Sharkey | b84a999 | 2020-05-01 20:06:02 -0600 | [diff] [blame] | 3586 | if (transactions.contains(helper)) continue; |
| 3587 | |
| 3588 | if (!helper.isTransactionActive()) { |
Hyoungho Choi | b0ddb73 | 2020-03-25 16:49:02 +0900 | [diff] [blame] | 3589 | helper.beginTransaction(); |
| 3590 | transactions.add(helper); |
Jeff Sharkey | b84a999 | 2020-05-01 20:06:02 -0600 | [diff] [blame] | 3591 | } 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 Nelissen | c80fa20 | 2017-04-19 11:59:01 -0700 | [diff] [blame] | 3600 | } |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 3601 | } |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 3602 | |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 3603 | final ContentProviderResult[] result = super.applyBatch(operations); |
Hyoungho Choi | b0ddb73 | 2020-03-25 16:49:02 +0900 | [diff] [blame] | 3604 | for (DatabaseHelper helper : transactions) { |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 3605 | helper.setTransactionSuccessful(); |
| 3606 | } |
| 3607 | return result; |
Hyoungho Choi | b0ddb73 | 2020-03-25 16:49:02 +0900 | [diff] [blame] | 3608 | } catch (VolumeNotFoundException e) { |
| 3609 | throw e.rethrowAsIllegalArgumentException(); |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 3610 | } finally { |
Hyoungho Choi | b0ddb73 | 2020-03-25 16:49:02 +0900 | [diff] [blame] | 3611 | for (DatabaseHelper helper : transactions) { |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 3612 | helper.endTransaction(); |
Marco Nelissen | cb0c5a6 | 2009-12-08 13:44:19 -0800 | [diff] [blame] | 3613 | } |
| 3614 | } |
| 3615 | } |
| 3616 | |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 3617 | private void appendWhereStandaloneMatch(@NonNull SQLiteQueryBuilder qb, |
| 3618 | @NonNull String column, /* @Match */ int match, Uri uri) { |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3619 | switch (match) { |
| 3620 | case MATCH_INCLUDE: |
| 3621 | // No special filtering needed |
| 3622 | break; |
| 3623 | case MATCH_EXCLUDE: |
Sahana Rao | b02e715 | 2020-06-12 17:07:31 +0100 | [diff] [blame] | 3624 | appendWhereStandalone(qb, getWhereClauseForMatchExclude(column)); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3625 | break; |
| 3626 | case MATCH_ONLY: |
| 3627 | appendWhereStandalone(qb, column + "=?", 1); |
| 3628 | break; |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 3629 | case MATCH_VISIBLE_FOR_FILEPATH: |
| 3630 | final String whereClause = |
| 3631 | getWhereClauseForMatchableVisibleFromFilePath(uri, column); |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 3632 | if (whereClause != null) { |
| 3633 | appendWhereStandalone(qb, whereClause); |
| 3634 | } |
| 3635 | break; |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3636 | default: |
| 3637 | throw new IllegalArgumentException(); |
| 3638 | } |
| 3639 | } |
| 3640 | |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 3641 | private static void appendWhereStandalone(@NonNull SQLiteQueryBuilder qb, |
| 3642 | @Nullable String selection, @Nullable Object... selectionArgs) { |
| 3643 | qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs)); |
| 3644 | } |
| 3645 | |
Jeff Sharkey | 9d29767 | 2020-01-15 13:11:54 -0700 | [diff] [blame] | 3646 | 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 Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 3655 | @Deprecated |
Sahana Rao | 0bbd3e1 | 2020-06-06 15:56:44 +0100 | [diff] [blame] | 3656 | private String getSharedPackages() { |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 3657 | final String[] sharedPackageNames = mCallingIdentity.get().getSharedPackageNames(); |
| 3658 | return bindList((Object[]) sharedPackageNames); |
Gavin Corkery | 75251c4 | 2019-05-07 16:08:57 +0100 | [diff] [blame] | 3659 | } |
| 3660 | |
Sahana Rao | 25db649 | 2020-06-06 16:22:23 +0100 | [diff] [blame] | 3661 | /** |
| 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 Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3674 | private static final int TYPE_QUERY = 0; |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3675 | private static final int TYPE_INSERT = 1; |
| 3676 | private static final int TYPE_UPDATE = 2; |
| 3677 | private static final int TYPE_DELETE = 3; |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3678 | |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 3679 | /** |
| 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 Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3694 | private @NonNull SQLiteQueryBuilder getQueryBuilder(int type, int match, |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 3695 | @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 3696 | Trace.beginSection("getQueryBuilder"); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 3697 | try { |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 3698 | return getQueryBuilderInternal(type, match, uri, extras, honored); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 3699 | } finally { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 3700 | Trace.endSection(); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 3701 | } |
| 3702 | } |
| 3703 | |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3704 | private @NonNull SQLiteQueryBuilder getQueryBuilderInternal(int type, int match, |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 3705 | @NonNull Uri uri, @NonNull Bundle extras, @Nullable Consumer<String> honored) { |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 3706 | final boolean forWrite; |
| 3707 | switch (type) { |
| 3708 | case TYPE_QUERY: forWrite = false; break; |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3709 | case TYPE_INSERT: forWrite = true; break; |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 3710 | case TYPE_UPDATE: forWrite = true; break; |
| 3711 | case TYPE_DELETE: forWrite = true; break; |
| 3712 | default: throw new IllegalStateException(); |
| 3713 | } |
| 3714 | |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3715 | final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); |
Jeff Sharkey | a9473e9 | 2020-04-17 15:54:30 -0600 | [diff] [blame] | 3716 | if (uri.getBooleanQueryParameter("distinct", false)) { |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3717 | qb.setDistinct(true); |
| 3718 | } |
Jeff Sharkey | 3a1265b | 2018-08-06 11:36:08 -0600 | [diff] [blame] | 3719 | qb.setStrict(true); |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 3720 | if (isCallingPackageSelf()) { |
Jeff Sharkey | e365064 | 2020-04-03 18:50:03 -0600 | [diff] [blame] | 3721 | // When caller is system, such as the media scanner, we're willing |
| 3722 | // to let them access any columns they want |
| 3723 | } else { |
Jeff Sharkey | 8e80fca | 2020-05-06 13:41:15 -0600 | [diff] [blame] | 3724 | qb.setTargetSdkVersion(getCallingPackageTargetSdkVersion()); |
Jeff Sharkey | d1961bd | 2020-04-10 14:20:03 -0600 | [diff] [blame] | 3725 | qb.setStrictColumns(true); |
| 3726 | qb.setStrictGrammar(true); |
Jeff Sharkey | 910ba4a | 2020-01-08 10:57:55 -0700 | [diff] [blame] | 3727 | } |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3728 | |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3729 | // 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 Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 3733 | includeVolumes = bindList(getExternalVolumeNames().toArray()); |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3734 | } else { |
| 3735 | includeVolumes = bindList(volumeName); |
| 3736 | } |
Sahana Rao | 0bbd3e1 | 2020-06-06 15:56:44 +0100 | [diff] [blame] | 3737 | final String sharedPackages = getSharedPackages(); |
Sahana Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 3738 | final String matchSharedPackagesClause = FileColumns.OWNER_PACKAGE_NAME + " IN " |
| 3739 | + sharedPackages; |
| 3740 | |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 3741 | final boolean allowGlobal = checkCallingPermissionGlobal(uri, forWrite); |
shafik | e87a1d2 | 2020-02-27 15:52:09 +0000 | [diff] [blame] | 3742 | final boolean allowLegacy = |
| 3743 | forWrite ? isCallingPackageLegacyWrite() : isCallingPackageLegacyRead(); |
Jeff Sharkey | 4816607 | 2019-04-18 14:38:47 -0600 | [diff] [blame] | 3744 | final boolean allowLegacyRead = allowLegacy && !forWrite; |
Jeff Sharkey | 0bf693f | 2018-10-27 19:47:17 -0600 | [diff] [blame] | 3745 | |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3746 | 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 | |
shafik | ac34fe9 | 2020-02-25 15:28:55 +0000 | [diff] [blame] | 3750 | final ArrayList<String> includedDefaultDirs = extras.getStringArrayList( |
| 3751 | INCLUDED_DEFAULT_DIRECTORIES); |
| 3752 | |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3753 | // Handle callers using legacy arguments |
| 3754 | if (MediaStore.getIncludePending(uri)) matchPending = MATCH_INCLUDE; |
| 3755 | |
| 3756 | // Resolve any remaining default options |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 3757 | 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 Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 3761 | defaultMatchForPendingAndTrashed = |
| 3762 | forWrite ? MATCH_INCLUDE : MATCH_VISIBLE_FOR_FILEPATH; |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 3763 | } else { |
| 3764 | defaultMatchForPendingAndTrashed = MATCH_EXCLUDE; |
| 3765 | } |
| 3766 | if (matchPending == MATCH_DEFAULT) matchPending = defaultMatchForPendingAndTrashed; |
| 3767 | if (matchTrashed == MATCH_DEFAULT) matchTrashed = defaultMatchForPendingAndTrashed; |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3768 | if (matchFavorite == MATCH_DEFAULT) matchFavorite = MATCH_INCLUDE; |
| 3769 | |
Jeff Sharkey | 9d29767 | 2020-01-15 13:11:54 -0700 | [diff] [blame] | 3770 | // Handle callers using legacy filtering |
| 3771 | final String filter = uri.getQueryParameter("filter"); |
| 3772 | |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3773 | boolean includeAllVolumes = false; |
Sahana Rao | 0bbd3e1 | 2020-06-06 15:56:44 +0100 | [diff] [blame] | 3774 | final String callingPackage = getCallingPackageOrSelf(); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 3775 | |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3776 | switch (match) { |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3777 | case IMAGES_MEDIA_ID: |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 3778 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3779 | matchPending = MATCH_INCLUDE; |
| 3780 | matchTrashed = MATCH_INCLUDE; |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 3781 | // fall-through |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3782 | case IMAGES_MEDIA: { |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3783 | if (type == TYPE_QUERY) { |
| 3784 | qb.setTables("images"); |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3785 | qb.setProjectionMap( |
| 3786 | getProjectionMap(Images.Media.class)); |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3787 | } else { |
| 3788 | qb.setTables("files"); |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3789 | qb.setProjectionMap( |
| 3790 | getProjectionMap(Images.Media.class, Files.FileColumns.class)); |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3791 | appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", |
| 3792 | FileColumns.MEDIA_TYPE_IMAGE); |
| 3793 | } |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 3794 | if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) { |
Sahana Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 3795 | appendWhereStandalone(qb, matchSharedPackagesClause); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 3796 | } |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 3797 | appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); |
| 3798 | appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); |
| 3799 | appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 3800 | 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 Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3805 | if (!includeAllVolumes) { |
| 3806 | appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); |
| 3807 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3808 | break; |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3809 | } |
Ray Chen | b386112 | 2009-09-07 23:39:01 -0700 | [diff] [blame] | 3810 | case IMAGES_THUMBNAILS_ID: |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 3811 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3812 | // fall-through |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 3813 | case IMAGES_THUMBNAILS: { |
Jeff Sharkey | 556d2d9 | 2018-07-12 19:51:27 -0600 | [diff] [blame] | 3814 | qb.setTables("thumbnails"); |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 3815 | |
| 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 Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 3822 | if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) { |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 3823 | appendWhereStandalone(qb, |
Sahana Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 3824 | "image_id IN (SELECT _id FROM images WHERE " |
| 3825 | + matchSharedPackagesClause + ")"); |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 3826 | } |
Ray Chen | b386112 | 2009-09-07 23:39:01 -0700 | [diff] [blame] | 3827 | break; |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 3828 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3829 | case AUDIO_MEDIA_ID: |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 3830 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3831 | matchPending = MATCH_INCLUDE; |
| 3832 | matchTrashed = MATCH_INCLUDE; |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3833 | // fall-through |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3834 | case AUDIO_MEDIA: { |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3835 | if (type == TYPE_QUERY) { |
| 3836 | qb.setTables("audio"); |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3837 | qb.setProjectionMap( |
| 3838 | getProjectionMap(Audio.Media.class)); |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3839 | } else { |
| 3840 | qb.setTables("files"); |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3841 | qb.setProjectionMap( |
| 3842 | getProjectionMap(Audio.Media.class, Files.FileColumns.class)); |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3843 | appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", |
| 3844 | FileColumns.MEDIA_TYPE_AUDIO); |
| 3845 | } |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 3846 | if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) { |
Jeff Sharkey | ec2508f | 2019-02-20 18:13:54 -0700 | [diff] [blame] | 3847 | // 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 Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 3851 | DatabaseUtils.bindSelection(matchSharedPackagesClause |
Gavin Corkery | 75251c4 | 2019-05-07 16:08:57 +0100 | [diff] [blame] | 3852 | + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1")); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 3853 | } |
Jeff Sharkey | 9d29767 | 2020-01-15 13:11:54 -0700 | [diff] [blame] | 3854 | appendWhereStandaloneFilter(qb, new String[] { |
| 3855 | AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY |
| 3856 | }, filter); |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 3857 | appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); |
| 3858 | appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); |
| 3859 | appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 3860 | 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 Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3865 | if (!includeAllVolumes) { |
| 3866 | appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); |
| 3867 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3868 | break; |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3869 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3870 | case AUDIO_MEDIA_ID_GENRES_ID: |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3871 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5)); |
| 3872 | // fall-through |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3873 | case AUDIO_MEDIA_ID_GENRES: { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3874 | 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 Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3880 | appendWhereStandalone(qb, "_id IN (SELECT genre_id FROM " + |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3881 | "audio WHERE _id=?)", uri.getPathSegments().get(3)); |
Jeff Sharkey | 1f6253a | 2019-02-15 17:38:56 -0700 | [diff] [blame] | 3882 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3887 | break; |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3888 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3889 | case AUDIO_GENRES_ID: |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 3890 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3891 | // fall-through |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3892 | case AUDIO_GENRES: { |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3893 | qb.setTables("audio_genres"); |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 3894 | qb.setProjectionMap(getProjectionMap(Audio.Genres.class)); |
Jeff Sharkey | 1f6253a | 2019-02-15 17:38:56 -0700 | [diff] [blame] | 3895 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3900 | break; |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3901 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3902 | case AUDIO_GENRES_ID_MEMBERS: |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 3903 | appendWhereStandalone(qb, "genre_id=?", uri.getPathSegments().get(3)); |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 3904 | // fall-through |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3905 | case AUDIO_GENRES_ALL_MEMBERS: { |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 3906 | if (type == TYPE_QUERY) { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3907 | 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 Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 3914 | } else { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 3915 | throw new UnsupportedOperationException("Genres cannot be directly modified"); |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 3916 | } |
Jeff Sharkey | 9d29767 | 2020-01-15 13:11:54 -0700 | [diff] [blame] | 3917 | appendWhereStandaloneFilter(qb, new String[] { |
| 3918 | AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY |
| 3919 | }, filter); |
Jeff Sharkey | 1f6253a | 2019-02-15 17:38:56 -0700 | [diff] [blame] | 3920 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3925 | break; |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3926 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3927 | case AUDIO_PLAYLISTS_ID: |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 3928 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 3929 | matchPending = MATCH_INCLUDE; |
| 3930 | matchTrashed = MATCH_INCLUDE; |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3931 | // fall-through |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3932 | case AUDIO_PLAYLISTS: { |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3933 | if (type == TYPE_QUERY) { |
| 3934 | qb.setTables("audio_playlists"); |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3935 | qb.setProjectionMap( |
| 3936 | getProjectionMap(Audio.Playlists.class)); |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3937 | } else { |
| 3938 | qb.setTables("files"); |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3939 | qb.setProjectionMap( |
| 3940 | getProjectionMap(Audio.Playlists.class, Files.FileColumns.class)); |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 3941 | appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", |
| 3942 | FileColumns.MEDIA_TYPE_PLAYLIST); |
| 3943 | } |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 3944 | if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) { |
Sahana Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 3945 | appendWhereStandalone(qb, matchSharedPackagesClause); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 3946 | } |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 3947 | appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); |
| 3948 | appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); |
| 3949 | appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 3950 | 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 Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3955 | if (!includeAllVolumes) { |
| 3956 | appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); |
| 3957 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3958 | break; |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3959 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3960 | case AUDIO_PLAYLISTS_ID_MEMBERS_ID: |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 3961 | appendWhereStandalone(qb, "audio_playlists_map._id=?", |
Jeff Sharkey | 556d2d9 | 2018-07-12 19:51:27 -0600 | [diff] [blame] | 3962 | uri.getPathSegments().get(5)); |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 3963 | // fall-through |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 3964 | case AUDIO_PLAYLISTS_ID_MEMBERS: { |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 3965 | appendWhereStandalone(qb, "playlist_id=?", uri.getPathSegments().get(3)); |
| 3966 | if (type == TYPE_QUERY) { |
| 3967 | qb.setTables("audio_playlists_map, audio"); |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 3968 | |
| 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 Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 3975 | appendWhereStandalone(qb, "audio._id = audio_id"); |
| 3976 | } else { |
| 3977 | qb.setTables("audio_playlists_map"); |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 3978 | qb.setProjectionMap(getProjectionMap(Audio.Playlists.Members.class)); |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 3979 | } |
Jeff Sharkey | 9d29767 | 2020-01-15 13:11:54 -0700 | [diff] [blame] | 3980 | appendWhereStandaloneFilter(qb, new String[] { |
| 3981 | AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY |
| 3982 | }, filter); |
Jeff Sharkey | 1f6253a | 2019-02-15 17:38:56 -0700 | [diff] [blame] | 3983 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3988 | break; |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 3989 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 3990 | case AUDIO_ALBUMART_ID: |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 3991 | appendWhereStandalone(qb, "album_id=?", uri.getPathSegments().get(3)); |
Jeff Sharkey | 9b7078d | 2019-03-22 14:30:28 -0600 | [diff] [blame] | 3992 | // fall-through |
| 3993 | case AUDIO_ALBUMART: { |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 3994 | qb.setTables("album_art"); |
Jeff Sharkey | 9b7078d | 2019-03-22 14:30:28 -0600 | [diff] [blame] | 3995 | |
| 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 Sharkey | 1f6253a | 2019-02-15 17:38:56 -0700 | [diff] [blame] | 4002 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 4007 | break; |
Jeff Sharkey | 9b7078d | 2019-03-22 14:30:28 -0600 | [diff] [blame] | 4008 | } |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 4009 | case AUDIO_ARTISTS_ID_ALBUMS: { |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4010 | if (type == TYPE_QUERY) { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 4011 | qb.setTables("audio_albums"); |
| 4012 | qb.setProjectionMap(getProjectionMap(Audio.Artists.Albums.class)); |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4013 | |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 4014 | final String artistId = uri.getPathSegments().get(3); |
| 4015 | appendWhereStandalone(qb, "artist_id=?", artistId); |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4016 | } else { |
| 4017 | throw new UnsupportedOperationException("Albums cannot be directly modified"); |
| 4018 | } |
Jeff Sharkey | 9d29767 | 2020-01-15 13:11:54 -0700 | [diff] [blame] | 4019 | appendWhereStandaloneFilter(qb, new String[] { |
| 4020 | AudioColumns.ALBUM_KEY |
| 4021 | }, filter); |
Jeff Sharkey | 1f6253a | 2019-02-15 17:38:56 -0700 | [diff] [blame] | 4022 | 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 Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4027 | break; |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 4028 | } |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4029 | case AUDIO_ARTISTS_ID: |
| 4030 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); |
| 4031 | // fall-through |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4032 | case AUDIO_ARTISTS: { |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4033 | if (type == TYPE_QUERY) { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 4034 | qb.setTables("audio_artists"); |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 4035 | qb.setProjectionMap(getProjectionMap(Audio.Artists.class)); |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4036 | } else { |
| 4037 | throw new UnsupportedOperationException("Artists cannot be directly modified"); |
| 4038 | } |
Jeff Sharkey | 9d29767 | 2020-01-15 13:11:54 -0700 | [diff] [blame] | 4039 | appendWhereStandaloneFilter(qb, new String[] { |
| 4040 | AudioColumns.ARTIST_KEY |
| 4041 | }, filter); |
Jeff Sharkey | 1f6253a | 2019-02-15 17:38:56 -0700 | [diff] [blame] | 4042 | 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 Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4047 | break; |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4048 | } |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4049 | case AUDIO_ALBUMS_ID: |
| 4050 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); |
| 4051 | // fall-through |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 4052 | case AUDIO_ALBUMS: { |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4053 | if (type == TYPE_QUERY) { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 4054 | qb.setTables("audio_albums"); |
| 4055 | qb.setProjectionMap(getProjectionMap(Audio.Albums.class)); |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4056 | } else { |
| 4057 | throw new UnsupportedOperationException("Albums cannot be directly modified"); |
| 4058 | } |
Jeff Sharkey | 9d29767 | 2020-01-15 13:11:54 -0700 | [diff] [blame] | 4059 | appendWhereStandaloneFilter(qb, new String[] { |
| 4060 | AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY |
| 4061 | }, filter); |
Jeff Sharkey | 1f6253a | 2019-02-15 17:38:56 -0700 | [diff] [blame] | 4062 | 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 Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 4067 | break; |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 4068 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 4069 | case VIDEO_MEDIA_ID: |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 4070 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4071 | matchPending = MATCH_INCLUDE; |
| 4072 | matchTrashed = MATCH_INCLUDE; |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 4073 | // fall-through |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4074 | case VIDEO_MEDIA: { |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 4075 | if (type == TYPE_QUERY) { |
| 4076 | qb.setTables("video"); |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4077 | qb.setProjectionMap( |
| 4078 | getProjectionMap(Video.Media.class)); |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 4079 | } else { |
| 4080 | qb.setTables("files"); |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4081 | qb.setProjectionMap( |
| 4082 | getProjectionMap(Video.Media.class, Files.FileColumns.class)); |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 4083 | appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", |
| 4084 | FileColumns.MEDIA_TYPE_VIDEO); |
| 4085 | } |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 4086 | if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) { |
Sahana Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 4087 | appendWhereStandalone(qb, matchSharedPackagesClause); |
Jeff Sharkey | 2fd9da7 | 2018-11-02 23:52:06 -0600 | [diff] [blame] | 4088 | } |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 4089 | appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); |
| 4090 | appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); |
| 4091 | appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 4092 | 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 Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 4097 | if (!includeAllVolumes) { |
| 4098 | appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); |
| 4099 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 4100 | break; |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4101 | } |
Ray Chen | b386112 | 2009-09-07 23:39:01 -0700 | [diff] [blame] | 4102 | case VIDEO_THUMBNAILS_ID: |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 4103 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 4104 | // fall-through |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4105 | case VIDEO_THUMBNAILS: { |
Jeff Sharkey | 556d2d9 | 2018-07-12 19:51:27 -0600 | [diff] [blame] | 4106 | qb.setTables("videothumbnails"); |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 4107 | qb.setProjectionMap(getProjectionMap(Video.Thumbnails.class)); |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 4108 | if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) { |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 4109 | appendWhereStandalone(qb, |
Sahana Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 4110 | "video_id IN (SELECT _id FROM video WHERE " + |
| 4111 | matchSharedPackagesClause + ")"); |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 4112 | } |
Ray Chen | b386112 | 2009-09-07 23:39:01 -0700 | [diff] [blame] | 4113 | break; |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4114 | } |
Mike Lockwood | 16dc0fd | 2010-09-08 12:52:17 -0400 | [diff] [blame] | 4115 | case FILES_ID: |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 4116 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4117 | matchPending = MATCH_INCLUDE; |
| 4118 | matchTrashed = MATCH_INCLUDE; |
Jeff Sharkey | ea2c967 | 2018-07-26 18:32:44 -0600 | [diff] [blame] | 4119 | // fall-through |
Jeff Sharkey | a7a789a | 2019-12-17 13:45:57 -0700 | [diff] [blame] | 4120 | case FILES: { |
Jeff Sharkey | 556d2d9 | 2018-07-12 19:51:27 -0600 | [diff] [blame] | 4121 | qb.setTables("files"); |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 4122 | qb.setProjectionMap(getProjectionMap(Files.FileColumns.class)); |
Jeff Sharkey | 0bf693f | 2018-10-27 19:47:17 -0600 | [diff] [blame] | 4123 | |
| 4124 | final ArrayList<String> options = new ArrayList<>(); |
Jeff Sharkey | 4816607 | 2019-04-18 14:38:47 -0600 | [diff] [blame] | 4125 | if (!allowGlobal && !allowLegacyRead) { |
Sahana Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 4126 | options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause)); |
Jeff Sharkey | 4816607 | 2019-04-18 14:38:47 -0600 | [diff] [blame] | 4127 | if (allowLegacy) { |
| 4128 | options.add(DatabaseUtils.bindSelection("volume_name=?", |
| 4129 | MediaStore.VOLUME_EXTERNAL_PRIMARY)); |
| 4130 | } |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 4131 | if (checkCallingPermissionAudio(forWrite, callingPackage)) { |
| 4132 | options.add(DatabaseUtils.bindSelection("media_type=?", |
| 4133 | FileColumns.MEDIA_TYPE_AUDIO)); |
Jeff Sharkey | 1f6253a | 2019-02-15 17:38:56 -0700 | [diff] [blame] | 4134 | options.add(DatabaseUtils.bindSelection("media_type=?", |
| 4135 | FileColumns.MEDIA_TYPE_PLAYLIST)); |
Jeff Sharkey | 21e297e | 2019-12-06 18:14:32 -0700 | [diff] [blame] | 4136 | options.add(DatabaseUtils.bindSelection("media_type=?", |
| 4137 | FileColumns.MEDIA_TYPE_SUBTITLE)); |
Sahana Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 4138 | options.add(matchSharedPackagesClause |
| 4139 | + " AND media_type=0 AND mime_type LIKE 'audio/%'"); |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 4140 | } |
| 4141 | if (checkCallingPermissionVideo(forWrite, callingPackage)) { |
| 4142 | options.add(DatabaseUtils.bindSelection("media_type=?", |
| 4143 | FileColumns.MEDIA_TYPE_VIDEO)); |
Jeff Sharkey | 21e297e | 2019-12-06 18:14:32 -0700 | [diff] [blame] | 4144 | options.add(DatabaseUtils.bindSelection("media_type=?", |
| 4145 | FileColumns.MEDIA_TYPE_SUBTITLE)); |
Sahana Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 4146 | options.add(matchSharedPackagesClause |
| 4147 | + " AND media_type=0 AND mime_type LIKE 'video/%'"); |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 4148 | } |
| 4149 | if (checkCallingPermissionImages(forWrite, callingPackage)) { |
| 4150 | options.add(DatabaseUtils.bindSelection("media_type=?", |
| 4151 | FileColumns.MEDIA_TYPE_IMAGE)); |
Sahana Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 4152 | options.add(matchSharedPackagesClause |
| 4153 | + " AND media_type=0 AND mime_type LIKE 'image/%'"); |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 4154 | } |
shafik | ac34fe9 | 2020-02-25 15:28:55 +0000 | [diff] [blame] | 4155 | if (includedDefaultDirs != null) { |
| 4156 | for (String defaultDir : includedDefaultDirs) { |
| 4157 | options.add(FileColumns.RELATIVE_PATH + " LIKE '" + defaultDir + "/%'"); |
| 4158 | } |
| 4159 | } |
Jeff Sharkey | 0bf693f | 2018-10-27 19:47:17 -0600 | [diff] [blame] | 4160 | } |
| 4161 | if (options.size() > 0) { |
| 4162 | appendWhereStandalone(qb, TextUtils.join(" OR ", options)); |
| 4163 | } |
| 4164 | |
Jeff Sharkey | 9d29767 | 2020-01-15 13:11:54 -0700 | [diff] [blame] | 4165 | appendWhereStandaloneFilter(qb, new String[] { |
| 4166 | AudioColumns.ARTIST_KEY, AudioColumns.ALBUM_KEY, AudioColumns.TITLE_KEY |
| 4167 | }, filter); |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 4168 | appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); |
| 4169 | appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); |
| 4170 | appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 4171 | 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 Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 4176 | if (!includeAllVolumes) { |
| 4177 | appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); |
| 4178 | } |
Mike Lockwood | 1717955 | 2010-07-09 10:46:58 -0400 | [diff] [blame] | 4179 | break; |
Jeff Sharkey | 4816607 | 2019-04-18 14:38:47 -0600 | [diff] [blame] | 4180 | } |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 4181 | case DOWNLOADS_ID: |
| 4182 | appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4183 | matchPending = MATCH_INCLUDE; |
| 4184 | matchTrashed = MATCH_INCLUDE; |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 4185 | // fall-through |
Jeff Sharkey | 4816607 | 2019-04-18 14:38:47 -0600 | [diff] [blame] | 4186 | case DOWNLOADS: { |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 4187 | if (type == TYPE_QUERY) { |
| 4188 | qb.setTables("downloads"); |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4189 | qb.setProjectionMap( |
| 4190 | getProjectionMap(Downloads.class)); |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 4191 | } else { |
| 4192 | qb.setTables("files"); |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4193 | qb.setProjectionMap( |
| 4194 | getProjectionMap(Downloads.class, Files.FileColumns.class)); |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 4195 | appendWhereStandalone(qb, FileColumns.IS_DOWNLOAD + "=1"); |
| 4196 | } |
Jeff Sharkey | 4816607 | 2019-04-18 14:38:47 -0600 | [diff] [blame] | 4197 | |
| 4198 | final ArrayList<String> options = new ArrayList<>(); |
| 4199 | if (!allowGlobal && !allowLegacyRead) { |
Sahana Rao | 3279b46 | 2020-04-23 00:29:39 +0100 | [diff] [blame] | 4200 | options.add(DatabaseUtils.bindSelection(matchSharedPackagesClause)); |
Jeff Sharkey | 4816607 | 2019-04-18 14:38:47 -0600 | [diff] [blame] | 4201 | if (allowLegacy) { |
| 4202 | options.add(DatabaseUtils.bindSelection("volume_name=?", |
| 4203 | MediaStore.VOLUME_EXTERNAL_PRIMARY)); |
| 4204 | } |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 4205 | } |
Jeff Sharkey | 4816607 | 2019-04-18 14:38:47 -0600 | [diff] [blame] | 4206 | if (options.size() > 0) { |
| 4207 | appendWhereStandalone(qb, TextUtils.join(" OR ", options)); |
| 4208 | } |
| 4209 | |
Sahana Rao | 02fb8f4 | 2020-05-14 16:54:35 +0100 | [diff] [blame] | 4210 | appendWhereStandaloneMatch(qb, FileColumns.IS_PENDING, matchPending, uri); |
| 4211 | appendWhereStandaloneMatch(qb, FileColumns.IS_TRASHED, matchTrashed, uri); |
| 4212 | appendWhereStandaloneMatch(qb, FileColumns.IS_FAVORITE, matchFavorite, uri); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 4213 | 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 Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 4218 | if (!includeAllVolumes) { |
| 4219 | appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); |
| 4220 | } |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 4221 | break; |
Jeff Sharkey | 4816607 | 2019-04-18 14:38:47 -0600 | [diff] [blame] | 4222 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 4223 | default: |
| 4224 | throw new UnsupportedOperationException( |
| 4225 | "Unknown or unsupported URL: " + uri.toString()); |
| 4226 | } |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 4227 | |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4228 | // 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 Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 4233 | |
Jeff Sharkey | cbdea0f | 2019-12-16 16:10:18 -0700 | [diff] [blame] | 4234 | // 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 Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 4239 | |
Jeff Sharkey | 556d2d9 | 2018-07-12 19:51:27 -0600 | [diff] [blame] | 4240 | return qb; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 4241 | } |
| 4242 | |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 4243 | /** |
| 4244 | * Determine if given {@link Uri} has a |
| 4245 | * {@link MediaColumns#OWNER_PACKAGE_NAME} column. |
| 4246 | */ |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 4247 | private boolean hasOwnerPackageName(Uri uri) { |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 4248 | // 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 Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4264 | /** |
| 4265 | * @deprecated all operations should be routed through the overload that |
| 4266 | * accepts a {@link Bundle} of extras. |
| 4267 | */ |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 4268 | @Override |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4269 | @Deprecated |
| 4270 | public int delete(Uri uri, String selection, String[] selectionArgs) { |
Jeff Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 4271 | return delete(uri, |
| 4272 | DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null)); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4273 | } |
| 4274 | |
| 4275 | @Override |
| 4276 | public int delete(@NonNull Uri uri, @Nullable Bundle extras) { |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 4277 | Trace.beginSection("delete"); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 4278 | try { |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4279 | return deleteInternal(uri, extras); |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 4280 | } catch (FallbackException e) { |
| 4281 | return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion()); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 4282 | } finally { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 4283 | Trace.endSection(); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 4284 | } |
| 4285 | } |
| 4286 | |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4287 | private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras) |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 4288 | throws FallbackException { |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 4289 | extras = (extras != null) ? extras : new Bundle(); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4290 | |
Nikita Ioffe | 710787b | 2020-06-11 14:35:14 +0100 | [diff] [blame] | 4291 | // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. |
| 4292 | extras.remove(INCLUDED_DEFAULT_DIRECTORIES); |
| 4293 | |
Sahana Rao | 8affdb0 | 2020-06-08 18:28:57 +0100 | [diff] [blame] | 4294 | 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 Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4321 | final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION); |
| 4322 | final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); |
| 4323 | |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 4324 | int count = 0; |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 4325 | |
Jeff Sharkey | 199f8c8 | 2019-03-23 11:54:21 -0600 | [diff] [blame] | 4326 | final String volumeName = getVolumeName(uri); |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 4327 | final int targetSdkVersion = getCallingPackageTargetSdkVersion(); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 4328 | |
| 4329 | // handle MEDIA_SCANNER before calling getDatabaseForUri() |
| 4330 | if (match == MEDIA_SCANNER) { |
| 4331 | if (mMediaScannerVolume == null) { |
| 4332 | return 0; |
| 4333 | } |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 4334 | |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 4335 | final DatabaseHelper helper = getDatabaseForUri( |
| 4336 | MediaStore.Files.getContentUri(mMediaScannerVolume)); |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 4337 | |
Jeff Sharkey | 7ea24f2 | 2019-08-22 10:14:18 -0600 | [diff] [blame] | 4338 | helper.mScanStopTime = SystemClock.elapsedRealtime(); |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 4339 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 4340 | mMediaScannerVolume = null; |
| 4341 | return 1; |
| 4342 | } |
| 4343 | |
Mike Lockwood | 819cafd | 2011-01-21 17:05:41 -0800 | [diff] [blame] | 4344 | if (match == VOLUMES_ID) { |
| 4345 | detachVolume(uri); |
| 4346 | count = 1; |
Jeff Sharkey | 199f8c8 | 2019-03-23 11:54:21 -0600 | [diff] [blame] | 4347 | } |
Jeff Sharkey | b39b32d | 2013-09-26 13:49:07 -0700 | [diff] [blame] | 4348 | |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 4349 | 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 Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 4366 | final DatabaseHelper helper = getDatabaseForUri(uri); |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 4367 | final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, match, uri, extras, null); |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 4368 | |
Jeff Sharkey | 199f8c8 | 2019-03-23 11:54:21 -0600 | [diff] [blame] | 4369 | { |
Jeff Sharkey | b6485ce | 2019-04-15 14:20:47 -0600 | [diff] [blame] | 4370 | // 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 Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4376 | enforceCallingPermission(uri, extras, true); |
Jeff Sharkey | b6485ce | 2019-04-15 14:20:47 -0600 | [diff] [blame] | 4377 | } |
| 4378 | |
Sudheer Shanka | 1a79ac0 | 2019-01-17 13:14:52 -0800 | [diff] [blame] | 4379 | final String[] projection = new String[] { |
| 4380 | FileColumns.MEDIA_TYPE, |
| 4381 | FileColumns.DATA, |
| 4382 | FileColumns._ID, |
| 4383 | FileColumns.IS_DOWNLOAD, |
| 4384 | FileColumns.MIME_TYPE, |
Sudheer Shanka | 1a79ac0 | 2019-01-17 13:14:52 -0800 | [diff] [blame] | 4385 | }; |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 4386 | final boolean isFilesTable = qb.getTables().equals("files"); |
Sudheer Shanka | 1a79ac0 | 2019-01-17 13:14:52 -0800 | [diff] [blame] | 4387 | final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>(); |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 4388 | if (isFilesTable) { |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4389 | String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA); |
| 4390 | if (deleteparam == null || ! deleteparam.equals("false")) { |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 4391 | Cursor c = qb.query(helper, projection, userWhere, userWhereArgs, |
| 4392 | null, null, null, null, null); |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4393 | 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 Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 4398 | final int isDownload = c.getInt(3); |
Sudheer Shanka | 1a79ac0 | 2019-01-17 13:14:52 -0800 | [diff] [blame] | 4399 | final String mimeType = c.getString(4); |
Jeff Sharkey | b39b32d | 2013-09-26 13:49:07 -0700 | [diff] [blame] | 4400 | |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 4401 | // Forget that caller is owner of this item |
| 4402 | mCallingIdentity.get().setOwned(id, false); |
| 4403 | |
shafik | f4290c0 | 2019-12-06 10:53:26 +0000 | [diff] [blame] | 4404 | deleteIfAllowed(uri, extras, data); |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 4405 | count += qb.delete(helper, BaseColumns._ID + "=" + id, null); |
shafik | f4290c0 | 2019-12-06 10:53:26 +0000 | [diff] [blame] | 4406 | |
Sudheer Shanka | 1a79ac0 | 2019-01-17 13:14:52 -0800 | [diff] [blame] | 4407 | // Only need to inform DownloadProvider about the downloads deleted on |
| 4408 | // external volume. |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 4409 | if (isDownload == 1) { |
Sudheer Shanka | 2b79a5c | 2019-04-12 10:04:20 -0700 | [diff] [blame] | 4410 | deletedDownloadIds.put(id, mimeType); |
Sudheer Shanka | 1a79ac0 | 2019-01-17 13:14:52 -0800 | [diff] [blame] | 4411 | } |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 4412 | |
| 4413 | // Update any playlists that reference this item |
| 4414 | if ((mediaType == FileColumns.MEDIA_TYPE_AUDIO) |
| 4415 | && helper.isExternal()) { |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 4416 | 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 Nelissen | 4eff7fe | 2012-04-06 12:41:41 -0700 | [diff] [blame] | 4426 | } |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 4427 | return null; |
| 4428 | }); |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4429 | } |
| 4430 | } |
| 4431 | } finally { |
Jeff Sharkey | b6781bc | 2019-07-18 18:45:52 -0600 | [diff] [blame] | 4432 | FileUtils.closeQuietly(c); |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4433 | } |
| 4434 | // Do not allow deletion if the file/object is referenced as parent |
| 4435 | // by some other entries. It could cause database corruption. |
Jeff Sharkey | f57fa20 | 2018-07-26 14:36:41 -0600 | [diff] [blame] | 4436 | appendWhereStandalone(qb, ID_NOT_PARENT_CLAUSE); |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4437 | } |
| 4438 | } |
| 4439 | |
| 4440 | switch (match) { |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4441 | case AUDIO_GENRES_ID_MEMBERS: |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 4442 | throw new FallbackException("Genres are read-only", Build.VERSION_CODES.R); |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4443 | |
| 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 Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 4449 | Cursor c = qb.query(helper, sDataOnlyColumn, userWhere, userWhereArgs, null, |
| 4450 | null, null, null, null); |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4451 | if (c != null) { |
| 4452 | try { |
| 4453 | while (c.moveToNext()) { |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4454 | deleteIfAllowed(uri, extras, c.getString(0)); |
Marco Nelissen | a620728 | 2012-02-03 16:54:57 -0800 | [diff] [blame] | 4455 | } |
Mattias Nilsson | a79fcf1 | 2014-03-26 17:18:35 +0100 | [diff] [blame] | 4456 | } finally { |
Jeff Sharkey | b6781bc | 2019-07-18 18:45:52 -0600 | [diff] [blame] | 4457 | FileUtils.closeQuietly(c); |
Marco Nelissen | a620728 | 2012-02-03 16:54:57 -0800 | [diff] [blame] | 4458 | } |
| 4459 | } |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 4460 | count += deleteRecursive(qb, helper, userWhere, userWhereArgs); |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4461 | break; |
Marco Nelissen | a620728 | 2012-02-03 16:54:57 -0800 | [diff] [blame] | 4462 | |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4463 | default: |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 4464 | count += deleteRecursive(qb, helper, userWhere, userWhereArgs); |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4465 | break; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 4466 | } |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 4467 | |
Sudheer Shanka | 1a79ac0 | 2019-01-17 13:14:52 -0800 | [diff] [blame] | 4468 | if (deletedDownloadIds.size() > 0) { |
Martijn Coenen | 24b9935 | 2020-06-18 14:12:35 +0200 | [diff] [blame] | 4469 | // 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 Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 4472 | getContext().getSystemService(DownloadManager.class) |
| 4473 | .onMediaStoreDownloadsDeleted(deletedDownloadIds); |
Martijn Coenen | 24b9935 | 2020-06-18 14:12:35 +0200 | [diff] [blame] | 4474 | }); |
Sudheer Shanka | 1a79ac0 | 2019-01-17 13:14:52 -0800 | [diff] [blame] | 4475 | } |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 4476 | |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 4477 | if (isFilesTable && !isCallingPackageSelf()) { |
shafik | ac069d3 | 2020-04-02 17:18:04 +0100 | [diff] [blame] | 4478 | Metrics.logDeletion(volumeName, mCallingIdentity.get().uid, |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 4479 | getCallingPackageOrSelf(), count); |
| 4480 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 4481 | } |
| 4482 | |
| 4483 | return count; |
| 4484 | } |
| 4485 | |
Jeff Sharkey | 556d2d9 | 2018-07-12 19:51:27 -0600 | [diff] [blame] | 4486 | /** |
| 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 Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 4492 | private int deleteRecursive(SQLiteQueryBuilder qb, DatabaseHelper helper, String userWhere, |
Jeff Sharkey | 556d2d9 | 2018-07-12 19:51:27 -0600 | [diff] [blame] | 4493 | String[] userWhereArgs) { |
Jeff Sharkey | ebe24b5 | 2020-04-21 14:41:44 -0600 | [diff] [blame] | 4494 | return (int) helper.runWithTransaction((db) -> { |
| 4495 | synchronized (mDirectoryCache) { |
| 4496 | mDirectoryCache.clear(); |
| 4497 | } |
Jeff Sharkey | c579312 | 2019-08-19 15:58:35 -0600 | [diff] [blame] | 4498 | |
Jeff Sharkey | ebe24b5 | 2020-04-21 14:41:44 -0600 | [diff] [blame] | 4499 | 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 Sharkey | 556d2d9 | 2018-07-12 19:51:27 -0600 | [diff] [blame] | 4507 | } |
| 4508 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 4509 | @Override |
Marco Nelissen | 38b4364 | 2012-01-27 09:40:07 -0800 | [diff] [blame] | 4510 | public Bundle call(String method, String arg, Bundle extras) { |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 4511 | 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 Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4520 | switch (method) { |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 4521 | 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 Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 4533 | 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 Sharkey | 4209a84 | 2019-06-12 11:01:07 -0600 | [diff] [blame] | 4548 | case MediaStore.WAIT_FOR_IDLE_CALL: { |
Jeff Sharkey | e04e2c6 | 2020-03-05 10:53:33 -0700 | [diff] [blame] | 4549 | ForegroundThread.waitForIdle(); |
| 4550 | BackgroundThread.waitForIdle(); |
Jeff Sharkey | 4209a84 | 2019-06-12 11:01:07 -0600 | [diff] [blame] | 4551 | return null; |
| 4552 | } |
Jeff Sharkey | 54e874e | 2019-01-25 17:22:23 -0700 | [diff] [blame] | 4553 | case MediaStore.SCAN_FILE_CALL: |
| 4554 | case MediaStore.SCAN_VOLUME_CALL: { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 4555 | final LocalCallingIdentity token = clearLocalCallingIdentity(); |
Jeff Sharkey | ce59a54 | 2019-06-11 13:06:13 -0600 | [diff] [blame] | 4556 | final CallingIdentity providerToken = clearCallingIdentity(); |
Jeff Sharkey | 54e874e | 2019-01-25 17:22:23 -0700 | [diff] [blame] | 4557 | try { |
Jeff Sharkey | 54e874e | 2019-01-25 17:22:23 -0700 | [diff] [blame] | 4558 | final Bundle res = new Bundle(); |
| 4559 | switch (method) { |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 4560 | case MediaStore.SCAN_FILE_CALL: { |
| 4561 | final File file = new File(arg); |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 4562 | res.putParcelable(Intent.EXTRA_STREAM, scanFile(file, REASON_DEMAND)); |
Jeff Sharkey | 54e874e | 2019-01-25 17:22:23 -0700 | [diff] [blame] | 4563 | break; |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 4564 | } |
| 4565 | case MediaStore.SCAN_VOLUME_CALL: { |
| 4566 | final String volumeName = arg; |
| 4567 | MediaService.onScanVolume(getContext(), volumeName, REASON_DEMAND); |
Jeff Sharkey | 54e874e | 2019-01-25 17:22:23 -0700 | [diff] [blame] | 4568 | break; |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 4569 | } |
Jeff Sharkey | 54e874e | 2019-01-25 17:22:23 -0700 | [diff] [blame] | 4570 | } |
| 4571 | return res; |
| 4572 | } catch (IOException e) { |
| 4573 | throw new RuntimeException(e); |
| 4574 | } finally { |
Jeff Sharkey | ce59a54 | 2019-06-11 13:06:13 -0600 | [diff] [blame] | 4575 | restoreCallingIdentity(providerToken); |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 4576 | restoreLocalCallingIdentity(token); |
Jeff Sharkey | 54e874e | 2019-01-25 17:22:23 -0700 | [diff] [blame] | 4577 | } |
| 4578 | } |
Jeff Sharkey | 44ef440 | 2019-03-01 11:31:10 -0700 | [diff] [blame] | 4579 | case MediaStore.GET_VERSION_CALL: { |
| 4580 | final String volumeName = extras.getString(Intent.EXTRA_TEXT); |
| 4581 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 4582 | final DatabaseHelper helper; |
Jeff Sharkey | 44ef440 | 2019-03-01 11:31:10 -0700 | [diff] [blame] | 4583 | try { |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 4584 | helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName)); |
Jeff Sharkey | 44ef440 | 2019-03-01 11:31:10 -0700 | [diff] [blame] | 4585 | } catch (VolumeNotFoundException e) { |
| 4586 | throw e.rethrowAsIllegalArgumentException(); |
| 4587 | } |
| 4588 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 4589 | final String version = helper.runWithoutTransaction((db) -> { |
| 4590 | return db.getVersion() + ":" + DatabaseHelper.getOrCreateUuid(db); |
| 4591 | }); |
Jeff Sharkey | 44ef440 | 2019-03-01 11:31:10 -0700 | [diff] [blame] | 4592 | |
| 4593 | final Bundle res = new Bundle(); |
| 4594 | res.putString(Intent.EXTRA_TEXT, version); |
| 4595 | return res; |
| 4596 | } |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 4597 | case MediaStore.GET_GENERATION_CALL: { |
| 4598 | final String volumeName = extras.getString(Intent.EXTRA_TEXT); |
| 4599 | |
Jeff Sharkey | b2dbc6c | 2020-03-31 19:35:28 -0600 | [diff] [blame] | 4600 | final DatabaseHelper helper; |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 4601 | try { |
Jeff Sharkey | b2dbc6c | 2020-03-31 19:35:28 -0600 | [diff] [blame] | 4602 | helper = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName)); |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 4603 | } catch (VolumeNotFoundException e) { |
| 4604 | throw e.rethrowAsIllegalArgumentException(); |
| 4605 | } |
| 4606 | |
Jeff Sharkey | b2dbc6c | 2020-03-31 19:35:28 -0600 | [diff] [blame] | 4607 | final long generation = helper.runWithoutTransaction((db) -> { |
| 4608 | return DatabaseHelper.getGeneration(db); |
| 4609 | }); |
| 4610 | |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 4611 | final Bundle res = new Bundle(); |
| 4612 | res.putLong(Intent.EXTRA_INDEX, generation); |
| 4613 | return res; |
| 4614 | } |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4615 | case MediaStore.GET_DOCUMENT_URI_CALL: { |
Jeff Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 4616 | final Uri mediaUri = extras.getParcelable(MediaStore.EXTRA_URI); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4617 | enforceCallingPermission(mediaUri, extras, false); |
Marco Nelissen | 38b4364 | 2012-01-27 09:40:07 -0800 | [diff] [blame] | 4618 | |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4619 | final Uri fileUri; |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 4620 | final LocalCallingIdentity token = clearLocalCallingIdentity(); |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4621 | try { |
| 4622 | fileUri = Uri.fromFile(queryForDataFile(mediaUri, null)); |
| 4623 | } catch (FileNotFoundException e) { |
| 4624 | throw new IllegalArgumentException(e); |
| 4625 | } finally { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 4626 | restoreLocalCallingIdentity(token); |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4627 | } |
| 4628 | |
| 4629 | try (ContentProviderClient client = getContext().getContentResolver() |
| 4630 | .acquireUnstableContentProviderClient( |
Jeff Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 4631 | MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) { |
| 4632 | extras.putParcelable(MediaStore.EXTRA_URI, fileUri); |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4633 | 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 Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 4639 | final Uri documentUri = extras.getParcelable(MediaStore.EXTRA_URI); |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4640 | 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 Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 4646 | MediaStore.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) { |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4647 | final Bundle res = client.call(method, null, extras); |
Jeff Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 4648 | fileUri = res.getParcelable(MediaStore.EXTRA_URI); |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4649 | } catch (RemoteException e) { |
| 4650 | throw new IllegalStateException(e); |
| 4651 | } |
| 4652 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 4653 | final LocalCallingIdentity token = clearLocalCallingIdentity(); |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4654 | try { |
| 4655 | final Bundle res = new Bundle(); |
Jeff Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 4656 | res.putParcelable(MediaStore.EXTRA_URI, |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4657 | queryForMediaUri(new File(fileUri.getPath()), null)); |
| 4658 | return res; |
| 4659 | } catch (FileNotFoundException e) { |
| 4660 | throw new IllegalArgumentException(e); |
| 4661 | } finally { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 4662 | restoreLocalCallingIdentity(token); |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4663 | } |
| 4664 | } |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 4665 | 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 Sharkey | 56c34e8 | 2019-11-21 15:56:37 -0700 | [diff] [blame] | 4673 | } |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 4674 | default: |
| 4675 | throw new UnsupportedOperationException("Unsupported call: " + method); |
| 4676 | } |
| 4677 | } |
Marco Nelissen | 8a1db2e | 2017-11-30 12:46:54 -0800 | [diff] [blame] | 4678 | |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 4679 | 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 Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 4696 | for (Uri uri : uris) { |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 4697 | 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 Sharkey | 05c3a03 | 2020-04-09 16:57:04 -0600 | [diff] [blame] | 4721 | MediaColumns.IS_TRASHED); |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 4722 | 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 Sharkey | e987615 | 2018-12-08 11:14:13 -0700 | [diff] [blame] | 4742 | /** |
Jeff Sharkey | 6688130 | 2019-10-05 10:50:06 -0600 | [diff] [blame] | 4743 | * 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 Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 4760 | helper.runWithoutTransaction((db) -> { |
| 4761 | db.execPerConnectionSQL("SELECT icu_load_collation(?, ?);", |
| 4762 | new String[] { locale, collationName }); |
| 4763 | return null; |
| 4764 | }); |
Jeff Sharkey | 6688130 | 2019-10-05 10:50:06 -0600 | [diff] [blame] | 4765 | } |
| 4766 | mCustomCollators.add(collationName); |
| 4767 | } |
| 4768 | } |
| 4769 | return collationName; |
| 4770 | } |
| 4771 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 4772 | private int pruneThumbnails(@NonNull SQLiteDatabase db, @NonNull CancellationSignal signal) { |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 4773 | int prunedCount = 0; |
| 4774 | |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 4775 | // 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 Nelissen | 8a1db2e | 2017-11-30 12:46:54 -0800 | [diff] [blame] | 4783 | |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 4784 | final long[] knownIdsRaw = knownIds.toArray(); |
| 4785 | Arrays.sort(knownIdsRaw); |
| 4786 | |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 4787 | for (String volumeName : getExternalVolumeNames()) { |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 4788 | final List<File> thumbDirs; |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 4789 | try { |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 4790 | thumbDirs = getThumbnailDirectories(volumeName); |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 4791 | } catch (FileNotFoundException e) { |
| 4792 | Log.w(TAG, "Failed to resolve volume " + volumeName, e); |
| 4793 | continue; |
Marco Nelissen | 8a1db2e | 2017-11-30 12:46:54 -0800 | [diff] [blame] | 4794 | } |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 4795 | |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 4796 | // Reconcile all thumbnails, deleting stale items |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 4797 | for (File thumbDir : thumbDirs) { |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 4798 | // Possibly bail before digging into each directory |
| 4799 | signal.throwIfCanceled(); |
| 4800 | |
Jeff Sharkey | 7ea24f2 | 2019-08-22 10:14:18 -0600 | [diff] [blame] | 4801 | final File[] files = thumbDir.listFiles(); |
| 4802 | for (File thumbFile : (files != null) ? files : new File[0]) { |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 4803 | if (Objects.equals(thumbFile.getName(), FILE_DATABASE_UUID)) continue; |
Jeff Sharkey | e76c426 | 2019-12-06 14:46:00 -0700 | [diff] [blame] | 4804 | final String name = FileUtils.extractFileName(thumbFile.getName()); |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 4805 | 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 Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 4815 | deleteAndInvalidate(thumbFile); |
Jeff Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 4816 | prunedCount++; |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 4817 | } |
| 4818 | } |
Marco Nelissen | 8a1db2e | 2017-11-30 12:46:54 -0800 | [diff] [blame] | 4819 | } |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 4820 | |
| 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 Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 4826 | |
| 4827 | return prunedCount; |
Marco Nelissen | 8a1db2e | 2017-11-30 12:46:54 -0800 | [diff] [blame] | 4828 | } |
| 4829 | |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 4830 | abstract class Thumbnailer { |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4831 | final String directoryName; |
| 4832 | |
| 4833 | public Thumbnailer(String directoryName) { |
| 4834 | this.directoryName = directoryName; |
| 4835 | } |
| 4836 | |
| 4837 | private File getThumbnailFile(Uri uri) throws IOException { |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 4838 | final String volumeName = resolveVolumeName(uri); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 4839 | final File volumePath = getVolumePath(volumeName); |
Jeff Sharkey | 7ea24f2 | 2019-08-22 10:14:18 -0600 | [diff] [blame] | 4840 | return FileUtils.buildPath(volumePath, directoryName, |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 4841 | DIRECTORY_THUMBNAILS, ContentUris.parseId(uri) + ".jpg"); |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4842 | } |
| 4843 | |
| 4844 | public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) |
| 4845 | throws IOException; |
| 4846 | |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 4847 | 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 Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4851 | final File thumbFile = getThumbnailFile(uri); |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 4852 | try { |
Jeff Sharkey | 9a49764 | 2020-04-23 13:15:10 -0600 | [diff] [blame] | 4853 | return FileUtils.openSafely(thumbFile, |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 4854 | ParcelFileDescriptor.MODE_READ_ONLY); |
| 4855 | } catch (FileNotFoundException ignored) { |
| 4856 | } |
| 4857 | |
Jeff Sharkey | f12923c | 2020-01-19 12:12:14 -0700 | [diff] [blame] | 4858 | final File thumbDir = thumbFile.getParentFile(); |
| 4859 | thumbDir.mkdirs(); |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 4860 | |
| 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 Sharkey | 9a49764 | 2020-04-23 13:15:10 -0600 | [diff] [blame] | 4876 | thumbWrite = FileUtils.openSafely(thumbTempFile, |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 4877 | ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE); |
Jeff Sharkey | 9a49764 | 2020-04-23 13:15:10 -0600 | [diff] [blame] | 4878 | thumbRead = FileUtils.openSafely(thumbTempFile, |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 4879 | ParcelFileDescriptor.MODE_READ_ONLY); |
| 4880 | |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4881 | final Bitmap thumbnail = getThumbnailBitmap(uri, signal); |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 4882 | 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 Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4890 | } |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 4891 | |
| 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 Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 4901 | deleteAndInvalidate(thumbTempFile); |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4902 | } |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4903 | } |
| 4904 | |
| 4905 | public void invalidateThumbnail(Uri uri) throws IOException { |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 4906 | deleteAndInvalidate(getThumbnailFile(uri)); |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4907 | } |
| 4908 | } |
| 4909 | |
Jeff Sharkey | beeca1e | 2019-01-24 15:57:08 -0700 | [diff] [blame] | 4910 | 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 Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4918 | 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 Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 4934 | 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 Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4942 | private void invalidateThumbnails(Uri uri) { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 4943 | Trace.beginSection("invalidateThumbnails"); |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 4944 | try { |
| 4945 | invalidateThumbnailsInternal(uri); |
| 4946 | } finally { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 4947 | Trace.endSection(); |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 4948 | } |
| 4949 | } |
| 4950 | |
| 4951 | private void invalidateThumbnailsInternal(Uri uri) { |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4952 | final long id = ContentUris.parseId(uri); |
| 4953 | try { |
Jeff Sharkey | beeca1e | 2019-01-24 15:57:08 -0700 | [diff] [blame] | 4954 | mAudioThumbnailer.invalidateThumbnail(uri); |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 4955 | mVideoThumbnailer.invalidateThumbnail(uri); |
| 4956 | mImageThumbnailer.invalidateThumbnail(uri); |
| 4957 | } catch (IOException ignored) { |
| 4958 | } |
| 4959 | |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 4960 | final DatabaseHelper helper; |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 4961 | try { |
| 4962 | helper = getDatabaseForUri(uri); |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 4963 | } catch (VolumeNotFoundException e) { |
| 4964 | Log.w(TAG, e); |
| 4965 | return; |
| 4966 | } |
| 4967 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 4968 | 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 Nelissen | 8a1db2e | 2017-11-30 12:46:54 -0800 | [diff] [blame] | 4977 | } |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 4978 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 4979 | 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 Nelissen | 8a1db2e | 2017-11-30 12:46:54 -0800 | [diff] [blame] | 4983 | } |
| 4984 | |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4985 | /** |
| 4986 | * @deprecated all operations should be routed through the overload that |
| 4987 | * accepts a {@link Bundle} of extras. |
| 4988 | */ |
Marco Nelissen | 38b4364 | 2012-01-27 09:40:07 -0800 | [diff] [blame] | 4989 | @Override |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4990 | @Deprecated |
| 4991 | public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { |
Jeff Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 4992 | return update(uri, values, |
| 4993 | DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null)); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 4994 | } |
| 4995 | |
| 4996 | @Override |
| 4997 | public int update(@NonNull Uri uri, @Nullable ContentValues values, |
| 4998 | @Nullable Bundle extras) { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 4999 | Trace.beginSection("update"); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 5000 | try { |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 5001 | return updateInternal(uri, values, extras); |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 5002 | } catch (FallbackException e) { |
| 5003 | return e.translateForUpdateDelete(getCallingPackageTargetSdkVersion()); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 5004 | } finally { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 5005 | Trace.endSection(); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 5006 | } |
| 5007 | } |
| 5008 | |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 5009 | private int updateInternal(@NonNull Uri uri, @Nullable ContentValues initialValues, |
| 5010 | @Nullable Bundle extras) throws FallbackException { |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5011 | extras = (extras != null) ? extras : new Bundle(); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 5012 | |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 5013 | // 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 Ioffe | 710787b | 2020-06-11 14:35:14 +0100 | [diff] [blame] | 5016 | // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider. |
| 5017 | extras.remove(INCLUDED_DEFAULT_DIRECTORIES); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 5018 | |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 5019 | final String userWhere = extras.getString(QUERY_ARG_SQL_SELECTION); |
| 5020 | final String[] userWhereArgs = extras.getStringArray(QUERY_ARG_SQL_SELECTION_ARGS); |
| 5021 | |
Nandana Dutt | 3a93194 | 2020-03-20 10:32:24 +0000 | [diff] [blame] | 5022 | // 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 Sharkey | d9767f2 | 2018-08-02 11:50:02 -0600 | [diff] [blame] | 5026 | 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 Sharkey | 3396601 | 2018-08-06 10:17:58 -0600 | [diff] [blame] | 5029 | } 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 Sharkey | d9767f2 | 2018-08-02 11:50:02 -0600 | [diff] [blame] | 5032 | } |
| 5033 | } |
| 5034 | |
Marco Nelissen | 01e706a | 2013-09-12 15:38:42 -0700 | [diff] [blame] | 5035 | uri = safeUncanonicalize(uri); |
Jeff Sharkey | c08c37d | 2019-02-28 16:39:19 -0700 | [diff] [blame] | 5036 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 5037 | int count; |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 5038 | |
Jeff Sharkey | d9628b1 | 2018-09-28 11:13:27 -0600 | [diff] [blame] | 5039 | final String volumeName = getVolumeName(uri); |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 5040 | final int targetSdkVersion = getCallingPackageTargetSdkVersion(); |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 5041 | final boolean allowHidden = isCallingPackageAllowedHidden(); |
| 5042 | final int match = matchUri(uri, allowHidden); |
| 5043 | |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5044 | 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 Sharkey | a9473e9 | 2020-04-17 15:54:30 -0600 | [diff] [blame] | 5054 | if (uri.getBooleanQueryParameter("move", false)) { |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5055 | // 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 Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 5089 | final DatabaseHelper helper = getDatabaseForUri(uri); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 5090 | final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, match, uri, extras, null); |
Jeff Sharkey | b6485ce | 2019-04-15 14:20:47 -0600 | [diff] [blame] | 5091 | |
| 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 Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 5098 | enforceCallingPermission(uri, extras, true); |
Jeff Sharkey | b6485ce | 2019-04-15 14:20:47 -0600 | [diff] [blame] | 5099 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 5100 | |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 5101 | boolean triggerInvalidate = false; |
Jeff Sharkey | 291e777 | 2019-01-03 14:16:33 -0700 | [diff] [blame] | 5102 | boolean triggerScan = false; |
Mike Lockwood | b8f9b76 | 2011-07-31 17:51:07 -0400 | [diff] [blame] | 5103 | if (initialValues != null) { |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 5104 | // IDs are forever; nobody should be editing them |
| 5105 | initialValues.remove(MediaColumns._ID); |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 5106 | |
Jeff Sharkey | 05c3a03 | 2020-04-09 16:57:04 -0600 | [diff] [blame] | 5107 | // Expiration times are hard-coded; let's derive them |
| 5108 | FileUtils.computeDateExpires(initialValues); |
| 5109 | |
Jeff Sharkey | b0cab58 | 2019-04-16 12:42:42 -0600 | [diff] [blame] | 5110 | // Ignore or augment incoming raw filesystem paths |
Jeff Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 5111 | for (String column : sDataColumns.keySet()) { |
| 5112 | if (!initialValues.containsKey(column)) continue; |
Jeff Sharkey | b0cab58 | 2019-04-16 12:42:42 -0600 | [diff] [blame] | 5113 | |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 5114 | if (isCallingPackageSelf() || isCallingPackageLegacyWrite()) { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 5115 | // Mutation allowed |
Jeff Sharkey | b0cab58 | 2019-04-16 12:42:42 -0600 | [diff] [blame] | 5116 | } else { |
| 5117 | Log.w(TAG, "Ignoring mutation of " + column + " from " |
| 5118 | + getCallingPackageOrSelf()); |
| 5119 | initialValues.remove(column); |
| 5120 | } |
Jeff Sharkey | 16fb805 | 2018-10-18 15:22:53 -0600 | [diff] [blame] | 5121 | } |
| 5122 | |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 5123 | // 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 Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 5158 | |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 5159 | if (!isCallingPackageSelf()) { |
| 5160 | Trace.beginSection("filter"); |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 5161 | |
Jeff Sharkey | d06f214 | 2019-04-29 12:30:17 -0600 | [diff] [blame] | 5162 | // 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 Sharkey | 95ee896 | 2019-05-20 09:45:40 -0600 | [diff] [blame] | 5165 | final Uri finalUri = uri; |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 5166 | final Supplier<Boolean> isPending = new CachedSupplier<>(() -> { |
Jeff Sharkey | 95ee896 | 2019-05-20 09:45:40 -0600 | [diff] [blame] | 5167 | return isPending(finalUri); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 5168 | }); |
Jeff Sharkey | d06f214 | 2019-04-29 12:30:17 -0600 | [diff] [blame] | 5169 | |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 5170 | // 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 Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 5174 | if (sMutableColumns.contains(column)) { |
| 5175 | // Mutation normally allowed |
| 5176 | } else if (isPending.get()) { |
| 5177 | // Mutation relaxed while pending |
| 5178 | } else { |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 5179 | Log.w(TAG, "Ignoring mutation of " + column + " from " |
| 5180 | + getCallingPackageOrSelf()); |
| 5181 | initialValues.remove(column); |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5182 | triggerScan = true; |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 5183 | } |
Jeff Sharkey | b3bd14f | 2019-04-13 19:29:27 -0600 | [diff] [blame] | 5184 | |
| 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 Sharkey | e1a7420 | 2020-04-29 20:24:58 -0600 | [diff] [blame] | 5189 | |
| 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 Sharkey | b3bd14f | 2019-04-13 19:29:27 -0600 | [diff] [blame] | 5194 | } |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 5195 | } |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 5196 | |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 5197 | Trace.endSection(); |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 5198 | } |
| 5199 | |
Sudheer Shanka | 56cba32 | 2018-12-07 10:55:58 -0800 | [diff] [blame] | 5200 | if ("files".equals(qb.getTables())) { |
| 5201 | maybeMarkAsDownload(initialValues); |
| 5202 | } |
Jeff Sharkey | d2a3822 | 2018-12-04 11:23:48 -0700 | [diff] [blame] | 5203 | |
| 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 Lockwood | b8f9b76 | 2011-07-31 17:51:07 -0400 | [diff] [blame] | 5211 | } |
| 5212 | |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 5213 | // If we're not updating anything, then we can skip |
| 5214 | if (initialValues.isEmpty()) return 0; |
| 5215 | |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5216 | 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 Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5231 | 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 Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 5236 | if (!isCallingPackageSelf()) { |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5237 | 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 Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5248 | // If we're touching columns that would change placement of a file, |
| 5249 | // blend in current values and recalculate path |
Jeff Sharkey | fd7d0a3 | 2020-04-17 15:21:52 -0600 | [diff] [blame] | 5250 | final boolean allowMovement = extras.getBoolean(MediaStore.QUERY_ARG_ALLOW_MOVEMENT, |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 5251 | !isCallingPackageSelf()); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5252 | if (containsAny(initialValues.keySet(), sPlacementColumns) |
| 5253 | && !initialValues.containsKey(MediaColumns.DATA) |
Jeff Sharkey | fd7d0a3 | 2020-04-17 15:21:52 -0600 | [diff] [blame] | 5254 | && !isThumbnail |
| 5255 | && allowMovement) { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 5256 | Trace.beginSection("movement"); |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 5257 | |
Jeff Sharkey | 0ab564c | 2019-04-28 11:55:21 -0600 | [diff] [blame] | 5258 | // We only support movement under well-defined collections |
| 5259 | switch (match) { |
| 5260 | case AUDIO_MEDIA_ID: |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5261 | case AUDIO_PLAYLISTS_ID: |
Jeff Sharkey | 0ab564c | 2019-04-28 11:55:21 -0600 | [diff] [blame] | 5262 | case VIDEO_MEDIA_ID: |
| 5263 | case IMAGES_MEDIA_ID: |
| 5264 | case DOWNLOADS_ID: |
Jeff Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 5265 | case FILES_ID: |
Jeff Sharkey | 0ab564c | 2019-04-28 11:55:21 -0600 | [diff] [blame] | 5266 | break; |
| 5267 | default: |
| 5268 | throw new IllegalArgumentException("Movement of " + uri |
| 5269 | + " which isn't part of well-defined collection not allowed"); |
| 5270 | } |
| 5271 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 5272 | final LocalCallingIdentity token = clearLocalCallingIdentity(); |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 5273 | final Uri genericUri = MediaStore.Files.getContentUri(volumeName, |
| 5274 | ContentUris.parseId(uri)); |
| 5275 | try (Cursor c = queryForSingleItem(genericUri, |
Jeff Sharkey | ad8ef4e | 2019-08-20 09:43:48 -0600 | [diff] [blame] | 5276 | sPlacementColumns.toArray(new String[0]), userWhere, userWhereArgs, null)) { |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5277 | 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 Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 5286 | restoreLocalCallingIdentity(token); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5287 | } |
| 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 Sharkey | ab91923 | 2019-04-15 10:39:35 -0600 | [diff] [blame] | 5292 | final String beforeVolume = extractVolumeName(beforePath); |
| 5293 | final String beforeOwner = extractPathOwnerPackageName(beforePath); |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 5294 | |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5295 | initialValues.remove(MediaColumns.DATA); |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 5296 | ensureNonUniqueFileColumns(match, uri, extras, initialValues, beforePath); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5297 | |
| 5298 | final String probePath = initialValues.getAsString(MediaColumns.DATA); |
Jeff Sharkey | ab91923 | 2019-04-15 10:39:35 -0600 | [diff] [blame] | 5299 | final String probeVolume = extractVolumeName(probePath); |
| 5300 | final String probeOwner = extractPathOwnerPackageName(probePath); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5301 | if (Objects.equals(beforePath, probePath)) { |
| 5302 | Log.d(TAG, "Identical paths " + beforePath + "; not moving"); |
Jeff Sharkey | ab91923 | 2019-04-15 10:39:35 -0600 | [diff] [blame] | 5303 | } 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 Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5309 | } 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 Sharkey | ab27f02 | 2020-04-29 20:58:55 -0600 | [diff] [blame] | 5313 | ensureUniqueFileColumns(match, uri, extras, initialValues, beforePath); |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 5314 | |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5315 | final String afterPath = initialValues.getAsString(MediaColumns.DATA); |
| 5316 | |
| 5317 | Log.d(TAG, "Moving " + beforePath + " to " + afterPath); |
| 5318 | try { |
| 5319 | Os.rename(beforePath, afterPath); |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 5320 | invalidateFuseDentry(beforePath); |
| 5321 | invalidateFuseDentry(afterPath); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5322 | } catch (ErrnoException e) { |
Jeff Sharkey | a2bdf54 | 2020-06-15 17:44:27 -0600 | [diff] [blame] | 5323 | if (e.errno == OsConstants.ENOENT) { |
| 5324 | Log.d(TAG, "Missing file at " + beforePath + "; continuing anyway"); |
| 5325 | } else { |
| 5326 | throw new IllegalStateException(e); |
| 5327 | } |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5328 | } |
| 5329 | initialValues.put(MediaColumns.DATA, afterPath); |
Jeff Sharkey | 456ca0f | 2020-01-06 14:02:09 -0700 | [diff] [blame] | 5330 | |
| 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 Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5334 | } |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 5335 | |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 5336 | Trace.endSection(); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 5337 | } |
| 5338 | |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 5339 | assertPrivatePathNotInValues(initialValues); |
| 5340 | |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 5341 | // Make sure any updated paths look sane |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 5342 | assertFileColumnsSane(match, uri, initialValues); |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 5343 | |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 5344 | if (initialValues.containsKey(FileColumns.DATA)) { |
| 5345 | // If we're changing paths, invalidate any thumbnails |
| 5346 | triggerInvalidate = true; |
Jeff Sharkey | 7362fe2 | 2020-04-13 17:39:28 -0600 | [diff] [blame] | 5347 | |
| 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 Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 5358 | if (isCallingPackageSelf()) { |
Jeff Sharkey | 7362fe2 | 2020-04-13 17:39:28 -0600 | [diff] [blame] | 5359 | triggerScan = false; |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 5360 | } |
| 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 Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 5366 | Trace.beginSection("snapshot"); |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 5367 | final LocalCallingIdentity token = clearLocalCallingIdentity(); |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 5368 | try (Cursor c = qb.query(helper, new String[] { FileColumns._ID }, |
| 5369 | userWhere, userWhereArgs, null, null, null, null, null)) { |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 5370 | while (c.moveToNext()) { |
| 5371 | updatedIds.add(c.getLong(0)); |
Marco Nelissen | 8a1db2e | 2017-11-30 12:46:54 -0800 | [diff] [blame] | 5372 | } |
| 5373 | } finally { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 5374 | restoreLocalCallingIdentity(token); |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 5375 | Trace.endSection(); |
Marco Nelissen | 8a1db2e | 2017-11-30 12:46:54 -0800 | [diff] [blame] | 5376 | } |
| 5377 | } |
| 5378 | |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 5379 | final ContentValues values = new ContentValues(initialValues); |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 5380 | switch (match) { |
Jeff Sharkey | cd38964 | 2020-04-21 15:03:09 -0600 | [diff] [blame] | 5381 | case AUDIO_MEDIA_ID: |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 5382 | case AUDIO_PLAYLISTS_ID: |
| 5383 | case VIDEO_MEDIA_ID: |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 5384 | case IMAGES_MEDIA_ID: |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 5385 | case FILES_ID: |
| 5386 | case DOWNLOADS_ID: { |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 5387 | FileUtils.computeValuesFromData(values, isFuseThread()); |
Marco Nelissen | bae4061 | 2017-06-22 12:53:21 -0700 | [diff] [blame] | 5388 | break; |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 5389 | } |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 5390 | } |
| 5391 | |
Jeff Sharkey | cd38964 | 2020-04-21 15:03:09 -0600 | [diff] [blame] | 5392 | 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 Rao | 34682a7 | 2020-05-04 02:02:37 +0100 | [diff] [blame] | 5403 | count = updateAllowingReplace(qb, helper, values, userWhere, userWhereArgs); |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5404 | |
Jeff Sharkey | 291e777 | 2019-01-03 14:16:33 -0700 | [diff] [blame] | 5405 | // 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 Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 5407 | if (triggerInvalidate || triggerScan) { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 5408 | Trace.beginSection("invalidate"); |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 5409 | final LocalCallingIdentity token = clearLocalCallingIdentity(); |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 5410 | 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 Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 5414 | helper.postBackground(() -> { |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 5415 | invalidateThumbnails(updatedUri); |
| 5416 | }); |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 5417 | |
| 5418 | if (triggerScan) { |
| 5419 | try (Cursor c = queryForSingleItem(updatedUri, |
| 5420 | new String[] { FileColumns.DATA }, null, null, null)) { |
Jeff Sharkey | c59bca9 | 2020-06-15 19:07:59 -0600 | [diff] [blame] | 5421 | 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 Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 5430 | } catch (Exception e) { |
| 5431 | Log.w(TAG, "Failed to update metadata for " + updatedUri, e); |
| 5432 | } |
| 5433 | } |
| 5434 | } |
Jeff Sharkey | 291e777 | 2019-01-03 14:16:33 -0700 | [diff] [blame] | 5435 | } finally { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 5436 | restoreLocalCallingIdentity(token); |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 5437 | Trace.endSection(); |
Jeff Sharkey | 291e777 | 2019-01-03 14:16:33 -0700 | [diff] [blame] | 5438 | } |
| 5439 | } |
| 5440 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 5441 | return count; |
| 5442 | } |
| 5443 | |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5444 | /** |
Sahana Rao | 34682a7 | 2020-05-04 02:02:37 +0100 | [diff] [blame] | 5445 | * 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 | |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 5473 | final Uri uri = FileUtils.getContentUriForPath(path); |
Sahana Rao | 34682a7 | 2020-05-04 02:02:37 +0100 | [diff] [blame] | 5474 | 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 Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 5477 | Bundle extras = new Bundle(); |
| 5478 | extras.putInt(QUERY_ARG_MATCH_PENDING, MATCH_INCLUDE); |
| 5479 | extras.putInt(QUERY_ARG_MATCH_TRASHED, MATCH_INCLUDE); |
Sahana Rao | 34682a7 | 2020-05-04 02:02:37 +0100 | [diff] [blame] | 5480 | final SQLiteQueryBuilder qbForReplace = getQueryBuilder(TYPE_DELETE, |
Sahana Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 5481 | matchUri(uri, allowHidden), uri, extras, null); |
Sahana Rao | 25db649 | 2020-06-06 16:22:23 +0100 | [diff] [blame] | 5482 | final long rowId = getIdIfPathOwnedByPackages(qbForReplace, helper, path, |
Sahana Rao | 0bbd3e1 | 2020-06-06 15:56:44 +0100 | [diff] [blame] | 5483 | getSharedPackages()); |
Sahana Rao | 34682a7 | 2020-05-04 02:02:37 +0100 | [diff] [blame] | 5484 | |
| 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 Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5497 | * 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 Nelissen | f5f9eca | 2009-12-09 09:26:15 -0800 | [diff] [blame] | 5507 | try { |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 5508 | 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 Nelissen | f5f9eca | 2009-12-09 09:26:15 -0800 | [diff] [blame] | 5519 | } finally { |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5520 | Trace.endSection(); |
Marco Nelissen | f5f9eca | 2009-12-09 09:26:15 -0800 | [diff] [blame] | 5521 | } |
| 5522 | } |
| 5523 | |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 5524 | private void resolvePlaylistMembersInternal(@NonNull Uri playlistUri, |
| 5525 | @NonNull SQLiteDatabase db) { |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5526 | try { |
| 5527 | // Refresh playlist members based on what we parse from disk |
Jeff Sharkey | c22cfcf | 2020-04-02 17:41:13 -0600 | [diff] [blame] | 5528 | final String volumeName = getVolumeName(playlistUri); |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5529 | 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 Sharkey | c22cfcf | 2020-04-02 17:41:13 -0600 | [diff] [blame] | 5538 | 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 Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5549 | } |
| 5550 | } |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5551 | } catch (IOException e) { |
| 5552 | Log.w(TAG, "Failed to refresh playlist", e); |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5553 | } |
| 5554 | } |
| 5555 | |
| 5556 | /** |
Jeff Sharkey | c22cfcf | 2020-04-02 17:41:13 -0600 | [diff] [blame] | 5557 | * 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 Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 5581 | * 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 | } |
Winson | b653af2 | 2019-06-05 12:14:13 -0700 | [diff] [blame] | 5701 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 5702 | |
| 5703 | @Override |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5704 | public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { |
| 5705 | return openFileCommon(uri, mode, null); |
| 5706 | } |
Marco Nelissen | 71ece60 | 2009-06-16 12:45:10 -0700 | [diff] [blame] | 5707 | |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5708 | @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 Nelissen | 01e706a | 2013-09-12 15:38:42 -0700 | [diff] [blame] | 5716 | uri = safeUncanonicalize(uri); |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5717 | |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 5718 | final boolean allowHidden = isCallingPackageAllowedHidden(); |
| 5719 | final int match = matchUri(uri, allowHidden); |
Jeff Sharkey | 2b2667b | 2019-01-21 15:30:12 -0700 | [diff] [blame] | 5720 | final String volumeName = getVolumeName(uri); |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 5721 | |
Jeff Sharkey | beeca1e | 2019-01-24 15:57:08 -0700 | [diff] [blame] | 5722 | // Handle some legacy cases where we need to redirect thumbnails |
| 5723 | switch (match) { |
| 5724 | case AUDIO_ALBUMART_ID: { |
Jeff Sharkey | 52e4f06 | 2019-04-16 11:43:18 -0600 | [diff] [blame] | 5725 | final long albumId = Long.parseLong(uri.getPathSegments().get(3)); |
| 5726 | final Uri targetUri = ContentUris |
| 5727 | .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId); |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 5728 | return ensureThumbnail(targetUri, signal); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 5729 | } |
Jeff Sharkey | beeca1e | 2019-01-24 15:57:08 -0700 | [diff] [blame] | 5730 | case AUDIO_ALBUMART_FILE_ID: { |
Jeff Sharkey | beeca1e | 2019-01-24 15:57:08 -0700 | [diff] [blame] | 5731 | final long audioId = Long.parseLong(uri.getPathSegments().get(3)); |
Jeff Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 5732 | final Uri targetUri = ContentUris |
| 5733 | .withAppendedId(Audio.Media.getContentUri(volumeName), audioId); |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 5734 | return ensureThumbnail(targetUri, signal); |
Jeff Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 5735 | } |
| 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 Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 5740 | return ensureThumbnail(targetUri, signal); |
Jeff Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 5741 | } |
| 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 Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 5746 | return ensureThumbnail(targetUri, signal); |
Marco Nelissen | 71ece60 | 2009-06-16 12:45:10 -0700 | [diff] [blame] | 5747 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 5748 | } |
Jeff Sharkey | beeca1e | 2019-01-24 15:57:08 -0700 | [diff] [blame] | 5749 | |
| 5750 | return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 5751 | } |
| 5752 | |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5753 | @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 Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5763 | } |
| 5764 | |
| 5765 | private AssetFileDescriptor openTypedAssetFileCommon(Uri uri, String mimeTypeFilter, |
| 5766 | Bundle opts, CancellationSignal signal) throws FileNotFoundException { |
| 5767 | uri = safeUncanonicalize(uri); |
| 5768 | |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 5769 | // TODO: enforce that caller has access to this uri |
| 5770 | |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5771 | // Offer thumbnail of media, when requested |
| 5772 | final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE) |
Jeff Sharkey | c4a5f81 | 2020-05-03 21:07:14 -0600 | [diff] [blame] | 5773 | && MimeUtils.startsWithIgnoreCase(mimeTypeFilter, "image/"); |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5774 | if (wantsThumb) { |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 5775 | final ParcelFileDescriptor pfd = ensureThumbnail(uri, signal); |
| 5776 | return new AssetFileDescriptor(pfd, 0, AssetFileDescriptor.UNKNOWN_LENGTH); |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5777 | } |
| 5778 | |
| 5779 | // Worst case, return the underlying file |
| 5780 | return new AssetFileDescriptor(openFileCommon(uri, "r", signal), 0, |
| 5781 | AssetFileDescriptor.UNKNOWN_LENGTH); |
| 5782 | } |
| 5783 | |
Jeff Sharkey | cce4311 | 2020-02-09 18:07:36 -0700 | [diff] [blame] | 5784 | private ParcelFileDescriptor ensureThumbnail(Uri uri, CancellationSignal signal) |
| 5785 | throws FileNotFoundException { |
Jeff Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 5786 | final boolean allowHidden = isCallingPackageAllowedHidden(); |
| 5787 | final int match = matchUri(uri, allowHidden); |
| 5788 | |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 5789 | Trace.beginSection("ensureThumbnail"); |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 5790 | final LocalCallingIdentity token = clearLocalCallingIdentity(); |
Jeff Sharkey | beeca1e | 2019-01-24 15:57:08 -0700 | [diff] [blame] | 5791 | try { |
Jeff Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 5792 | switch (match) { |
Jeff Sharkey | 52e4f06 | 2019-04-16 11:43:18 -0600 | [diff] [blame] | 5793 | 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 Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 5808 | 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 Sharkey | cea04d5 | 2020-01-06 15:08:39 -0700 | [diff] [blame] | 5814 | case FILES_ID: |
| 5815 | case DOWNLOADS_ID: { |
| 5816 | // When item is referenced in a generic way, resolve to actual type |
Jeff Sharkey | c4a5f81 | 2020-05-03 21:07:14 -0600 | [diff] [blame] | 5817 | final int mediaType = MimeUtils.resolveMediaType(getType(uri)); |
| 5818 | switch (mediaType) { |
Jeff Sharkey | cea04d5 | 2020-01-06 15:08:39 -0700 | [diff] [blame] | 5819 | 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 Sharkey | d256887 | 2019-02-09 13:49:05 -0700 | [diff] [blame] | 5829 | default: |
| 5830 | throw new FileNotFoundException(); |
| 5831 | } |
Jeff Sharkey | beeca1e | 2019-01-24 15:57:08 -0700 | [diff] [blame] | 5832 | } catch (IOException e) { |
| 5833 | Log.w(TAG, e); |
| 5834 | throw new FileNotFoundException(e.getMessage()); |
| 5835 | } finally { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 5836 | restoreLocalCallingIdentity(token); |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 5837 | Trace.endSection(); |
Jeff Sharkey | beeca1e | 2019-01-24 15:57:08 -0700 | [diff] [blame] | 5838 | } |
| 5839 | } |
| 5840 | |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5841 | /** |
| 5842 | * Update the metadata columns for the image residing at given {@link Uri} |
| 5843 | * by reading data from the underlying image. |
| 5844 | */ |
Jeff Sharkey | 4b1921d | 2018-12-11 12:24:46 -0700 | [diff] [blame] | 5845 | 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 Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5849 | |
Jeff Sharkey | 4b1921d | 2018-12-11 12:24:46 -0700 | [diff] [blame] | 5850 | values.put(MediaColumns.WIDTH, bitmapOpts.outWidth); |
| 5851 | values.put(MediaColumns.HEIGHT, bitmapOpts.outHeight); |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5852 | } |
| 5853 | |
Sahana Rao | a211c4e | 2020-03-23 02:59:33 +0000 | [diff] [blame] | 5854 | 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 Rao | 1e8271b | 2020-04-03 14:01:08 +0100 | [diff] [blame] | 5890 | 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 Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 5900 | /** |
| 5901 | * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. |
| 5902 | */ |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 5903 | 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 Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 5913 | 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 Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 5920 | } |
Jeff Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 5921 | } |
| 5922 | } |
| 5923 | |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 5924 | /** |
| 5925 | * Return the {@link Uri} for the given {@code File}. |
| 5926 | */ |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 5927 | Uri queryForMediaUri(File file, CancellationSignal signal) throws FileNotFoundException { |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 5928 | final String volumeName = FileUtils.getVolumeName(getContext(), file); |
Jeff Sharkey | 458e40d | 2019-04-14 11:13:56 -0600 | [diff] [blame] | 5929 | final Uri uri = Files.getContentUri(volumeName); |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 5930 | 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 Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 5945 | DatabaseUtils.createSqlQueryBundle(selection, selectionArgs, null), signal); |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 5946 | if (c == null) { |
| 5947 | throw new FileNotFoundException("Missing cursor for " + uri); |
| 5948 | } else if (c.getCount() < 1) { |
Jeff Sharkey | b6781bc | 2019-07-18 18:45:52 -0600 | [diff] [blame] | 5949 | FileUtils.closeQuietly(c); |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 5950 | throw new FileNotFoundException("No item at " + uri); |
| 5951 | } else if (c.getCount() > 1) { |
Jeff Sharkey | b6781bc | 2019-07-18 18:45:52 -0600 | [diff] [blame] | 5952 | FileUtils.closeQuietly(c); |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 5953 | throw new FileNotFoundException("Multiple items at " + uri); |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 5954 | } |
| 5955 | |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 5956 | if (c.moveToFirst()) { |
| 5957 | return c; |
| 5958 | } else { |
Jeff Sharkey | b6781bc | 2019-07-18 18:45:52 -0600 | [diff] [blame] | 5959 | FileUtils.closeQuietly(c); |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 5960 | throw new FileNotFoundException("Failed to read row from " + uri); |
Jeff Sharkey | a17c1ee | 2018-10-24 19:26:19 -0600 | [diff] [blame] | 5961 | } |
| 5962 | } |
| 5963 | |
Jeff Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 5964 | /** |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 5965 | * 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 | |
Zim | edbe69e | 2019-12-13 18:49:36 +0000 | [diff] [blame] | 5978 | 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 Sharkey | f06febd | 2020-04-07 13:03:30 -0600 | [diff] [blame] | 5984 | private @NonNull FuseDaemon getFuseDaemonForFile(@NonNull File file) |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 5985 | 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; |
Zim | a76c349 | 2020-02-19 01:23:26 +0000 | [diff] [blame] | 5991 | } |
Zim | a76c349 | 2020-02-19 01:23:26 +0000 | [diff] [blame] | 5992 | } |
| 5993 | |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 5994 | private void invalidateFuseDentry(@NonNull File file) { |
| 5995 | invalidateFuseDentry(file.getAbsolutePath()); |
| 5996 | } |
| 5997 | |
| 5998 | private void invalidateFuseDentry(@NonNull String path) { |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 5999 | try { |
| 6000 | final FuseDaemon daemon = getFuseDaemonForFile(new File(path)); |
Sahana Rao | c22c85a | 2020-03-16 10:23:48 +0000 | [diff] [blame] | 6001 | if (isFuseThread()) { |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 6002 | // 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 Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 6006 | } else { |
| 6007 | daemon.invalidateFuseDentryCache(path); |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 6008 | } |
Jeff Sharkey | 564929d | 2020-04-06 16:51:58 -0600 | [diff] [blame] | 6009 | } catch (FileNotFoundException e) { |
| 6010 | Log.w(TAG, "Failed to invalidate FUSE dentry", e); |
Zim | 7e50bbc | 2020-03-06 13:50:45 +0000 | [diff] [blame] | 6011 | } |
| 6012 | } |
| 6013 | |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6014 | /** |
Jeff Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 6015 | * Replacement for {@link #openFileHelper(Uri, String)} which enforces any |
| 6016 | * permissions applicable to the path before returning. |
Nandana Dutt | 4f5e15a | 2019-11-29 10:45:58 +0000 | [diff] [blame] | 6017 | * |
| 6018 | * <p>This function should never be called from the fuse thread since it tries to open |
| 6019 | * a "/mnt/user" path. |
Jeff Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 6020 | */ |
Jeff Sharkey | 4b1921d | 2018-12-11 12:24:46 -0700 | [diff] [blame] | 6021 | private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match, |
| 6022 | String mode, CancellationSignal signal) throws FileNotFoundException { |
Zim | 7e83068 | 2020-06-09 11:20:22 +0100 | [diff] [blame] | 6023 | 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 Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 6030 | |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6031 | final boolean hasOwnerPackageName = hasOwnerPackageName(uri); |
Jeff Sharkey | dceceae | 2019-03-08 18:01:18 -0700 | [diff] [blame] | 6032 | final String[] projection = new String[] { |
| 6033 | MediaColumns.DATA, |
| 6034 | hasOwnerPackageName ? MediaColumns.OWNER_PACKAGE_NAME : "NULL", |
| 6035 | hasOwnerPackageName ? MediaColumns.IS_PENDING : "0", |
| 6036 | }; |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6037 | |
Jeff Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 6038 | final File file; |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6039 | final String ownerPackageName; |
Jeff Sharkey | dceceae | 2019-03-08 18:01:18 -0700 | [diff] [blame] | 6040 | final boolean isPending; |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 6041 | final LocalCallingIdentity token = clearLocalCallingIdentity(); |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6042 | 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 Sharkey | b057c7b | 2018-12-05 19:18:23 -0700 | [diff] [blame] | 6047 | file = new File(data).getCanonicalFile(); |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6048 | } |
| 6049 | ownerPackageName = c.getString(1); |
Jeff Sharkey | dceceae | 2019-03-08 18:01:18 -0700 | [diff] [blame] | 6050 | isPending = c.getInt(2) != 0; |
Jeff Sharkey | b057c7b | 2018-12-05 19:18:23 -0700 | [diff] [blame] | 6051 | } catch (IOException e) { |
| 6052 | throw new FileNotFoundException(e.toString()); |
Jeff Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 6053 | } finally { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 6054 | restoreLocalCallingIdentity(token); |
Jeff Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 6055 | } |
Marco Nelissen | b2c3695 | 2013-08-28 12:19:49 -0700 | [diff] [blame] | 6056 | |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 6057 | checkAccess(uri, Bundle.EMPTY, file, forWrite); |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 6058 | |
Sahana Rao | b02e715 | 2020-06-12 17:07:31 +0100 | [diff] [blame] | 6059 | // We don't check ownership for files with IS_PENDING set by FUSE |
| 6060 | if (isPending && !isPendingFromFuse(file)) { |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6061 | requireOwnershipForItem(ownerPackageName, uri); |
Jeff Sharkey | dceceae | 2019-03-08 18:01:18 -0700 | [diff] [blame] | 6062 | } |
| 6063 | |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6064 | final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName); |
Jeff Sharkey | dceceae | 2019-03-08 18:01:18 -0700 | [diff] [blame] | 6065 | // Figure out if we need to redact contents |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6066 | final boolean redactionNeeded = callerIsOwner ? false : isRedactionNeeded(uri); |
shafik | a2ae907 | 2019-10-28 12:16:00 +0000 | [diff] [blame] | 6067 | 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 Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6074 | |
| 6075 | // Yell if caller requires original, since we can't give it to them |
| 6076 | // unless they have access granted above |
Jeff Sharkey | 7ea24f2 | 2019-08-22 10:14:18 -0600 | [diff] [blame] | 6077 | if (redactionNeeded && MediaStore.getRequireOriginal(uri)) { |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6078 | throw new UnsupportedOperationException( |
| 6079 | "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"); |
| 6080 | } |
| 6081 | |
Jeff Sharkey | 4b1921d | 2018-12-11 12:24:46 -0700 | [diff] [blame] | 6082 | // Kick off metadata update when writing is finished |
| 6083 | final OnCloseListener listener = (e) -> { |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 6084 | // We always update metadata to reflect the state on disk, even when |
| 6085 | // the remote writer tried claiming an exception |
Jeff Sharkey | 7d48f8a | 2018-12-19 14:52:33 -0700 | [diff] [blame] | 6086 | invalidateThumbnails(uri); |
| 6087 | |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 6088 | 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 Sharkey | 3c0a6c6 | 2019-11-15 20:45:41 -0700 | [diff] [blame] | 6097 | mMediaScanner.scanFile(file, REASON_DEMAND); |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 6098 | break; |
Jeff Sharkey | 4b1921d | 2018-12-11 12:24:46 -0700 | [diff] [blame] | 6099 | } |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 6100 | } catch (Exception e2) { |
| 6101 | Log.w(TAG, "Failed to update metadata for " + uri, e2); |
Jeff Sharkey | 4b1921d | 2018-12-11 12:24:46 -0700 | [diff] [blame] | 6102 | } |
| 6103 | }; |
| 6104 | |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 6105 | try { |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6106 | // First, handle any redaction that is needed for caller |
| 6107 | final ParcelFileDescriptor pfd; |
Zim | edbe69e | 2019-12-13 18:49:36 +0000 | [diff] [blame] | 6108 | final String filePath = file.getPath(); |
Andrew Lewis | 360b9d3 | 2019-07-10 13:11:50 +0100 | [diff] [blame] | 6109 | if (redactionInfo.redactionRanges.length > 0) { |
Nandana Dutt | 4f5e15a | 2019-11-29 10:45:58 +0000 | [diff] [blame] | 6110 | 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. |
Zim | edbe69e | 2019-12-13 18:49:36 +0000 | [diff] [blame] | 6113 | Log.i(TAG, "Redacting with new FUSE for " + filePath); |
Zim | 7d249ef | 2020-05-26 13:55:56 +0100 | [diff] [blame] | 6114 | 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 Dutt | 4f5e15a | 2019-11-29 10:45:58 +0000 | [diff] [blame] | 6125 | } else { |
| 6126 | // TODO(b/135341978): Remove this and associated code |
| 6127 | // when fuse is on by default. |
Zim | edbe69e | 2019-12-13 18:49:36 +0000 | [diff] [blame] | 6128 | Log.i(TAG, "Redacting with old FUSE for " + filePath); |
Nandana Dutt | 4f5e15a | 2019-11-29 10:45:58 +0000 | [diff] [blame] | 6129 | pfd = RedactingFileDescriptor.open( |
| 6130 | getContext(), |
| 6131 | file, |
| 6132 | modeBits, |
| 6133 | redactionInfo.redactionRanges, |
| 6134 | redactionInfo.freeOffsets); |
| 6135 | } |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 6136 | } else { |
Jeff Sharkey | f06febd | 2020-04-07 13:03:30 -0600 | [diff] [blame] | 6137 | FuseDaemon daemon = null; |
| 6138 | try { |
| 6139 | daemon = getFuseDaemonForFile(file); |
| 6140 | } catch (FileNotFoundException ignored) { |
| 6141 | } |
Jeff Sharkey | 9a49764 | 2020-04-23 13:15:10 -0600 | [diff] [blame] | 6142 | ParcelFileDescriptor lowerFsFd = FileUtils.openSafely(file, modeBits); |
Zim | 7e83068 | 2020-06-09 11:20:22 +0100 | [diff] [blame] | 6143 | // Always acquire a readLock. This allows us make multiple opens via lower |
| 6144 | // filesystem |
Zim | edbe69e | 2019-12-13 18:49:36 +0000 | [diff] [blame] | 6145 | boolean shouldOpenWithFuse = daemon != null |
Zim | 7e83068 | 2020-06-09 11:20:22 +0100 | [diff] [blame] | 6146 | && daemon.shouldOpenWithFuse(filePath, true /* forRead */, lowerFsFd.getFd()); |
Zim | edbe69e | 2019-12-13 18:49:36 +0000 | [diff] [blame] | 6147 | |
| 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 Sharkey | 9a49764 | 2020-04-23 13:15:10 -0600 | [diff] [blame] | 6154 | pfd = FileUtils.openSafely(getFuseFile(file), modeBits); |
Zim | edbe69e | 2019-12-13 18:49:36 +0000 | [diff] [blame] | 6155 | 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); |
Zim | 782cb07 | 2020-04-08 10:41:22 +0100 | [diff] [blame] | 6162 | 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 | |
Zim | edbe69e | 2019-12-13 18:49:36 +0000 | [diff] [blame] | 6172 | pfd = lowerFsFd; |
| 6173 | } |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6174 | } |
| 6175 | |
| 6176 | // Second, wrap in any listener that we've requested |
Jeff Sharkey | 670198e | 2019-04-28 11:47:13 -0600 | [diff] [blame] | 6177 | if (!isPending && forWrite && listener != null) { |
Jeff Sharkey | e275032 | 2020-01-07 22:06:24 -0700 | [diff] [blame] | 6178 | return ParcelFileDescriptor.wrap(pfd, BackgroundThread.getHandler(), listener); |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6179 | } else { |
| 6180 | return pfd; |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 6181 | } |
| 6182 | } catch (IOException e) { |
| 6183 | if (e instanceof FileNotFoundException) { |
| 6184 | throw (FileNotFoundException) e; |
| 6185 | } else { |
| 6186 | throw new IllegalStateException(e); |
| 6187 | } |
| 6188 | } |
Marco Nelissen | b2c3695 | 2013-08-28 12:19:49 -0700 | [diff] [blame] | 6189 | } |
| 6190 | |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 6191 | 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 Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 6200 | private void deleteIfAllowed(Uri uri, Bundle extras, String path) { |
Marco Nelissen | b2c3695 | 2013-08-28 12:19:49 -0700 | [diff] [blame] | 6201 | try { |
Dipankar Bhardwaj | 043e4a3 | 2022-06-07 07:37:18 +0000 | [diff] [blame] | 6202 | final File file = new File(path).getCanonicalFile(); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 6203 | checkAccess(uri, extras, file, true); |
Jeff Sharkey | bb4e5e6 | 2020-02-09 17:14:08 -0700 | [diff] [blame] | 6204 | deleteAndInvalidate(file); |
Marco Nelissen | b2c3695 | 2013-08-28 12:19:49 -0700 | [diff] [blame] | 6205 | } catch (Exception e) { |
Jeff Sharkey | 2942111 | 2018-07-27 20:56:44 -0600 | [diff] [blame] | 6206 | Log.e(TAG, "Couldn't delete " + path, e); |
Marco Nelissen | b2c3695 | 2013-08-28 12:19:49 -0700 | [diff] [blame] | 6207 | } |
| 6208 | } |
| 6209 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 6210 | @Deprecated |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 6211 | 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 Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6229 | private boolean isRedactionNeeded(Uri uri) { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 6230 | return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED); |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6231 | } |
| 6232 | |
shafik | c3f6267 | 2019-08-30 11:15:48 +0100 | [diff] [blame] | 6233 | private boolean isRedactionNeeded() { |
| 6234 | return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED); |
| 6235 | } |
| 6236 | |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6237 | private boolean isCallingPackageRequestingLegacy() { |
| 6238 | return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_GRANTED); |
| 6239 | } |
| 6240 | |
shafik | 62cf1f6 | 2020-01-28 11:12:29 +0000 | [diff] [blame] | 6241 | 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 Rao | 406cf6d | 2020-04-08 21:52:59 +0100 | [diff] [blame] | 6247 | private boolean canAccessMediaFile(String filePath, boolean allowLegacy) { |
| 6248 | if (!allowLegacy && isCallingPackageRequestingLegacy()) { |
| 6249 | return false; |
| 6250 | } |
shafik | 62cf1f6 | 2020-01-28 11:12:29 +0000 | [diff] [blame] | 6251 | 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 | |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6261 | /** |
shafik | 62cf1f6 | 2020-01-28 11:12:29 +0000 | [diff] [blame] | 6262 | * 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 Coenen | 9a1f679 | 2020-03-17 07:13:47 +0100 | [diff] [blame] | 6267 | * <li>the calling identity owns filePath (eg /Android/data/com.foo) |
shafik | 62cf1f6 | 2020-01-28 11:12:29 +0000 | [diff] [blame] | 6268 | * <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> |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6271 | */ |
shafik | 62cf1f6 | 2020-01-28 11:12:29 +0000 | [diff] [blame] | 6272 | private boolean shouldBypassFuseRestrictions(boolean forWrite, String filePath) { |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6273 | boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite() |
| 6274 | : isCallingPackageLegacyRead(); |
Abhijeet Kaur | 7a46974 | 2020-02-19 11:51:53 +0000 | [diff] [blame] | 6275 | if (isRequestingLegacyStorage) { |
shafik | ae5b349 | 2020-01-10 15:55:51 +0000 | [diff] [blame] | 6276 | return true; |
| 6277 | } |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6278 | |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 6279 | if (isCallingPackageManager()) { |
shafik | ae5b349 | 2020-01-10 15:55:51 +0000 | [diff] [blame] | 6280 | return true; |
| 6281 | } |
| 6282 | |
Martijn Coenen | 9a1f679 | 2020-03-17 07:13:47 +0100 | [diff] [blame] | 6283 | // Files under the apps own private directory |
| 6284 | final String appSpecificDir = extractPathOwnerPackageName(filePath); |
| 6285 | if (appSpecificDir != null && isCallingIdentitySharedPackageName(appSpecificDir)) { |
| 6286 | return true; |
| 6287 | } |
| 6288 | |
shafik | 62cf1f6 | 2020-01-28 11:12:29 +0000 | [diff] [blame] | 6289 | // 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 Rao | 406cf6d | 2020-04-08 21:52:59 +0100 | [diff] [blame] | 6291 | if (canAccessMediaFile(filePath, /*allowLegacy*/ true)) { |
shafik | 62cf1f6 | 2020-01-28 11:12:29 +0000 | [diff] [blame] | 6292 | return true; |
| 6293 | } |
| 6294 | |
shafik | ae5b349 | 2020-01-10 15:55:51 +0000 | [diff] [blame] | 6295 | return false; |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6296 | } |
| 6297 | |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6298 | /** |
Martijn Coenen | 9a1f679 | 2020-03-17 07:13:47 +0100 | [diff] [blame] | 6299 | * 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 Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6322 | * Set of Exif tags that should be considered for redaction. |
| 6323 | */ |
Jeff Sharkey | 60ca298 | 2019-05-11 13:44:09 -0600 | [diff] [blame] | 6324 | private static final String[] REDACTED_EXIF_TAGS = new String[] { |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6325 | 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 Sharkey | 60ca298 | 2019-05-11 13:44:09 -0600 | [diff] [blame] | 6356 | }; |
| 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 Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6366 | }; |
| 6367 | |
Jeff Sharkey | 0043442 | 2020-02-09 12:57:58 -0700 | [diff] [blame] | 6368 | public static final Set<String> sRedactedExifTags = new ArraySet<>( |
| 6369 | Arrays.asList(REDACTED_EXIF_TAGS)); |
| 6370 | |
Andrew Lewis | 360b9d3 | 2019-07-10 13:11:50 +0100 | [diff] [blame] | 6371 | 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 | |
shafik | c3f6267 | 2019-08-30 11:15:48 +0100 | [diff] [blame] | 6380 | /** |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6381 | * Calculates the ranges that need to be redacted for the given file and user that wants to |
shafik | c3f6267 | 2019-08-30 11:15:48 +0100 | [diff] [blame] | 6382 | * access the file. |
| 6383 | * |
| 6384 | * @param uid UID of the package wanting to access the file |
shafik | 9dd60eb | 2019-11-12 20:28:53 +0000 | [diff] [blame] | 6385 | * @param path File path |
Zim | 7d249ef | 2020-05-26 13:55:56 +0100 | [diff] [blame] | 6386 | * @param tid thread id making IO on the FUSE filesystem |
shafik | a2ae907 | 2019-10-28 12:16:00 +0000 | [diff] [blame] | 6387 | * @return Ranges that should be redacted. |
| 6388 | * |
| 6389 | * @throws IOException if an error occurs while calculating the redaction ranges |
shafik | c3f6267 | 2019-08-30 11:15:48 +0100 | [diff] [blame] | 6390 | * |
| 6391 | * Called from JNI in jni/MediaProviderWrapper.cpp |
| 6392 | */ |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6393 | @Keep |
shafik | c3f6267 | 2019-08-30 11:15:48 +0100 | [diff] [blame] | 6394 | @NonNull |
Zim | 7d249ef | 2020-05-26 13:55:56 +0100 | [diff] [blame] | 6395 | public long[] getRedactionRangesForFuse(String path, int uid, int tid) throws IOException { |
shafik | c580b6d | 2019-12-10 18:45:17 +0000 | [diff] [blame] | 6396 | final File file = new File(path); |
| 6397 | |
| 6398 | // When we're calculating redaction ranges for MediaProvider, it means we're actually |
Zim | 7d249ef | 2020-05-26 13:55:56 +0100 | [diff] [blame] | 6399 | // 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 |
shafik | c580b6d | 2019-12-10 18:45:17 +0000 | [diff] [blame] | 6401 | if (uid == android.os.Process.myUid()) { |
Zim | 7d249ef | 2020-05-26 13:55:56 +0100 | [diff] [blame] | 6402 | 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 | } |
shafik | c580b6d | 2019-12-10 18:45:17 +0000 | [diff] [blame] | 6411 | } |
| 6412 | |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 6413 | final LocalCallingIdentity token = |
| 6414 | clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); |
shafik | c3f6267 | 2019-08-30 11:15:48 +0100 | [diff] [blame] | 6415 | |
| 6416 | long[] res = new long[0]; |
| 6417 | try { |
shafik | 63abf8b | 2020-03-02 15:44:37 +0000 | [diff] [blame] | 6418 | if (!isRedactionNeeded() |
| 6419 | || shouldBypassFuseRestrictions(/*forWrite*/ false, path)) { |
| 6420 | return res; |
| 6421 | } |
| 6422 | |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 6423 | final Uri contentUri = FileUtils.getContentUriForPath(path); |
shafik | 6958fce | 2019-12-17 10:49:27 +0000 | [diff] [blame] | 6424 | 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) { |
shafik | a2ae907 | 2019-10-28 12:16:00 +0000 | [diff] [blame] | 6451 | res = getRedactionRanges(file).redactionRanges; |
shafik | c3f6267 | 2019-08-30 11:15:48 +0100 | [diff] [blame] | 6452 | } |
| 6453 | } finally { |
| 6454 | restoreLocalCallingIdentity(token); |
| 6455 | } |
| 6456 | return res; |
| 6457 | } |
| 6458 | |
shafik | a2ae907 | 2019-10-28 12:16:00 +0000 | [diff] [blame] | 6459 | /** |
| 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 Sharkey | f06febd | 2020-04-07 13:03:30 -0600 | [diff] [blame] | 6468 | @VisibleForTesting |
| 6469 | public static RedactionInfo getRedactionRanges(File file) throws IOException { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 6470 | Trace.beginSection("getRedactionRanges"); |
Jeff Sharkey | 60ca298 | 2019-05-11 13:44:09 -0600 | [diff] [blame] | 6471 | final LongArray res = new LongArray(); |
Andrew Lewis | 360b9d3 | 2019-07-10 13:11:50 +0100 | [diff] [blame] | 6472 | final LongArray freeOffsets = new LongArray(); |
shafik | a2ae907 | 2019-10-28 12:16:00 +0000 | [diff] [blame] | 6473 | try (FileInputStream is = new FileInputStream(file)) { |
Jeff Sharkey | 1f6ad1a | 2019-12-20 14:26:34 -0700 | [diff] [blame] | 6474 | final String mimeType = MimeUtils.resolveMimeType(file); |
shafik | a2ae907 | 2019-10-28 12:16:00 +0000 | [diff] [blame] | 6475 | 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 Sharkey | 0043442 | 2020-02-09 12:57:58 -0700 | [diff] [blame] | 6485 | final XmpInterface exifXmp = XmpInterface.fromContainer(exif); |
shafik | a2ae907 | 2019-10-28 12:16:00 +0000 | [diff] [blame] | 6486 | res.addAll(exifXmp.getRedactionRanges()); |
Jeff Sharkey | 60ca298 | 2019-05-11 13:44:09 -0600 | [diff] [blame] | 6487 | } |
shafik | c3f6267 | 2019-08-30 11:15:48 +0100 | [diff] [blame] | 6488 | |
shafik | a2ae907 | 2019-10-28 12:16:00 +0000 | [diff] [blame] | 6489 | 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 Sharkey | 0043442 | 2020-02-09 12:57:58 -0700 | [diff] [blame] | 6501 | final XmpInterface isoXmp = XmpInterface.fromContainer(iso); |
shafik | a2ae907 | 2019-10-28 12:16:00 +0000 | [diff] [blame] | 6502 | res.addAll(isoXmp.getRedactionRanges()); |
shafik | c3f6267 | 2019-08-30 11:15:48 +0100 | [diff] [blame] | 6503 | } |
shafik | b849697 | 2019-11-28 16:32:59 +0000 | [diff] [blame] | 6504 | } catch (FileNotFoundException ignored) { |
| 6505 | // If file not found, then there's nothing to redact |
shafik | a2ae907 | 2019-10-28 12:16:00 +0000 | [diff] [blame] | 6506 | } catch (IOException e) { |
| 6507 | throw new IOException("Failed to redact " + file, e); |
shafik | c3f6267 | 2019-08-30 11:15:48 +0100 | [diff] [blame] | 6508 | } |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 6509 | Trace.endSection(); |
Andrew Lewis | 360b9d3 | 2019-07-10 13:11:50 +0100 | [diff] [blame] | 6510 | return new RedactionInfo(res.toArray(), freeOffsets.toArray()); |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 6511 | } |
| 6512 | |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6513 | /** |
Sahana Rao | b02e715 | 2020-06-12 17:07:31 +0100 | [diff] [blame] | 6514 | * @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 | /** |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6524 | * 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 Rao | a91179d | 2020-06-10 22:31:18 +0100 | [diff] [blame] | 6530 | * @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. |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6533 | * |
| 6534 | * Called from JNI in jni/MediaProviderWrapper.cpp |
| 6535 | */ |
| 6536 | @Keep |
shafik | a2966a7 | 2019-12-12 13:02:43 +0000 | [diff] [blame] | 6537 | public int isOpenAllowedForFuse(String path, int uid, boolean forWrite) { |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 6538 | final LocalCallingIdentity token = |
| 6539 | clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 6540 | |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6541 | try { |
Martijn Coenen | 9a1f679 | 2020-03-17 07:13:47 +0100 | [diff] [blame] | 6542 | if (isPrivatePackagePathNotOwnedByCaller(path)) { |
| 6543 | Log.e(TAG, "Can't open a file in another app's external directory!"); |
| 6544 | return OsConstants.ENOENT; |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6545 | } |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6546 | |
shafik | 63abf8b | 2020-03-02 15:44:37 +0000 | [diff] [blame] | 6547 | if (shouldBypassFuseRestrictions(forWrite, path)) { |
| 6548 | return 0; |
| 6549 | } |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6550 | // 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()) { |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6553 | return OsConstants.EACCES; |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6554 | } |
| 6555 | |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 6556 | final Uri contentUri = FileUtils.getContentUriForPath(path); |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6557 | 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 Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 6564 | final boolean isPending; |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6565 | 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 Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 6574 | checkAccess(fileUri, Bundle.EMPTY, file, forWrite); |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6575 | |
Sahana Rao | b02e715 | 2020-06-12 17:07:31 +0100 | [diff] [blame] | 6576 | // We don't check ownership for files with IS_PENDING set by FUSE |
| 6577 | if (isPending && !isPendingFromFuse(new File(path))) { |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6578 | requireOwnershipForItem(ownerPackageName, fileUri); |
| 6579 | } |
| 6580 | return 0; |
| 6581 | } catch (FileNotFoundException e) { |
Sahana Rao | a91179d | 2020-06-10 22:31:18 +0100 | [diff] [blame] | 6582 | // 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. |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6587 | Log.e(TAG, "Couldn't find file: " + path); |
Sahana Rao | a91179d | 2020-06-10 22:31:18 +0100 | [diff] [blame] | 6588 | return OsConstants.EACCES; |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6589 | } catch (IllegalStateException | SecurityException e) { |
| 6590 | Log.e(TAG, "Permission to access file: " + path + " is denied"); |
Sahana Rao | a91179d | 2020-06-10 22:31:18 +0100 | [diff] [blame] | 6591 | return OsConstants.EACCES; |
shafik | 15e2d61 | 2019-10-31 20:10:25 +0000 | [diff] [blame] | 6592 | } finally { |
| 6593 | restoreLocalCallingIdentity(token); |
| 6594 | } |
| 6595 | } |
| 6596 | |
| 6597 | /** |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6598 | * Returns {@code true} if {@link #mCallingIdentity#getSharedPackages(String)} contains the |
| 6599 | * given package name, {@code false} otherwise. |
shafik | 9edfb14 | 2019-11-06 11:01:40 +0000 | [diff] [blame] | 6600 | * <p> Assumes that {@code mCallingIdentity} has been properly set to reflect the calling |
| 6601 | * package. |
| 6602 | */ |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6603 | 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; |
shafik | 9edfb14 | 2019-11-06 11:01:40 +0000 | [diff] [blame] | 6608 | } |
| 6609 | } |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6610 | return false; |
shafik | 9edfb14 | 2019-11-06 11:01:40 +0000 | [diff] [blame] | 6611 | } |
| 6612 | |
| 6613 | /** |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6614 | * @throws IllegalStateException if path is invalid or doesn't match a volume. |
| 6615 | */ |
| 6616 | @NonNull |
Jeff Sharkey | c5c3914 | 2019-12-15 22:46:03 -0700 | [diff] [blame] | 6617 | private Uri getContentUriForFile(@NonNull String filePath, @NonNull String mimeType) { |
Sahana Rao | 9771047 | 2020-04-03 11:55:02 +0100 | [diff] [blame] | 6618 | final String volName = FileUtils.getVolumeName(getContext(), new File(filePath)); |
| 6619 | Uri uri = Files.getContentUri(volName); |
| 6620 | final String topLevelDir = extractTopLevelDir(filePath); |
shafik | a32e93c | 2019-11-01 12:17:34 +0000 | [diff] [blame] | 6621 | 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 Rao | 9771047 | 2020-04-03 11:55:02 +0100 | [diff] [blame] | 6624 | return uri; |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6625 | } |
shafik | a32e93c | 2019-11-01 12:17:34 +0000 | [diff] [blame] | 6626 | switch (topLevelDir) { |
shafik | a32e93c | 2019-11-01 12:17:34 +0000 | [diff] [blame] | 6627 | case DIRECTORY_PODCASTS: |
| 6628 | case DIRECTORY_RINGTONES: |
| 6629 | case DIRECTORY_ALARMS: |
| 6630 | case DIRECTORY_NOTIFICATIONS: |
| 6631 | case DIRECTORY_AUDIOBOOKS: |
Sahana Rao | 9771047 | 2020-04-03 11:55:02 +0100 | [diff] [blame] | 6632 | 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 Rao | 7169344 | 2019-11-13 13:48:07 +0000 | [diff] [blame] | 6641 | break; |
shafik | a32e93c | 2019-11-01 12:17:34 +0000 | [diff] [blame] | 6642 | case DIRECTORY_MOVIES: |
Sahana Rao | 9771047 | 2020-04-03 11:55:02 +0100 | [diff] [blame] | 6643 | 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 Rao | 7169344 | 2019-11-13 13:48:07 +0000 | [diff] [blame] | 6649 | break; |
shafik | a32e93c | 2019-11-01 12:17:34 +0000 | [diff] [blame] | 6650 | case DIRECTORY_DCIM: |
shafik | b95e024 | 2020-02-12 17:41:32 +0000 | [diff] [blame] | 6651 | case DIRECTORY_PICTURES: |
Sahana Rao | 9771047 | 2020-04-03 11:55:02 +0100 | [diff] [blame] | 6652 | if (MimeUtils.isImageMimeType(mimeType)) { |
| 6653 | uri = Images.Media.getContentUri(volName); |
shafik | a32e93c | 2019-11-01 12:17:34 +0000 | [diff] [blame] | 6654 | } else { |
Sahana Rao | 9771047 | 2020-04-03 11:55:02 +0100 | [diff] [blame] | 6655 | uri = Video.Media.getContentUri(volName); |
shafik | a32e93c | 2019-11-01 12:17:34 +0000 | [diff] [blame] | 6656 | } |
Sahana Rao | 7169344 | 2019-11-13 13:48:07 +0000 | [diff] [blame] | 6657 | break; |
shafik | a32e93c | 2019-11-01 12:17:34 +0000 | [diff] [blame] | 6658 | case DIRECTORY_DOWNLOADS: |
| 6659 | case DIRECTORY_DOCUMENTS: |
| 6660 | break; |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6661 | default: |
shafik | a32e93c | 2019-11-01 12:17:34 +0000 | [diff] [blame] | 6662 | Log.w(TAG, "Forgot to handle a top level directory in getContentUriForFile?"); |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6663 | } |
Sahana Rao | 9771047 | 2020-04-03 11:55:02 +0100 | [diff] [blame] | 6664 | return uri; |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6665 | } |
| 6666 | |
Sahana Rao | 5b7a9bd | 2020-06-12 16:32:16 +0100 | [diff] [blame] | 6667 | private boolean fileExists(@NonNull String absolutePath) { |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6668 | // 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 Rao | 5b7a9bd | 2020-06-12 16:32:16 +0100 | [diff] [blame] | 6673 | final Uri uri = FileUtils.getContentUriForPath(absolutePath); |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6674 | |
Sahana Rao | 5b7a9bd | 2020-06-12 16:32:16 +0100 | [diff] [blame] | 6675 | 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); |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6683 | } |
| 6684 | } |
| 6685 | |
Sahana Rao | 80ecfba | 2020-04-03 10:48:01 +0100 | [diff] [blame] | 6686 | 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 Rao | ad97157 | 2020-03-30 17:58:28 +0100 | [diff] [blame] | 6694 | 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 Rao | ea587fc | 2020-06-03 15:56:23 +0100 | [diff] [blame] | 6699 | values.put(FileColumns.IS_PENDING, 1); |
Sahana Rao | ad97157 | 2020-03-30 17:58:28 +0100 | [diff] [blame] | 6700 | |
| 6701 | if (useData) { |
Sahana Rao | 6b7baf4 | 2020-04-17 20:42:23 +0100 | [diff] [blame] | 6702 | values.put(FileColumns.DATA, path); |
Sahana Rao | ad97157 | 2020-03-30 17:58:28 +0100 | [diff] [blame] | 6703 | } else { |
Sahana Rao | 6ff8649 | 2020-04-30 12:39:14 +0100 | [diff] [blame] | 6704 | values.put(FileColumns.VOLUME_NAME, extractVolumeName(path)); |
Sahana Rao | ad97157 | 2020-03-30 17:58:28 +0100 | [diff] [blame] | 6705 | 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 | |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6711 | /** |
shafik | 9edfb14 | 2019-11-06 11:01:40 +0000 | [diff] [blame] | 6712 | * 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. |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6716 | * |
| 6717 | * @param path the path of the file |
| 6718 | * @param uid UID of the app requesting to create the file |
shafik | 9edfb14 | 2019-11-06 11:01:40 +0000 | [diff] [blame] | 6719 | * @return In case of success, 0. If the operation is illegal or not permitted, returns the |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6720 | * appropriate {@code errno} value: |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6721 | * <ul> |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6722 | * <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 |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6724 | * <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. |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6726 | * <li>{@link OsConstants#EIO} in case of any other I/O exception |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6727 | * </ul> |
| 6728 | * |
| 6729 | * @throws IllegalStateException if given path is invalid. |
| 6730 | * |
| 6731 | * Called from JNI in jni/MediaProviderWrapper.cpp |
| 6732 | */ |
| 6733 | @Keep |
shafik | a2966a7 | 2019-12-12 13:02:43 +0000 | [diff] [blame] | 6734 | public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) { |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6735 | final LocalCallingIdentity token = |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 6736 | clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 6737 | |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6738 | try { |
Martijn Coenen | 9a1f679 | 2020-03-17 07:13:47 +0100 | [diff] [blame] | 6739 | if (isPrivatePackagePathNotOwnedByCaller(path)) { |
| 6740 | Log.e(TAG, "Can't create a file in another app's external directory"); |
| 6741 | return OsConstants.ENOENT; |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6742 | } |
| 6743 | |
Sahana Rao | 6b7baf4 | 2020-04-17 20:42:23 +0100 | [diff] [blame] | 6744 | if (!path.equals(getAbsoluteSanitizedPath(path))) { |
| 6745 | Log.e(TAG, "File name contains invalid characters"); |
| 6746 | return OsConstants.EPERM; |
| 6747 | } |
| 6748 | |
Sahana Rao | ad97157 | 2020-03-30 17:58:28 +0100 | [diff] [blame] | 6749 | final String mimeType = MimeUtils.resolveMimeType(new File(path)); |
| 6750 | |
shafik | 63abf8b | 2020-03-02 15:44:37 +0000 | [diff] [blame] | 6751 | if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) { |
Narayan Kamath | 53a9ac3 | 2020-06-16 10:05:32 +0100 | [diff] [blame] | 6752 | final boolean callerRequestingLegacy = isCallingPackageRequestingLegacy(); |
Sahana Rao | 5b7a9bd | 2020-06-12 16:32:16 +0100 | [diff] [blame] | 6753 | 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 Kamath | 53a9ac3 | 2020-06-16 10:05:32 +0100 | [diff] [blame] | 6758 | insertFileForFuse(path, FileUtils.getContentUriForPath(path), |
| 6759 | mimeType, /*useData*/ callerRequestingLegacy); |
Sahana Rao | 5b7a9bd | 2020-06-12 16:32:16 +0100 | [diff] [blame] | 6760 | } catch (Exception ignored) { |
| 6761 | } |
Narayan Kamath | 53a9ac3 | 2020-06-16 10:05:32 +0100 | [diff] [blame] | 6762 | } 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 Rao | 5b7a9bd | 2020-06-12 16:32:16 +0100 | [diff] [blame] | 6774 | } |
Narayan Kamath | 53a9ac3 | 2020-06-16 10:05:32 +0100 | [diff] [blame] | 6775 | |
shafik | 63abf8b | 2020-03-02 15:44:37 +0000 | [diff] [blame] | 6776 | return 0; |
| 6777 | } |
Sahana Rao | ad97157 | 2020-03-30 17:58:28 +0100 | [diff] [blame] | 6778 | |
shafik | 63abf8b | 2020-03-02 15:44:37 +0000 | [diff] [blame] | 6779 | // 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 |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6781 | if (isCallingPackageRequestingLegacy()) { |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6782 | return OsConstants.EPERM; |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6783 | } |
| 6784 | |
Sahana Rao | 5b7a9bd | 2020-06-12 16:32:16 +0100 | [diff] [blame] | 6785 | if (fileExists(path)) { |
| 6786 | // If the file already exists in the db, we shouldn't allow the file creation. |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6787 | return OsConstants.EEXIST; |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6788 | } |
| 6789 | |
Sahana Rao | 5b7a9bd | 2020-06-12 16:32:16 +0100 | [diff] [blame] | 6790 | final Uri contentUri = getContentUriForFile(path, mimeType); |
Sahana Rao | ad97157 | 2020-03-30 17:58:28 +0100 | [diff] [blame] | 6791 | final Uri item = insertFileForFuse(path, contentUri, mimeType, /*useData*/ false); |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6792 | if (item == null) { |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6793 | return OsConstants.EPERM; |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6794 | } |
shafik | 9edfb14 | 2019-11-06 11:01:40 +0000 | [diff] [blame] | 6795 | return 0; |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6796 | } catch (IllegalArgumentException e) { |
shafik | 9edfb14 | 2019-11-06 11:01:40 +0000 | [diff] [blame] | 6797 | Log.e(TAG, "insertFileIfNecessary failed", e); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6798 | return OsConstants.EPERM; |
shafik | a51f3ce | 2019-10-10 17:06:41 +0100 | [diff] [blame] | 6799 | } finally { |
| 6800 | restoreLocalCallingIdentity(token); |
| 6801 | } |
| 6802 | } |
| 6803 | |
Narayan Kamath | 53a9ac3 | 2020-06-16 10:05:32 +0100 | [diff] [blame] | 6804 | 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 | |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6821 | private static int deleteFileUnchecked(@NonNull String path) { |
shafik | 0c0e0d7 | 2019-10-16 17:34:17 +0100 | [diff] [blame] | 6822 | final File toDelete = new File(path); |
| 6823 | if (toDelete.delete()) { |
| 6824 | return 0; |
| 6825 | } else { |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6826 | return OsConstants.ENOENT; |
shafik | 0c0e0d7 | 2019-10-16 17:34:17 +0100 | [diff] [blame] | 6827 | } |
| 6828 | } |
| 6829 | |
| 6830 | /** |
shafik | 9dd60eb | 2019-11-12 20:28:53 +0000 | [diff] [blame] | 6831 | * Deletes file with the given {@code path} on behalf of the app with the given {@code uid}. |
shafik | 0c0e0d7 | 2019-10-16 17:34:17 +0100 | [diff] [blame] | 6832 | * <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> |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6839 | * <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. |
shafik | 0c0e0d7 | 2019-10-16 17:34:17 +0100 | [diff] [blame] | 6843 | * </ul> |
| 6844 | * |
| 6845 | * Called from JNI in jni/MediaProviderWrapper.cpp |
| 6846 | */ |
| 6847 | @Keep |
Sahana Rao | 8a8af8c | 2020-02-03 10:40:18 +0000 | [diff] [blame] | 6848 | public int deleteFileForFuse(@NonNull String path, int uid) throws IOException { |
shafik | 0c0e0d7 | 2019-10-16 17:34:17 +0100 | [diff] [blame] | 6849 | final LocalCallingIdentity token = |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 6850 | clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); |
shafik | 0c0e0d7 | 2019-10-16 17:34:17 +0100 | [diff] [blame] | 6851 | try { |
Martijn Coenen | 9a1f679 | 2020-03-17 07:13:47 +0100 | [diff] [blame] | 6852 | if (isPrivatePackagePathNotOwnedByCaller(path)) { |
| 6853 | Log.e(TAG, "Can't delete a file in another app's external directory!"); |
| 6854 | return OsConstants.ENOENT; |
shafik | 0c0e0d7 | 2019-10-16 17:34:17 +0100 | [diff] [blame] | 6855 | } |
| 6856 | |
Sahana Rao | 7448453 | 2020-04-07 14:58:29 +0100 | [diff] [blame] | 6857 | final boolean shouldBypass = shouldBypassFuseRestrictions(/*forWrite*/ true, path); |
| 6858 | |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6859 | // 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 Rao | 7448453 | 2020-04-07 14:58:29 +0100 | [diff] [blame] | 6861 | if (!shouldBypass && isCallingPackageRequestingLegacy()) { |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6862 | return OsConstants.EPERM; |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6863 | } |
| 6864 | |
shafik | 536982a | 2020-05-14 17:54:05 +0100 | [diff] [blame] | 6865 | final Uri contentUri = FileUtils.getContentUriForPath(path); |
shafik | 0c0e0d7 | 2019-10-16 17:34:17 +0100 | [diff] [blame] | 6866 | final String where = FileColumns.DATA + " = ?"; |
Sahana Rao | 6b7baf4 | 2020-04-17 20:42:23 +0100 | [diff] [blame] | 6867 | final String[] whereArgs = {path}; |
shafik | 0c0e0d7 | 2019-10-16 17:34:17 +0100 | [diff] [blame] | 6868 | |
| 6869 | if (delete(contentUri, where, whereArgs) == 0) { |
Sahana Rao | 7448453 | 2020-04-07 14:58:29 +0100 | [diff] [blame] | 6870 | if (shouldBypass) { |
| 6871 | return deleteFileUnchecked(path); |
| 6872 | } |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6873 | return OsConstants.ENOENT; |
shafik | 0c0e0d7 | 2019-10-16 17:34:17 +0100 | [diff] [blame] | 6874 | } 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); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6881 | return OsConstants.EPERM; |
shafik | 0c0e0d7 | 2019-10-16 17:34:17 +0100 | [diff] [blame] | 6882 | } finally { |
| 6883 | restoreLocalCallingIdentity(token); |
| 6884 | } |
| 6885 | } |
| 6886 | |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6887 | /** |
| 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 |
shafik | f0fea69 | 2020-02-14 15:49:17 +0000 | [diff] [blame] | 6893 | * @param forCreate denotes whether the operation is directory creation or deletion |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6894 | * @return 0 if the operation is allowed, or the following {@code errno} values: |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6895 | * <ul> |
| 6896 | * <li>{@link OsConstants#EACCES} if the app tries to create/delete a dir in another app's |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6897 | * external directory, or if the calling package is a legacy app that doesn't have |
| 6898 | * WRITE_EXTERNAL_STORAGE permission. |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6899 | * <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 |
shafik | f0fea69 | 2020-02-14 15:49:17 +0000 | [diff] [blame] | 6905 | public int isDirectoryCreationOrDeletionAllowedForFuse( |
| 6906 | @NonNull String path, int uid, boolean forCreate) { |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 6907 | final LocalCallingIdentity token = |
| 6908 | clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); |
Zim | 696dea4 | 2020-03-07 11:41:42 +0000 | [diff] [blame] | 6909 | |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6910 | try { |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6911 | // App dirs are not indexed, so we don't create an entry for the file. |
Martijn Coenen | 9a1f679 | 2020-03-17 07:13:47 +0100 | [diff] [blame] | 6912 | if (isPrivatePackagePathNotOwnedByCaller(path)) { |
| 6913 | Log.e(TAG, "Can't modify another app's external directory!"); |
| 6914 | return OsConstants.EACCES; |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6915 | } |
| 6916 | |
shafik | 63abf8b | 2020-03-02 15:44:37 +0000 | [diff] [blame] | 6917 | if (shouldBypassFuseRestrictions(/*forWrite*/ true, path)) { |
| 6918 | return 0; |
| 6919 | } |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6920 | // 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()) { |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6923 | return OsConstants.EACCES; |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6924 | } |
| 6925 | |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6926 | final String[] relativePath = sanitizePath(extractRelativePath(path)); |
shafik | f0fea69 | 2020-02-14 15:49:17 +0000 | [diff] [blame] | 6927 | final boolean isTopLevelDir = |
| 6928 | relativePath.length == 1 && TextUtils.isEmpty(relativePath[0]); |
| 6929 | if (isTopLevelDir) { |
Nandana Dutt | 992e52f | 2020-06-25 10:55:52 +0100 | [diff] [blame] | 6930 | // We allow creating the default top level directories only, all other operations on |
shafik | f0fea69 | 2020-02-14 15:49:17 +0000 | [diff] [blame] | 6931 | // 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!"); |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6938 | return OsConstants.EPERM; |
shafik | bba5b67 | 2019-11-15 16:52:51 +0000 | [diff] [blame] | 6939 | } |
| 6940 | return 0; |
| 6941 | } finally { |
| 6942 | restoreLocalCallingIdentity(token); |
| 6943 | } |
| 6944 | } |
| 6945 | |
shafik | 824c108 | 2019-11-22 12:00:52 +0000 | [diff] [blame] | 6946 | /** |
| 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 |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6952 | * @return 0 if it's allowed to open the diretory, {@link OsConstants#EACCES} if the calling |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6953 | * package is a legacy app that doesn't have READ_EXTERNAL_STORAGE permission, |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6954 | * {@link OsConstants#ENOENT} otherwise. |
shafik | 824c108 | 2019-11-22 12:00:52 +0000 | [diff] [blame] | 6955 | * |
| 6956 | * Called from JNI in jni/MediaProviderWrapper.cpp |
| 6957 | */ |
| 6958 | @Keep |
Nandana Dutt | 992e52f | 2020-06-25 10:55:52 +0100 | [diff] [blame] | 6959 | public int isOpendirAllowedForFuse(@NonNull String path, int uid, boolean forWrite) { |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 6960 | final LocalCallingIdentity token = |
| 6961 | clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); |
shafik | 824c108 | 2019-11-22 12:00:52 +0000 | [diff] [blame] | 6962 | try { |
Nandana Dutt | c7db6a5 | 2020-06-11 21:35:28 +0100 | [diff] [blame] | 6963 | if ("/storage/emulated".equals(path)) { |
| 6964 | return OsConstants.EPERM; |
| 6965 | } |
Martijn Coenen | 9a1f679 | 2020-03-17 07:13:47 +0100 | [diff] [blame] | 6966 | if (isPrivatePackagePathNotOwnedByCaller(path)) { |
| 6967 | Log.e(TAG, "Can't access another app's external directory!"); |
| 6968 | return OsConstants.ENOENT; |
shafik | 824c108 | 2019-11-22 12:00:52 +0000 | [diff] [blame] | 6969 | } |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6970 | |
Sahana Rao | d19d3f9 | 2020-06-09 17:10:33 +0100 | [diff] [blame] | 6971 | // 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 Dutt | 992e52f | 2020-06-25 10:55:52 +0100 | [diff] [blame] | 6978 | if (shouldBypassFuseRestrictions(forWrite, path)) { |
shafik | 63abf8b | 2020-03-02 15:44:37 +0000 | [diff] [blame] | 6979 | return 0; |
| 6980 | } |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6981 | // 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()) { |
shafik | e4fb146 | 2020-01-29 16:25:23 +0000 | [diff] [blame] | 6984 | return OsConstants.EACCES; |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 6985 | } |
Nandana Dutt | 992e52f | 2020-06-25 10:55:52 +0100 | [diff] [blame] | 6986 | // 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)); |
Zim | 7838bee | 2020-07-03 14:21:32 +0100 | [diff] [blame] | 6990 | if (relativePath.length == 0) { |
| 6991 | Log.e(TAG, "Directoy write not allowed on invalid relative path for " + path); |
| 6992 | return OsConstants.EPERM; |
| 6993 | } |
Nandana Dutt | 992e52f | 2020-06-25 10:55:52 +0100 | [diff] [blame] | 6994 | 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 | } |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 7006 | |
shafik | 824c108 | 2019-11-22 12:00:52 +0000 | [diff] [blame] | 7007 | return 0; |
| 7008 | } finally { |
| 7009 | restoreLocalCallingIdentity(token); |
| 7010 | } |
| 7011 | } |
| 7012 | |
Ricky Wai | f40c402 | 2020-04-15 19:00:06 +0100 | [diff] [blame] | 7013 | @Keep |
| 7014 | public boolean isUidForPackageForFuse(@NonNull String packageName, int uid) { |
| 7015 | final LocalCallingIdentity token = |
shafik | d84da09 | 2020-04-29 17:53:30 +0100 | [diff] [blame] | 7016 | clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid)); |
Ricky Wai | f40c402 | 2020-04-15 19:00:06 +0100 | [diff] [blame] | 7017 | try { |
| 7018 | return isCallingIdentitySharedPackageName(packageName); |
| 7019 | } finally { |
| 7020 | restoreLocalCallingIdentity(token); |
| 7021 | } |
| 7022 | } |
| 7023 | |
Abhijeet Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 7024 | 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 Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 7105 | private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) { |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 7106 | // System internals can work with all media |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 7107 | if (isCallingPackageSelf() || isCallingPackageShell()) { |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 7108 | return true; |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 7109 | } |
| 7110 | |
shafik | 66f1022 | 2020-02-17 18:06:19 +0000 | [diff] [blame] | 7111 | // Apps that have permission to manage external storage can work with all files |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 7112 | if (isCallingPackageManager()) { |
shafik | 66f1022 | 2020-02-17 18:06:19 +0000 | [diff] [blame] | 7113 | return true; |
| 7114 | } |
| 7115 | |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 7116 | // 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 Chiang | ee937df | 2021-11-16 08:56:59 +0000 | [diff] [blame] | 7129 | 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 Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 7149 | } |
| 7150 | |
Jeff Sharkey | 0bf693f | 2018-10-27 19:47:17 -0600 | [diff] [blame] | 7151 | // Outstanding grant means they get access |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7152 | 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 Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 7156 | return true; |
Jeff Sharkey | 0bf693f | 2018-10-27 19:47:17 -0600 | [diff] [blame] | 7157 | } |
| 7158 | |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 7159 | return false; |
| 7160 | } |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 7161 | |
Jeff Sharkey | 4ef0563 | 2020-04-06 17:21:36 -0600 | [diff] [blame] | 7162 | @VisibleForTesting |
| 7163 | public boolean isFuseThread() { |
Sahana Rao | c22c85a | 2020-03-16 10:23:48 +0000 | [diff] [blame] | 7164 | return FuseDaemon.native_is_fuse_thread(); |
| 7165 | } |
| 7166 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7167 | @Deprecated |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 7168 | private boolean checkCallingPermissionAudio(boolean forWrite, String callingPackage) { |
Jeff Sharkey | dd5db18 | 2019-03-25 11:44:15 -0600 | [diff] [blame] | 7169 | if (forWrite) { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7170 | return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_AUDIO); |
Jeff Sharkey | dd5db18 | 2019-03-25 11:44:15 -0600 | [diff] [blame] | 7171 | } else { |
shafik | 260c140 | 2020-02-18 17:40:05 +0000 | [diff] [blame] | 7172 | // 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 Sharkey | dd5db18 | 2019-03-25 11:44:15 -0600 | [diff] [blame] | 7175 | } |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 7176 | } |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 7177 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7178 | @Deprecated |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 7179 | private boolean checkCallingPermissionVideo(boolean forWrite, String callingPackage) { |
Jeff Sharkey | dd5db18 | 2019-03-25 11:44:15 -0600 | [diff] [blame] | 7180 | if (forWrite) { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7181 | return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO); |
Jeff Sharkey | dd5db18 | 2019-03-25 11:44:15 -0600 | [diff] [blame] | 7182 | } else { |
shafik | 260c140 | 2020-02-18 17:40:05 +0000 | [diff] [blame] | 7183 | // 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 Sharkey | dd5db18 | 2019-03-25 11:44:15 -0600 | [diff] [blame] | 7186 | } |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 7187 | } |
| 7188 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7189 | @Deprecated |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 7190 | private boolean checkCallingPermissionImages(boolean forWrite, String callingPackage) { |
Jeff Sharkey | dd5db18 | 2019-03-25 11:44:15 -0600 | [diff] [blame] | 7191 | if (forWrite) { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7192 | return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES); |
Jeff Sharkey | dd5db18 | 2019-03-25 11:44:15 -0600 | [diff] [blame] | 7193 | } else { |
shafik | 260c140 | 2020-02-18 17:40:05 +0000 | [diff] [blame] | 7194 | // 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 Sharkey | dd5db18 | 2019-03-25 11:44:15 -0600 | [diff] [blame] | 7197 | } |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 7198 | } |
| 7199 | |
| 7200 | /** |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 7201 | * Enforce that caller has access to the given {@link Uri}. |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 7202 | * |
| 7203 | * @throws SecurityException if access isn't allowed. |
| 7204 | */ |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 7205 | private void enforceCallingPermission(@NonNull Uri uri, @NonNull Bundle extras, |
| 7206 | boolean forWrite) { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 7207 | Trace.beginSection("enforceCallingPermission"); |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 7208 | try { |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 7209 | enforceCallingPermissionInternal(uri, extras, forWrite); |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 7210 | } finally { |
Jeff Sharkey | 0b801a5 | 2019-08-08 11:19:51 -0600 | [diff] [blame] | 7211 | Trace.endSection(); |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 7212 | } |
| 7213 | } |
| 7214 | |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 7215 | private void enforceCallingPermissionInternal(@NonNull Uri uri, @NonNull Bundle extras, |
| 7216 | boolean forWrite) { |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 7217 | Objects.requireNonNull(uri); |
| 7218 | Objects.requireNonNull(extras); |
| 7219 | |
Jeff Sharkey | 3388f6e | 2018-11-19 12:11:38 -0700 | [diff] [blame] | 7220 | // 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 Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 7223 | // Access allowed, yay! |
| 7224 | return; |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7225 | } |
Jeff Sharkey | b057c7b | 2018-12-05 19:18:23 -0700 | [diff] [blame] | 7226 | |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 7227 | final DatabaseHelper helper; |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 7228 | try { |
| 7229 | helper = getDatabaseForUri(uri); |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 7230 | } catch (VolumeNotFoundException e) { |
| 7231 | throw e.rethrowAsIllegalArgumentException(); |
| 7232 | } |
Jeff Sharkey | b057c7b | 2018-12-05 19:18:23 -0700 | [diff] [blame] | 7233 | |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7234 | 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 Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 7239 | final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, table, uri, extras, null); |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 7240 | try (Cursor c = qb.query(helper, new String[0], |
| 7241 | null, null, null, null, null, null, null)) { |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 7242 | if (c.moveToFirst()) { |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7243 | // Direct write access granted, yay! |
Jeff Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 7244 | return; |
| 7245 | } |
| 7246 | } |
| 7247 | } |
| 7248 | |
Jeff Sharkey | 8f9ca6a | 2018-12-20 12:48:55 -0700 | [diff] [blame] | 7249 | // 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 Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7261 | // Second, check to see if caller has direct read access |
Jeff Sharkey | 61378cb | 2019-11-23 16:11:09 -0700 | [diff] [blame] | 7262 | final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, table, uri, extras, null); |
Jeff Sharkey | 88d84fb | 2020-01-13 21:38:46 -0700 | [diff] [blame] | 7263 | try (Cursor c = qb.query(helper, new String[0], |
| 7264 | null, null, null, null, null, null, null)) { |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7265 | if (c.moveToFirst()) { |
Jeff Sharkey | 8f9ca6a | 2018-12-20 12:48:55 -0700 | [diff] [blame] | 7266 | if (!forWrite) { |
| 7267 | // Direct read access granted, yay! |
| 7268 | return; |
| 7269 | } else if (allowUserGrant) { |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7270 | // 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 Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 7273 | final Collection<Uri> uris = Arrays.asList(uri); |
| 7274 | final PendingIntent intent = MediaStore |
| 7275 | .createWriteRequest(ContentResolver.wrap(this), uris); |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7276 | |
| 7277 | final Icon icon = getCollectionIcon(uri); |
| 7278 | final RemoteAction action = new RemoteAction(icon, |
Jeff Sharkey | 6ebf996 | 2019-02-15 17:43:37 -0700 | [diff] [blame] | 7279 | context.getText(R.string.permission_required_action), |
| 7280 | context.getText(R.string.permission_required_action), |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7281 | intent); |
| 7282 | |
| 7283 | throw new RecoverableSecurityException(new SecurityException( |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7284 | getCallingPackageOrSelf() + " has no access to " + uri), |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7285 | context.getText(R.string.permission_required), action); |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7286 | } |
| 7287 | } |
| 7288 | } |
| 7289 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7290 | throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri); |
Jeff Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7291 | } |
| 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 Sharkey | c3088d8 | 2018-12-11 17:32:51 -0700 | [diff] [blame] | 7298 | 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 Sharkey | 5a8bb56 | 2018-08-10 18:04:10 -0600 | [diff] [blame] | 7306 | } |
| 7307 | |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 7308 | private void checkAccess(@NonNull Uri uri, @NonNull Bundle extras, @NonNull File file, |
| 7309 | boolean isWrite) throws FileNotFoundException { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7310 | // First, does caller have the needed row-level access? |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 7311 | enforceCallingPermission(uri, extras, isWrite); |
Jeff Sharkey | 55d5bd9 | 2018-12-01 18:26:52 -0700 | [diff] [blame] | 7312 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7313 | // Second, does the path look sane? |
| 7314 | if (!FileUtils.contains(Environment.getStorageDirectory(), file)) { |
| 7315 | checkWorldReadAccess(file.getAbsolutePath()); |
Jeff Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 7316 | } |
Jeff Sharkey | 007645e | 2012-03-08 17:45:12 -0800 | [diff] [blame] | 7317 | } |
| 7318 | |
Marco Nelissen | 70eadbf | 2013-07-12 12:44:36 -0700 | [diff] [blame] | 7319 | /** |
| 7320 | * Check whether the path is a world-readable file |
| 7321 | */ |
Jeff Sharkey | f06febd | 2020-04-07 13:03:30 -0600 | [diff] [blame] | 7322 | @VisibleForTesting |
| 7323 | public static void checkWorldReadAccess(String path) throws FileNotFoundException { |
Jeff Sharkey | 55f7690 | 2015-07-24 15:22:08 -0700 | [diff] [blame] | 7324 | // 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 Nelissen | 70eadbf | 2013-07-12 12:44:36 -0700 | [diff] [blame] | 7328 | try { |
Elliott Hughes | f3b67d5 | 2014-04-28 11:42:08 -0700 | [diff] [blame] | 7329 | StructStat stat = Os.stat(path); |
Marco Nelissen | 70eadbf | 2013-07-12 12:44:36 -0700 | [diff] [blame] | 7330 | 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 Sharkey | 55f7690 | 2015-07-24 15:22:08 -0700 | [diff] [blame] | 7343 | private static void checkLeadingPathComponentsWorldExecutable(String filePath) |
Marco Nelissen | 70eadbf | 2013-07-12 12:44:36 -0700 | [diff] [blame] | 7344 | throws FileNotFoundException { |
| 7345 | File parent = new File(filePath).getParentFile(); |
| 7346 | |
Jeff Sharkey | 55f7690 | 2015-07-24 15:22:08 -0700 | [diff] [blame] | 7347 | // 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 Nelissen | 70eadbf | 2013-07-12 12:44:36 -0700 | [diff] [blame] | 7351 | |
| 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 Hughes | f3b67d5 | 2014-04-28 11:42:08 -0700 | [diff] [blame] | 7358 | StructStat stat = Os.stat(parent.getPath()); |
Marco Nelissen | 70eadbf | 2013-07-12 12:44:36 -0700 | [diff] [blame] | 7359 | 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 Sharkey | f06febd | 2020-04-07 13:03:30 -0600 | [diff] [blame] | 7371 | @VisibleForTesting |
| 7372 | static class FallbackException extends Exception { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 7373 | private final int mThrowSdkVersion; |
| 7374 | |
| 7375 | public FallbackException(String message, int throwSdkVersion) { |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 7376 | super(message); |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 7377 | mThrowSdkVersion = throwSdkVersion; |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 7378 | } |
| 7379 | |
Jeff Sharkey | d669782 | 2020-03-22 20:59:47 -0600 | [diff] [blame] | 7380 | 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 Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 7394 | public IllegalArgumentException rethrowAsIllegalArgumentException() { |
| 7395 | throw new IllegalArgumentException(getMessage()); |
| 7396 | } |
| 7397 | |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 7398 | public Cursor translateForQuery(int targetSdkVersion) { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 7399 | if (targetSdkVersion >= mThrowSdkVersion) { |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 7400 | throw new IllegalArgumentException(getMessage()); |
| 7401 | } else { |
| 7402 | Log.w(TAG, getMessage()); |
| 7403 | return null; |
| 7404 | } |
| 7405 | } |
| 7406 | |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 7407 | public Uri translateForInsert(int targetSdkVersion) { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 7408 | if (targetSdkVersion >= mThrowSdkVersion) { |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 7409 | throw new IllegalArgumentException(getMessage()); |
| 7410 | } else { |
| 7411 | Log.w(TAG, getMessage()); |
| 7412 | return null; |
| 7413 | } |
| 7414 | } |
| 7415 | |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 7416 | public int translateForUpdateDelete(int targetSdkVersion) { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 7417 | if (targetSdkVersion >= mThrowSdkVersion) { |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 7418 | throw new IllegalArgumentException(getMessage()); |
| 7419 | } else { |
| 7420 | Log.w(TAG, getMessage()); |
| 7421 | return 0; |
| 7422 | } |
| 7423 | } |
| 7424 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7425 | |
Jeff Sharkey | f06febd | 2020-04-07 13:03:30 -0600 | [diff] [blame] | 7426 | @VisibleForTesting |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 7427 | static class VolumeNotFoundException extends FallbackException { |
| 7428 | public VolumeNotFoundException(String volumeName) { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 7429 | super("Volume " + volumeName + " not found", Build.VERSION_CODES.Q); |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 7430 | } |
| 7431 | } |
| 7432 | |
Jeff Sharkey | f06febd | 2020-04-07 13:03:30 -0600 | [diff] [blame] | 7433 | @VisibleForTesting |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 7434 | static class VolumeArgumentException extends FallbackException { |
| 7435 | public VolumeArgumentException(File actual, Collection<File> allowed) { |
Jeff Sharkey | cc5c31d | 2019-10-08 16:10:53 -0600 | [diff] [blame] | 7436 | super("Requested path " + actual + " doesn't appear under " + allowed, |
| 7437 | Build.VERSION_CODES.Q); |
Jeff Sharkey | 4fc388d | 2019-03-08 18:24:21 -0700 | [diff] [blame] | 7438 | } |
| 7439 | } |
| 7440 | |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 7441 | private @NonNull DatabaseHelper getDatabaseForUri(Uri uri) throws VolumeNotFoundException { |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 7442 | final String volumeName = resolveVolumeName(uri); |
| 7443 | synchronized (mAttachedVolumeNames) { |
| 7444 | if (!mAttachedVolumeNames.contains(volumeName)) { |
Jeff Sharkey | 5ed3360 | 2019-01-23 14:31:30 -0700 | [diff] [blame] | 7445 | throw new VolumeNotFoundException(volumeName); |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 7446 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7447 | } |
Jeff Sharkey | 4dbbdfd | 2019-05-21 10:37:02 -0600 | [diff] [blame] | 7448 | if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) { |
| 7449 | return mInternalDatabase; |
| 7450 | } else { |
| 7451 | return mExternalDatabase; |
| 7452 | } |
Jeff Sharkey | 6cf27b9 | 2019-03-24 13:03:02 -0600 | [diff] [blame] | 7453 | } |
| 7454 | |
Dianne Hackborn | fd8402c | 2011-08-18 19:46:51 -0700 | [diff] [blame] | 7455 | 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.kim | 168d49e | 2013-04-03 21:08:05 +0900 | [diff] [blame] | 7462 | if (name.startsWith("external-") && name.endsWith(".db")) { |
Dianne Hackborn | fd8402c | 2011-08-18 19:46:51 -0700 | [diff] [blame] | 7463 | 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 Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 7475 | private @NonNull Uri getBaseContentUri(@NonNull String volumeName) { |
| 7476 | return MediaStore.AUTHORITY_URI.buildUpon().appendPath(volumeName).build(); |
| 7477 | } |
| 7478 | |
Zim | 604f452 | 2020-06-05 15:30:09 +0100 | [diff] [blame] | 7479 | public Uri attachVolume(String volume, boolean validate) { |
Jeff Sharkey | 3650e85 | 2019-05-29 10:11:38 -0600 | [diff] [blame] | 7480 | if (mCallingIdentity.get().pid != android.os.Process.myPid()) { |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7481 | throw new SecurityException( |
| 7482 | "Opening and closing databases not allowed."); |
| 7483 | } |
| 7484 | |
Jeff Sharkey | 5fdbd98 | 2019-01-20 11:03:28 -0700 | [diff] [blame] | 7485 | // Quick sanity check for shady volume names |
| 7486 | MediaStore.checkArgumentVolumeName(volume); |
| 7487 | |
Jeff Sharkey | 7eed0eb | 2019-01-19 17:27:46 -0700 | [diff] [blame] | 7488 | // Quick sanity check that volume actually exists |
Zim | 604f452 | 2020-06-05 15:30:09 +0100 | [diff] [blame] | 7489 | if (!MediaStore.VOLUME_INTERNAL.equals(volume) && validate) { |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 7490 | try { |
Jeff Sharkey | eeda7ba | 2019-05-17 18:48:04 -0600 | [diff] [blame] | 7491 | getVolumePath(volume); |
Jeff Sharkey | 077b71e | 2019-01-22 13:19:51 -0700 | [diff] [blame] | 7492 | } catch (IOException e) { |
| 7493 | throw new IllegalArgumentException( |
| 7494 | "Volume " + volume + " currently unavailable", e); |
| 7495 | } |
Jeff Sharkey | 7eed0eb | 2019-01-19 17:27:46 -0700 | [diff] [blame] | 7496 | } |
| 7497 | |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 7498 | synchronized (mAttachedVolumeNames) { |
| 7499 | mAttachedVolumeNames.add(volume); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7500 | } |
| 7501 | |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 7502 | final ContentResolver resolver = getContext().getContentResolver(); |
| 7503 | final Uri uri = getBaseContentUri(volume); |
| 7504 | resolver.notifyChange(getBaseContentUri(volume), null); |
| 7505 | |
Jeff Sharkey | c55994b | 2019-12-20 19:43:59 -0700 | [diff] [blame] | 7506 | if (LOGV) Log.v(TAG, "Attached volume: " + volume); |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 7507 | if (!MediaStore.VOLUME_INTERNAL.equals(volume)) { |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 7508 | // Also notify on synthetic view of all devices |
| 7509 | resolver.notifyChange(getBaseContentUri(MediaStore.VOLUME_EXTERNAL), null); |
| 7510 | |
Jeff Sharkey | 2298864 | 2020-03-05 17:09:39 -0700 | [diff] [blame] | 7511 | ForegroundThread.getExecutor().execute(() -> { |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 7512 | final DatabaseHelper helper = MediaStore.VOLUME_INTERNAL.equals(volume) |
| 7513 | ? mInternalDatabase : mExternalDatabase; |
Jeff Sharkey | a44a7ba | 2020-03-31 19:13:24 -0600 | [diff] [blame] | 7514 | helper.runWithTransaction((db) -> { |
| 7515 | ensureDefaultFolders(volume, db); |
| 7516 | ensureThumbnailsValid(volume, db); |
| 7517 | return null; |
| 7518 | }); |
Jeff Sharkey | 9aca51f | 2020-04-29 11:28:08 -0600 | [diff] [blame] | 7519 | |
| 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 Sharkey | f0ebe93 | 2019-11-13 16:09:48 -0700 | [diff] [blame] | 7524 | }); |
Jeff Sharkey | 72613f7 | 2015-08-19 14:18:19 -0700 | [diff] [blame] | 7525 | } |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 7526 | return uri; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7527 | } |
| 7528 | |
Jeff Sharkey | 7eed0eb | 2019-01-19 17:27:46 -0700 | [diff] [blame] | 7529 | private void detachVolume(Uri uri) { |
| 7530 | detachVolume(MediaStore.getVolumeName(uri)); |
| 7531 | } |
| 7532 | |
Jeff Sharkey | 5fdbd98 | 2019-01-20 11:03:28 -0700 | [diff] [blame] | 7533 | public void detachVolume(String volume) { |
Jeff Sharkey | 3650e85 | 2019-05-29 10:11:38 -0600 | [diff] [blame] | 7534 | if (mCallingIdentity.get().pid != android.os.Process.myPid()) { |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7535 | throw new SecurityException( |
| 7536 | "Opening and closing databases not allowed."); |
| 7537 | } |
| 7538 | |
Jeff Sharkey | 5fdbd98 | 2019-01-20 11:03:28 -0700 | [diff] [blame] | 7539 | // Quick sanity check for shady volume names |
| 7540 | MediaStore.checkArgumentVolumeName(volume); |
| 7541 | |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 7542 | if (MediaStore.VOLUME_INTERNAL.equals(volume)) { |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7543 | throw new UnsupportedOperationException( |
| 7544 | "Deleting the internal volume is not allowed"); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7545 | } |
| 7546 | |
Jeff Sharkey | 68b2f06 | 2019-03-22 16:54:30 -0600 | [diff] [blame] | 7547 | // Signal any scanning to shut down |
Jeff Sharkey | 85acbbe | 2019-10-15 17:10:30 -0600 | [diff] [blame] | 7548 | mMediaScanner.onDetachVolume(volume); |
Jeff Sharkey | 68b2f06 | 2019-03-22 16:54:30 -0600 | [diff] [blame] | 7549 | |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 7550 | synchronized (mAttachedVolumeNames) { |
| 7551 | mAttachedVolumeNames.remove(volume); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7552 | } |
| 7553 | |
Jeff Sharkey | 5cff16b | 2020-01-17 19:00:50 -0700 | [diff] [blame] | 7554 | 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 Sharkey | c55994b | 2019-12-20 19:43:59 -0700 | [diff] [blame] | 7563 | if (LOGV) Log.v(TAG, "Detached volume: " + volume); |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7564 | } |
| 7565 | |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 7566 | @GuardedBy("mAttachedVolumeNames") |
| 7567 | private final ArraySet<String> mAttachedVolumeNames = new ArraySet<>(); |
Jeff Sharkey | 6688130 | 2019-10-05 10:50:06 -0600 | [diff] [blame] | 7568 | @GuardedBy("mCustomCollators") |
| 7569 | private final ArraySet<String> mCustomCollators = new ArraySet<>(); |
Jeff Sharkey | 7143730 | 2019-04-09 23:46:52 -0600 | [diff] [blame] | 7570 | |
Jeff Sharkey | 85acbbe | 2019-10-15 17:10:30 -0600 | [diff] [blame] | 7571 | private MediaScanner mMediaScanner; |
| 7572 | |
Jeff Sharkey | 4dbbdfd | 2019-05-21 10:37:02 -0600 | [diff] [blame] | 7573 | private DatabaseHelper mInternalDatabase; |
| 7574 | private DatabaseHelper mExternalDatabase; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7575 | |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7576 | // name of the volume currently being scanned by the media scanner (or null) |
| 7577 | private String mMediaScannerVolume; |
| 7578 | |
Marco Nelissen | 0027019 | 2010-01-08 08:35:20 -0800 | [diff] [blame] | 7579 | // current FAT volume ID |
Mike Lockwood | 993b6f0 | 2011-01-19 14:20:51 -0800 | [diff] [blame] | 7580 | private int mVolumeId = -1; |
Marco Nelissen | 0027019 | 2010-01-08 08:35:20 -0800 | [diff] [blame] | 7581 | |
Mike Lockwood | 1717955 | 2010-07-09 10:46:58 -0400 | [diff] [blame] | 7582 | // WARNING: the values of IMAGES_MEDIA, AUDIO_MEDIA, and VIDEO_MEDIA and AUDIO_PLAYLISTS |
Mike Lockwood | 16dc0fd | 2010-09-08 12:52:17 -0400 | [diff] [blame] | 7583 | // are stored in the "files" table, so do not renumber them unless you also add |
Mike Lockwood | 1717955 | 2010-07-09 10:46:58 -0400 | [diff] [blame] | 7584 | // a corresponding database upgrade step for it. |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 7585 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7590 | |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 7591 | 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 Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 7595 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7611 | |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 7612 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7617 | |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 7618 | static final int VOLUMES = 300; |
| 7619 | static final int VOLUMES_ID = 301; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7620 | |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 7621 | static final int MEDIA_SCANNER = 500; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7622 | |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 7623 | static final int FS_ID = 600; |
| 7624 | static final int VERSION = 601; |
Marco Nelissen | 0027019 | 2010-01-08 08:35:20 -0800 | [diff] [blame] | 7625 | |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 7626 | static final int FILES = 700; |
| 7627 | static final int FILES_ID = 701; |
Brian Muramatsu | a36cfae | 2010-11-30 13:46:02 -0800 | [diff] [blame] | 7628 | |
Jeff Sharkey | eea49d3 | 2019-12-11 17:45:38 -0700 | [diff] [blame] | 7629 | static final int DOWNLOADS = 800; |
| 7630 | static final int DOWNLOADS_ID = 801; |
Sudheer Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 7631 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7632 | private LocalUriMatcher mUriMatcher; |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7633 | |
Mike Lockwood | 1d4a47c | 2010-10-12 14:24:00 -0400 | [diff] [blame] | 7634 | private static final String[] PATH_PROJECTION = new String[] { |
| 7635 | MediaStore.MediaColumns._ID, |
| 7636 | MediaStore.MediaColumns.DATA, |
| 7637 | }; |
| 7638 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7639 | private int matchUri(Uri uri, boolean allowHidden) { |
| 7640 | return mUriMatcher.matchUri(uri, allowHidden); |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 7641 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7642 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7643 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7646 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7647 | 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 Nelissen | 0027019 | 2010-01-08 08:35:20 -0800 | [diff] [blame] | 7652 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7653 | 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 Lockwood | 819cafd | 2011-01-21 17:05:41 -0800 | [diff] [blame] | 7663 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7664 | return UriMatcher.NO_MATCH; |
| 7665 | } |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 7666 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7667 | 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 Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 7673 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7674 | 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 Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7678 | 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 Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 7698 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7699 | 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 Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7704 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7705 | mPublic.addURI(auth, "*/media_scanner", MEDIA_SCANNER); |
Mike Lockwood | b78ad0d | 2010-07-03 00:45:10 -0400 | [diff] [blame] | 7706 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7707 | // 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 Shanka | aa62651 | 2018-11-15 20:29:28 -0800 | [diff] [blame] | 7711 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7712 | mHidden.addURI(auth, "*", VOLUMES_ID); |
| 7713 | mHidden.addURI(auth, null, VOLUMES); |
| 7714 | |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7715 | mPublic.addURI(auth, "*/file", FILES); |
| 7716 | mPublic.addURI(auth, "*/file/#", FILES_ID); |
Jeff Sharkey | 74f7373 | 2019-11-12 15:36:32 -0700 | [diff] [blame] | 7717 | |
| 7718 | mPublic.addURI(auth, "*/downloads", DOWNLOADS); |
| 7719 | mPublic.addURI(auth, "*/downloads/#", DOWNLOADS_ID); |
| 7720 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7721 | } |
Marco Nelissen | 10af34f | 2011-12-16 17:59:52 -0800 | [diff] [blame] | 7722 | |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 7723 | /** |
| 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 Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 7729 | private static final ArraySet<String> sMutableColumns = new ArraySet<>(); |
| 7730 | |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 7731 | { |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 7732 | sMutableColumns.add(MediaStore.MediaColumns.DATA); |
Jeff Sharkey | 2b13ebe | 2019-03-20 20:19:35 -0600 | [diff] [blame] | 7733 | sMutableColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 7734 | sMutableColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); |
Jeff Sharkey | bac84e2 | 2018-12-20 15:11:17 -0700 | [diff] [blame] | 7735 | sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING); |
Jeff Sharkey | 711d10f | 2019-01-04 16:09:52 -0700 | [diff] [blame] | 7736 | sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED); |
Jeff Sharkey | d4070ec | 2019-11-23 10:08:38 -0700 | [diff] [blame] | 7737 | sMutableColumns.add(MediaStore.MediaColumns.IS_FAVORITE); |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 7738 | sMutableColumns.add(MediaStore.MediaColumns.OWNER_PACKAGE_NAME); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 7739 | |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 7740 | 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 | |
Winson | b653af2 | 2019-06-05 12:14:13 -0700 | [diff] [blame] | 7746 | sMutableColumns.add(MediaStore.Audio.Playlists.NAME); |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 7747 | sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID); |
| 7748 | sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER); |
| 7749 | |
Jeff Sharkey | 033b1ca | 2020-06-23 07:12:19 -0600 | [diff] [blame] | 7750 | sMutableColumns.add(MediaStore.DownloadColumns.DOWNLOAD_URI); |
| 7751 | sMutableColumns.add(MediaStore.DownloadColumns.REFERER_URI); |
| 7752 | |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 7753 | 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 Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 7763 | sPlacementColumns.add(MediaStore.MediaColumns.DATA); |
Jeff Sharkey | 2b13ebe | 2019-03-20 20:19:35 -0600 | [diff] [blame] | 7764 | sPlacementColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 7765 | sPlacementColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); |
| 7766 | sPlacementColumns.add(MediaStore.MediaColumns.MIME_TYPE); |
Jeff Sharkey | 89149b6 | 2020-03-29 22:03:44 -0600 | [diff] [blame] | 7767 | sPlacementColumns.add(MediaStore.MediaColumns.IS_PENDING); |
| 7768 | sPlacementColumns.add(MediaStore.MediaColumns.IS_TRASHED); |
| 7769 | sPlacementColumns.add(MediaStore.MediaColumns.DATE_EXPIRES); |
Jeff Sharkey | 0c48d9e | 2018-08-04 20:03:34 -0600 | [diff] [blame] | 7770 | } |
| 7771 | |
Jeff Sharkey | 4230e19 | 2018-09-13 17:07:41 -0600 | [diff] [blame] | 7772 | /** |
| 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 Hansson | 459d3c7 | 2019-04-02 20:20:59 +0100 | [diff] [blame] | 7778 | private static void addGreylistPattern(String pattern) { |
| 7779 | sGreylist.add(Pattern.compile(" *" + pattern + " *")); |
| 7780 | } |
| 7781 | |
| 7782 | static { |
Jeff Sharkey | 8cfbb23 | 2018-11-18 16:23:35 -0700 | [diff] [blame] | 7783 | final String maybeAs = "( (as )?[_a-z0-9]+)?"; |
Anton Hansson | 459d3c7 | 2019-04-02 20:20:59 +0100 | [diff] [blame] | 7784 | addGreylistPattern("(?i)[_a-z0-9]+" + maybeAs); |
Anton Hansson | 00036fc | 2019-04-04 19:43:08 +0100 | [diff] [blame] | 7785 | addGreylistPattern("audio\\._id AS _id"); |
Anton Hansson | 459d3c7 | 2019-04-02 20:20:59 +0100 | [diff] [blame] | 7786 | 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 Sharkey | 14b2887 | 2019-04-03 13:00:50 -0600 | [diff] [blame] | 7794 | 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 Sharkey | 4230e19 | 2018-09-13 17:07:41 -0600 | [diff] [blame] | 7795 | } |
| 7796 | |
Jeff Sharkey | c55994b | 2019-12-20 19:43:59 -0700 | [diff] [blame] | 7797 | public ArrayMap<String, String> getProjectionMap(Class<?>... clazzes) { |
| 7798 | return mExternalDatabase.getProjectionMap(clazzes); |
Jeff Sharkey | a57867a | 2019-02-14 13:27:35 -0700 | [diff] [blame] | 7799 | } |
| 7800 | |
Jeff Sharkey | 0e88071 | 2019-02-11 11:01:31 -0700 | [diff] [blame] | 7801 | 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 Sharkey | 6688130 | 2019-10-05 10:50:06 -0600 | [diff] [blame] | 7810 | @VisibleForTesting |
Jeff Sharkey | 199f8c8 | 2019-03-23 11:54:21 -0600 | [diff] [blame] | 7811 | 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 Kaur | 3bc1577 | 2021-11-17 08:40:34 +0000 | [diff] [blame] | 7840 | private int getCallingUidOrSelf() { |
| 7841 | return mCallingIdentity.get().uid; |
| 7842 | } |
| 7843 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7844 | @Deprecated |
Jeff Sharkey | 4b0fb0c | 2015-07-27 09:50:25 -0700 | [diff] [blame] | 7845 | private String getCallingPackageOrSelf() { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7846 | return mCallingIdentity.get().getPackageName(); |
Jeff Sharkey | 4b0fb0c | 2015-07-27 09:50:25 -0700 | [diff] [blame] | 7847 | } |
| 7848 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7849 | @Deprecated |
Sahana Rao | b105c22 | 2020-06-17 20:18:48 +0100 | [diff] [blame] | 7850 | @VisibleForTesting |
| 7851 | public int getCallingPackageTargetSdkVersion() { |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7852 | return mCallingIdentity.get().getTargetSdkVersion(); |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 7853 | } |
| 7854 | |
Jeff Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 7855 | @Deprecated |
Jeff Sharkey | 9446158 | 2018-07-12 14:34:47 -0600 | [diff] [blame] | 7856 | private boolean isCallingPackageAllowedHidden() { |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 7857 | return isCallingPackageSelf(); |
Jeff Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 7858 | } |
| 7859 | |
Jeff Sharkey | 2b4e4bd | 2019-05-15 18:43:37 -0600 | [diff] [blame] | 7860 | @Deprecated |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 7861 | private boolean isCallingPackageSelf() { |
| 7862 | return mCallingIdentity.get().hasPermission(PERMISSION_IS_SELF); |
Jeff Sharkey | b0cab58 | 2019-04-16 12:42:42 -0600 | [diff] [blame] | 7863 | } |
| 7864 | |
Jeff Sharkey | 031af8d | 2019-04-28 11:11:30 -0600 | [diff] [blame] | 7865 | @Deprecated |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 7866 | private boolean isCallingPackageShell() { |
| 7867 | return mCallingIdentity.get().hasPermission(PERMISSION_IS_SHELL); |
Jeff Sharkey | bd26274 | 2019-12-17 16:40:29 -0700 | [diff] [blame] | 7868 | } |
| 7869 | |
| 7870 | @Deprecated |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 7871 | 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); |
shafik | 575d074 | 2019-11-25 17:02:57 +0000 | [diff] [blame] | 7878 | } |
| 7879 | |
| 7880 | @Deprecated |
| 7881 | private boolean isCallingPackageLegacyRead() { |
| 7882 | return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_READ); |
Jeff Sharkey | 0218c14 | 2018-10-19 15:37:00 -0600 | [diff] [blame] | 7883 | } |
| 7884 | |
Jeff Sharkey | 8411c40 | 2020-04-29 22:12:36 -0600 | [diff] [blame] | 7885 | @Deprecated |
| 7886 | private boolean isCallingPackageLegacyWrite() { |
| 7887 | return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY_WRITE); |
Sahana Rao | 406cf6d | 2020-04-08 21:52:59 +0100 | [diff] [blame] | 7888 | } |
| 7889 | |
Marco Nelissen | 10af34f | 2011-12-16 17:59:52 -0800 | [diff] [blame] | 7890 | @Override |
| 7891 | public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { |
Jeff Sharkey | f05c4e7 | 2019-08-20 10:35:50 -0600 | [diff] [blame] | 7892 | writer.println("mThumbSize=" + mThumbSize); |
Jeff Sharkey | b3e6603 | 2020-05-03 11:34:41 -0600 | [diff] [blame] | 7893 | synchronized (mAttachedVolumeNames) { |
| 7894 | writer.println("mAttachedVolumeNames=" + mAttachedVolumeNames); |
| 7895 | } |
Jeff Sharkey | 5278ead | 2020-01-07 16:40:18 -0700 | [diff] [blame] | 7896 | writer.println(); |
Marco Nelissen | 988280a | 2012-05-15 14:19:24 -0700 | [diff] [blame] | 7897 | |
Jeff Sharkey | 5278ead | 2020-01-07 16:40:18 -0700 | [diff] [blame] | 7898 | Logging.dumpPersistent(writer); |
Marco Nelissen | 10af34f | 2011-12-16 17:59:52 -0800 | [diff] [blame] | 7899 | } |
The Android Open Source Project | 7021527 | 2009-03-03 19:32:43 -0800 | [diff] [blame] | 7900 | } |