1 /* 2 * Copyright (C) 2006 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.providers.media; 18 19 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; 20 import static android.app.PendingIntent.FLAG_IMMUTABLE; 21 import static android.app.PendingIntent.FLAG_ONE_SHOT; 22 import static android.content.pm.PackageManager.PERMISSION_GRANTED; 23 import static android.os.Environment.buildPath; 24 import static android.os.Trace.TRACE_TAG_DATABASE; 25 import static android.provider.MediaStore.AUTHORITY; 26 import static android.provider.MediaStore.Downloads.PATTERN_DOWNLOADS_FILE; 27 import static android.provider.MediaStore.Downloads.isDownload; 28 import static android.provider.MediaStore.getVolumeName; 29 30 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY; 31 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED; 32 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SYSTEM; 33 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_AUDIO; 34 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES; 35 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO; 36 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_AUDIO; 37 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_IMAGES; 38 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_WRITE_VIDEO; 39 40 import android.annotation.BytesLong; 41 import android.annotation.NonNull; 42 import android.annotation.Nullable; 43 import android.app.AppGlobals; 44 import android.app.AppOpsManager; 45 import android.app.AppOpsManager.OnOpActiveChangedListener; 46 import android.app.PendingIntent; 47 import android.app.RecoverableSecurityException; 48 import android.app.RemoteAction; 49 import android.content.BroadcastReceiver; 50 import android.content.ContentProvider; 51 import android.content.ContentProviderClient; 52 import android.content.ContentProviderOperation; 53 import android.content.ContentProviderResult; 54 import android.content.ContentResolver; 55 import android.content.ContentUris; 56 import android.content.ContentValues; 57 import android.content.Context; 58 import android.content.Intent; 59 import android.content.IntentFilter; 60 import android.content.OperationApplicationException; 61 import android.content.SharedPreferences; 62 import android.content.UriMatcher; 63 import android.content.pm.PackageManager; 64 import android.content.pm.PackageManager.NameNotFoundException; 65 import android.content.pm.PermissionGroupInfo; 66 import android.content.res.AssetFileDescriptor; 67 import android.content.res.Configuration; 68 import android.content.res.Resources; 69 import android.database.AbstractCursor; 70 import android.database.Cursor; 71 import android.database.DatabaseUtils; 72 import android.database.MatrixCursor; 73 import android.database.sqlite.SQLiteDatabase; 74 import android.database.sqlite.SQLiteOpenHelper; 75 import android.database.sqlite.SQLiteQueryBuilder; 76 import android.graphics.Bitmap; 77 import android.graphics.BitmapFactory; 78 import android.graphics.drawable.Icon; 79 import android.media.ExifInterface; 80 import android.media.MediaFile; 81 import android.media.ThumbnailUtils; 82 import android.mtp.MtpConstants; 83 import android.net.Uri; 84 import android.os.Binder; 85 import android.os.Build; 86 import android.os.Bundle; 87 import android.os.CancellationSignal; 88 import android.os.Environment; 89 import android.os.FileUtils; 90 import android.os.IBinder; 91 import android.os.ParcelFileDescriptor; 92 import android.os.ParcelFileDescriptor.OnCloseListener; 93 import android.os.RedactingFileDescriptor; 94 import android.os.RemoteException; 95 import android.os.SystemClock; 96 import android.os.SystemProperties; 97 import android.os.Trace; 98 import android.os.UserHandle; 99 import android.os.UserManager; 100 import android.os.storage.StorageEventListener; 101 import android.os.storage.StorageManager; 102 import android.os.storage.StorageVolume; 103 import android.os.storage.VolumeInfo; 104 import android.os.storage.VolumeRecord; 105 import android.preference.PreferenceManager; 106 import android.provider.BaseColumns; 107 import android.provider.Column; 108 import android.provider.DocumentsContract; 109 import android.provider.MediaStore; 110 import android.provider.MediaStore.Audio; 111 import android.provider.MediaStore.Audio.AudioColumns; 112 import android.provider.MediaStore.Audio.Playlists; 113 import android.provider.MediaStore.Downloads; 114 import android.provider.MediaStore.Files; 115 import android.provider.MediaStore.Files.FileColumns; 116 import android.provider.MediaStore.Images; 117 import android.provider.MediaStore.Images.ImageColumns; 118 import android.provider.MediaStore.MediaColumns; 119 import android.provider.MediaStore.Video; 120 import android.system.ErrnoException; 121 import android.system.Os; 122 import android.system.OsConstants; 123 import android.system.StructStat; 124 import android.text.TextUtils; 125 import android.text.format.DateUtils; 126 import android.util.ArrayMap; 127 import android.util.ArraySet; 128 import android.util.DisplayMetrics; 129 import android.util.Log; 130 import android.util.LongArray; 131 import android.util.LongSparseArray; 132 import android.util.Pair; 133 import android.util.Size; 134 import android.util.SparseArray; 135 136 import com.android.internal.annotations.GuardedBy; 137 import com.android.internal.annotations.VisibleForTesting; 138 import com.android.internal.os.BackgroundThread; 139 import com.android.internal.util.ArrayUtils; 140 import com.android.internal.util.IndentingPrintWriter; 141 import com.android.providers.media.scan.MediaScanner; 142 import com.android.providers.media.scan.ModernMediaScanner; 143 import com.android.providers.media.util.CachedSupplier; 144 import com.android.providers.media.util.IsoInterface; 145 import com.android.providers.media.util.XmpInterface; 146 147 import libcore.io.IoUtils; 148 import libcore.util.EmptyArray; 149 150 import java.io.File; 151 import java.io.FileDescriptor; 152 import java.io.FileInputStream; 153 import java.io.FileNotFoundException; 154 import java.io.FileOutputStream; 155 import java.io.FilenameFilter; 156 import java.io.IOException; 157 import java.io.OutputStream; 158 import java.io.PrintWriter; 159 import java.lang.reflect.Field; 160 import java.util.ArrayList; 161 import java.util.Arrays; 162 import java.util.Collection; 163 import java.util.List; 164 import java.util.Locale; 165 import java.util.Map; 166 import java.util.Objects; 167 import java.util.Set; 168 import java.util.UUID; 169 import java.util.concurrent.TimeUnit; 170 import java.util.function.Consumer; 171 import java.util.function.Supplier; 172 import java.util.regex.Matcher; 173 import java.util.regex.Pattern; 174 175 /** 176 * Media content provider. See {@link android.provider.MediaStore} for details. 177 * Separate databases are kept for each external storage card we see (using the 178 * card's ID as an index). The content visible at content://media/external/... 179 * changes with the card. 180 */ 181 public class MediaProvider extends ContentProvider { 182 public static final boolean ENABLE_MODERN_SCANNER = SystemProperties 183 .getBoolean("persist.sys.modern_scanner", true); 184 185 /** 186 * Regex that matches paths in all well-known package-specific directories, 187 * and which captures the package name as the first group. 188 */ 189 private static final Pattern PATTERN_OWNED_PATH = Pattern.compile( 190 "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:data|media|obb|sandbox)/([^/]+)/.*"); 191 192 /** 193 * Regex that matches paths under well-known storage paths. 194 */ 195 private static final Pattern PATTERN_STORAGE_PATH = Pattern.compile( 196 "(?i)^/storage/[^/]+/(?:[0-9]+/)?"); 197 198 /** 199 * Regex that matches paths for {@link MediaColumns#RELATIVE_PATH}; it 200 * captures both top-level paths and sandboxed paths. 201 */ 202 private static final Pattern PATTERN_RELATIVE_PATH = Pattern.compile( 203 "(?i)^/storage/[^/]+/(?:[0-9]+/)?(Android/sandbox/([^/]+)/)?"); 204 205 /** 206 * Regex that matches paths under well-known storage paths. 207 */ 208 private static final Pattern PATTERN_VOLUME_NAME = Pattern.compile( 209 "(?i)^/storage/([^/]+)"); 210 211 /** 212 * Regex of a selection string that matches a specific ID. 213 */ 214 private static final Pattern PATTERN_SELECTION_ID = Pattern.compile( 215 "(?:image_id|video_id)\\s*=\\s*(\\d+)"); 216 217 /** 218 * Set of {@link Cursor} columns that refer to raw filesystem paths. 219 */ 220 private static final ArrayMap<String, Object> sDataColumns = new ArrayMap<>(); 221 222 { sDataColumns.put(MediaStore.MediaColumns.DATA, null)223 sDataColumns.put(MediaStore.MediaColumns.DATA, null); sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null)224 sDataColumns.put(MediaStore.Images.Thumbnails.DATA, null); sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null)225 sDataColumns.put(MediaStore.Video.Thumbnails.DATA, null); sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null)226 sDataColumns.put(MediaStore.Audio.PlaylistsColumns.DATA, null); sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null)227 sDataColumns.put(MediaStore.Audio.AlbumColumns.ALBUM_ART, null); 228 } 229 230 private static final Object sCacheLock = new Object(); 231 232 @GuardedBy("sCacheLock") 233 private static final List<VolumeInfo> sCachedVolumes = new ArrayList<>(); 234 @GuardedBy("sCacheLock") 235 private static final Set<String> sCachedExternalVolumeNames = new ArraySet<>(); 236 @GuardedBy("sCacheLock") 237 private static final Map<String, Collection<File>> sCachedVolumeScanPaths = new ArrayMap<>(); 238 updateVolumes()239 private void updateVolumes() { 240 synchronized (sCacheLock) { 241 sCachedVolumes.clear(); 242 sCachedVolumes.addAll(mStorageManager.getVolumes()); 243 244 sCachedExternalVolumeNames.clear(); 245 sCachedExternalVolumeNames.addAll(MediaStore.getExternalVolumeNames(getContext())); 246 247 sCachedVolumeScanPaths.clear(); 248 try { 249 sCachedVolumeScanPaths.put(MediaStore.VOLUME_INTERNAL, 250 MediaStore.getVolumeScanPaths(MediaStore.VOLUME_INTERNAL)); 251 for (String volumeName : sCachedExternalVolumeNames) { 252 sCachedVolumeScanPaths.put(volumeName, 253 MediaStore.getVolumeScanPaths(volumeName)); 254 } 255 } catch (FileNotFoundException e) { 256 throw new IllegalStateException(e.getMessage()); 257 } 258 } 259 } 260 getVolumePath(String volumeName)261 public static File getVolumePath(String volumeName) throws FileNotFoundException { 262 synchronized (sCacheLock) { 263 return MediaStore.getVolumePath(sCachedVolumes, volumeName); 264 } 265 } 266 getExternalVolumeNames()267 public static Set<String> getExternalVolumeNames() { 268 synchronized (sCacheLock) { 269 return new ArraySet<>(sCachedExternalVolumeNames); 270 } 271 } 272 getVolumeScanPaths(String volumeName)273 public static Collection<File> getVolumeScanPaths(String volumeName) { 274 synchronized (sCacheLock) { 275 return new ArrayList<>(sCachedVolumeScanPaths.get(volumeName)); 276 } 277 } 278 279 private StorageManager mStorageManager; 280 private AppOpsManager mAppOpsManager; 281 private PackageManager mPackageManager; 282 283 private Size mThumbSize; 284 285 /** 286 * Map from UID to cached {@link LocalCallingIdentity}. Values are only 287 * maintained in this map while the UID is actively working with a 288 * performance-critical component, such as camera. 289 */ 290 @GuardedBy("mCachedCallingIdentity") 291 private final SparseArray<LocalCallingIdentity> mCachedCallingIdentity = new SparseArray<>(); 292 293 private static volatile long sBackgroundDelay = 0; 294 295 private final OnOpActiveChangedListener mActiveListener = (code, uid, packageName, active) -> { 296 synchronized (mCachedCallingIdentity) { 297 if (active) { 298 mCachedCallingIdentity.put(uid, 299 LocalCallingIdentity.fromExternal(uid, packageName)); 300 } else { 301 mCachedCallingIdentity.remove(uid); 302 } 303 304 if (mCachedCallingIdentity.size() > 0) { 305 sBackgroundDelay = 10 * DateUtils.SECOND_IN_MILLIS; 306 } else { 307 sBackgroundDelay = 0; 308 } 309 } 310 }; 311 312 /** 313 * Calling identity state about on the current thread. Populated on demand, 314 * and invalidated by {@link #onCallingPackageChanged()} when each remote 315 * call is finished. 316 */ 317 private final ThreadLocal<LocalCallingIdentity> mCallingIdentity = ThreadLocal 318 .withInitial(() -> { 319 synchronized (mCachedCallingIdentity) { 320 final LocalCallingIdentity cached = mCachedCallingIdentity 321 .get(Binder.getCallingUid()); 322 return (cached != null) ? cached : LocalCallingIdentity.fromBinder(this); 323 } 324 }); 325 326 // In memory cache of path<->id mappings, to speed up inserts during media scan 327 @GuardedBy("mDirectoryCache") 328 private final ArrayMap<String, Long> mDirectoryCache = new ArrayMap<>(); 329 330 private static final String[] sMediaTableColumns = new String[] { 331 FileColumns._ID, 332 FileColumns.MEDIA_TYPE, 333 }; 334 335 private static final String[] sIdOnlyColumn = new String[] { 336 FileColumns._ID 337 }; 338 339 private static final String[] sDataOnlyColumn = new String[] { 340 FileColumns.DATA 341 }; 342 343 private static final String[] sPlaylistIdPlayOrder = new String[] { 344 Playlists.Members.PLAYLIST_ID, 345 Playlists.Members.PLAY_ORDER 346 }; 347 348 private static final String ID_NOT_PARENT_CLAUSE = 349 "_id NOT IN (SELECT parent FROM files)"; 350 351 private static final String CANONICAL = "canonical"; 352 353 private BroadcastReceiver mMediaReceiver = new BroadcastReceiver() { 354 @Override 355 public void onReceive(Context context, Intent intent) { 356 final StorageVolume sv = intent.getParcelableExtra(StorageVolume.EXTRA_STORAGE_VOLUME); 357 if (sv != null) { 358 final String volumeName; 359 if (sv.isPrimary()) { 360 volumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY; 361 } else { 362 try { 363 volumeName = MediaStore.checkArgumentVolumeName(sv.getNormalizedUuid()); 364 } catch (IllegalArgumentException ignored) { 365 return; 366 } 367 } 368 369 switch (intent.getAction()) { 370 case Intent.ACTION_MEDIA_MOUNTED: 371 attachVolume(volumeName); 372 break; 373 case Intent.ACTION_MEDIA_UNMOUNTED: 374 case Intent.ACTION_MEDIA_EJECT: 375 case Intent.ACTION_MEDIA_REMOVED: 376 case Intent.ACTION_MEDIA_BAD_REMOVAL: 377 detachVolume(volumeName); 378 break; 379 } 380 } 381 } 382 }; 383 384 private final SQLiteDatabase.CustomFunction mObjectRemovedCallback = 385 new SQLiteDatabase.CustomFunction() { 386 @Override 387 public void callback(String[] args) { 388 // We could remove only the deleted entry from the cache, but that 389 // requires the path, which we don't have here, so instead we just 390 // clear the entire cache. 391 // TODO: include the path in the callback and only remove the affected 392 // entry from the cache 393 synchronized (mDirectoryCache) { 394 mDirectoryCache.clear(); 395 } 396 } 397 }; 398 399 /** 400 * Wrapper class for a specific database (associated with one particular 401 * external card, or with internal storage). Can open the actual database 402 * on demand, create and upgrade the schema, etc. 403 */ 404 static class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable { 405 final Context mContext; 406 final String mName; 407 final int mVersion; 408 final boolean mInternal; // True if this is the internal database 409 final boolean mEarlyUpgrade; 410 final SQLiteDatabase.CustomFunction mObjectRemovedCallback; 411 long mScanStartTime; 412 long mScanStopTime; 413 414 // In memory caches of artist and album data. 415 ArrayMap<String, Long> mArtistCache = new ArrayMap<String, Long>(); 416 ArrayMap<String, Long> mAlbumCache = new ArrayMap<String, Long>(); 417 DatabaseHelper(Context context, String name, boolean internal, boolean earlyUpgrade, SQLiteDatabase.CustomFunction objectRemovedCallback)418 public DatabaseHelper(Context context, String name, boolean internal, 419 boolean earlyUpgrade, SQLiteDatabase.CustomFunction objectRemovedCallback) { 420 this(context, name, getDatabaseVersion(context), internal, earlyUpgrade, 421 objectRemovedCallback); 422 } 423 DatabaseHelper(Context context, String name, int version, boolean internal, boolean earlyUpgrade, SQLiteDatabase.CustomFunction objectRemovedCallback)424 public DatabaseHelper(Context context, String name, int version, boolean internal, 425 boolean earlyUpgrade, SQLiteDatabase.CustomFunction objectRemovedCallback) { 426 super(context, name, null, version); 427 mContext = context; 428 mName = name; 429 mVersion = version; 430 mInternal = internal; 431 mEarlyUpgrade = earlyUpgrade; 432 mObjectRemovedCallback = objectRemovedCallback; 433 setWriteAheadLoggingEnabled(true); 434 setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS); 435 } 436 437 @Override onCreate(final SQLiteDatabase db)438 public void onCreate(final SQLiteDatabase db) { 439 Log.v(TAG, "onCreate() for " + mName); 440 updateDatabase(mContext, db, mInternal, 0, mVersion); 441 } 442 443 @Override onUpgrade(final SQLiteDatabase db, final int oldV, final int newV)444 public void onUpgrade(final SQLiteDatabase db, final int oldV, final int newV) { 445 Log.v(TAG, "onUpgrade() for " + mName + " from " + oldV + " to " + newV); 446 updateDatabase(mContext, db, mInternal, oldV, newV); 447 } 448 449 @Override onDowngrade(final SQLiteDatabase db, final int oldV, final int newV)450 public void onDowngrade(final SQLiteDatabase db, final int oldV, final int newV) { 451 Log.v(TAG, "onDowngrade() for " + mName + " from " + oldV + " to " + newV); 452 downgradeDatabase(mContext, db, mInternal, oldV, newV); 453 } 454 455 /** 456 * For devices that have removable storage, we support keeping multiple databases 457 * to allow users to switch between a number of cards. 458 * On such devices, touch this particular database and garbage collect old databases. 459 * An LRU cache system is used to clean up databases for old external 460 * storage volumes. 461 */ 462 @Override onOpen(SQLiteDatabase db)463 public void onOpen(SQLiteDatabase db) { 464 465 if (mEarlyUpgrade) return; // Doing early upgrade. 466 467 if (mObjectRemovedCallback != null) { 468 db.addCustomFunction("_OBJECT_REMOVED", 1, mObjectRemovedCallback); 469 } 470 471 if (mInternal) return; // The internal database is kept separately. 472 473 // the code below is only needed on devices with removable storage 474 if (!Environment.isExternalStorageRemovable()) return; 475 476 // touch the database file to show it is most recently used 477 File file = new File(db.getPath()); 478 long now = System.currentTimeMillis(); 479 file.setLastModified(now); 480 481 // delete least recently used databases if we are over the limit 482 String[] databases = mContext.databaseList(); 483 // Don't delete wal auxiliary files(db-shm and db-wal) directly because db file may 484 // not be deleted, and it will cause Disk I/O error when accessing this database. 485 List<String> dbList = new ArrayList<String>(); 486 for (String database : databases) { 487 if (database != null && database.endsWith(".db")) { 488 dbList.add(database); 489 } 490 } 491 databases = dbList.toArray(new String[0]); 492 int count = databases.length; 493 int limit = MAX_EXTERNAL_DATABASES; 494 495 // delete external databases that have not been used in the past two months 496 long twoMonthsAgo = now - OBSOLETE_DATABASE_DB; 497 for (int i = 0; i < databases.length; i++) { 498 File other = mContext.getDatabasePath(databases[i]); 499 if (INTERNAL_DATABASE_NAME.equals(databases[i]) || file.equals(other)) { 500 databases[i] = null; 501 count--; 502 if (file.equals(other)) { 503 // reduce limit to account for the existence of the database we 504 // are about to open, which we removed from the list. 505 limit--; 506 } 507 } else { 508 long time = other.lastModified(); 509 if (time < twoMonthsAgo) { 510 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[i]); 511 mContext.deleteDatabase(databases[i]); 512 databases[i] = null; 513 count--; 514 } 515 } 516 } 517 518 // delete least recently used databases until 519 // we are no longer over the limit 520 while (count > limit) { 521 int lruIndex = -1; 522 long lruTime = 0; 523 524 for (int i = 0; i < databases.length; i++) { 525 if (databases[i] != null) { 526 long time = mContext.getDatabasePath(databases[i]).lastModified(); 527 if (lruTime == 0 || time < lruTime) { 528 lruIndex = i; 529 lruTime = time; 530 } 531 } 532 } 533 534 // delete least recently used database 535 if (lruIndex != -1) { 536 if (LOCAL_LOGV) Log.v(TAG, "Deleting old database " + databases[lruIndex]); 537 mContext.deleteDatabase(databases[lruIndex]); 538 databases[lruIndex] = null; 539 count--; 540 } 541 } 542 } 543 544 /** 545 * List of {@link Uri} that would have been sent directly via 546 * {@link ContentResolver#notifyChange}, but are instead being collected 547 * due to an ongoing transaction. 548 */ 549 private final ThreadLocal<List<Uri>> mNotifyChanges = new ThreadLocal<>(); 550 beginTransaction()551 public void beginTransaction() { 552 getWritableDatabase().beginTransaction(); 553 mNotifyChanges.set(new ArrayList<>()); 554 } 555 setTransactionSuccessful()556 public void setTransactionSuccessful() { 557 getWritableDatabase().setTransactionSuccessful(); 558 final List<Uri> uris = mNotifyChanges.get(); 559 if (uris != null) { 560 BackgroundThread.getHandler().postDelayed(() -> { 561 for (Uri uri : uris) { 562 notifyChangeInternal(uri); 563 } 564 }, sBackgroundDelay); 565 } 566 mNotifyChanges.remove(); 567 } 568 endTransaction()569 public void endTransaction() { 570 getWritableDatabase().endTransaction(); 571 } 572 573 /** 574 * Notify that the given {@link Uri} has changed. This enqueues the 575 * notification if currently inside a transaction, and they'll be 576 * clustered and sent when the transaction completes. 577 */ notifyChange(Uri uri)578 public void notifyChange(Uri uri) { 579 if (LOCAL_LOGV) Log.v(TAG, "Notifying " + uri); 580 final List<Uri> uris = mNotifyChanges.get(); 581 if (uris != null) { 582 uris.add(uri); 583 } else { 584 BackgroundThread.getHandler().postDelayed(() -> { 585 notifyChangeInternal(uri); 586 }, sBackgroundDelay); 587 } 588 } 589 notifyChangeInternal(Uri uri)590 private void notifyChangeInternal(Uri uri) { 591 Trace.traceBegin(TRACE_TAG_DATABASE, "notifyChange"); 592 try { 593 mContext.getContentResolver().notifyChange(uri, null); 594 } finally { 595 Trace.traceEnd(TRACE_TAG_DATABASE); 596 } 597 } 598 } 599 600 /** 601 * Apply {@link Consumer#accept} to the given {@link Uri}. 602 * <p> 603 * Since media items can be exposed through multiple collections or views, 604 * this method expands the single item being accepted to also accept all 605 * relevant views. 606 */ acceptWithExpansion(Consumer<Uri> consumer, Uri uri)607 public static void acceptWithExpansion(Consumer<Uri> consumer, Uri uri) { 608 final int match = matchUri(uri, true); 609 acceptWithExpansionInternal(consumer, uri, match); 610 611 try { 612 // When targeting a specific volume, we need to expand to also 613 // notify the top-level view 614 final String volumeName = getVolumeName(uri); 615 switch (volumeName) { 616 case MediaStore.VOLUME_INTERNAL: 617 case MediaStore.VOLUME_EXTERNAL: 618 // Already a top-level view, no need to expand 619 break; 620 default: 621 final List<String> segments = new ArrayList<>(uri.getPathSegments()); 622 segments.set(0, MediaStore.VOLUME_EXTERNAL); 623 final Uri.Builder builder = uri.buildUpon().path(null); 624 for (String segment : segments) { 625 builder.appendPath(segment); 626 } 627 acceptWithExpansionInternal(consumer, builder.build(), match); 628 break; 629 } 630 } catch (IllegalArgumentException ignored) { 631 } 632 } 633 acceptWithExpansionInternal(Consumer<Uri> consumer, Uri uri, int match)634 private static void acceptWithExpansionInternal(Consumer<Uri> consumer, Uri uri, int match) { 635 // Start by always notifying the base item 636 consumer.accept(uri); 637 638 // Some items can be exposed through multiple collections, 639 // so we need to notify all possible views of those items 640 switch (match) { 641 case AUDIO_MEDIA_ID: 642 case VIDEO_MEDIA_ID: 643 case IMAGES_MEDIA_ID: { 644 final String volumeName = getVolumeName(uri); 645 final long id = ContentUris.parseId(uri); 646 consumer.accept(Files.getContentUri(volumeName, id)); 647 consumer.accept(Downloads.getContentUri(volumeName, id)); 648 break; 649 } 650 case AUDIO_MEDIA: 651 case VIDEO_MEDIA: 652 case IMAGES_MEDIA: { 653 final String volumeName = getVolumeName(uri); 654 consumer.accept(Files.getContentUri(volumeName)); 655 consumer.accept(Downloads.getContentUri(volumeName)); 656 break; 657 } 658 case FILES_ID: 659 case DOWNLOADS_ID: { 660 final String volumeName = getVolumeName(uri); 661 final long id = ContentUris.parseId(uri); 662 consumer.accept(Audio.Media.getContentUri(volumeName, id)); 663 consumer.accept(Video.Media.getContentUri(volumeName, id)); 664 consumer.accept(Images.Media.getContentUri(volumeName, id)); 665 break; 666 } 667 case FILES: 668 case DOWNLOADS: { 669 final String volumeName = getVolumeName(uri); 670 consumer.accept(Audio.Media.getContentUri(volumeName)); 671 consumer.accept(Video.Media.getContentUri(volumeName)); 672 consumer.accept(Images.Media.getContentUri(volumeName)); 673 break; 674 } 675 } 676 677 // Any changing audio items mean we probably need to invalidate all 678 // indexed views built from that media 679 switch (match) { 680 case AUDIO_MEDIA: 681 case AUDIO_MEDIA_ID: { 682 final String volumeName = getVolumeName(uri); 683 consumer.accept(Audio.Genres.getContentUri(volumeName)); 684 consumer.accept(Audio.Playlists.getContentUri(volumeName)); 685 consumer.accept(Audio.Artists.getContentUri(volumeName)); 686 consumer.accept(Audio.Albums.getContentUri(volumeName)); 687 break; 688 } 689 } 690 } 691 692 private static final String[] sDefaultFolderNames = { 693 Environment.DIRECTORY_MUSIC, 694 Environment.DIRECTORY_PODCASTS, 695 Environment.DIRECTORY_RINGTONES, 696 Environment.DIRECTORY_ALARMS, 697 Environment.DIRECTORY_NOTIFICATIONS, 698 Environment.DIRECTORY_PICTURES, 699 Environment.DIRECTORY_MOVIES, 700 Environment.DIRECTORY_DOWNLOADS, 701 Environment.DIRECTORY_DCIM, 702 }; 703 704 /** 705 * This method cleans up any files created by android.media.MiniThumbFile, removed after P. 706 * It's triggered during database update only, in order to run only once. 707 */ deleteLegacyThumbnailData()708 private static void deleteLegacyThumbnailData() { 709 File directory = new File(Environment.getExternalStorageDirectory(), "/DCIM/.thumbnails"); 710 711 FilenameFilter filter = (dir, filename) -> filename.startsWith(".thumbdata"); 712 for (File f : ArrayUtils.defeatNullable(directory.listFiles(filter))) { 713 if (!f.delete()) { 714 Log.e(TAG, "Failed to delete legacy thumbnail data " + f.getAbsolutePath()); 715 } 716 } 717 } 718 719 /** 720 * Ensure that default folders are created on mounted primary storage 721 * devices. We only do this once per volume so we don't annoy the user if 722 * deleted manually. 723 */ ensureDefaultFolders(String volumeName, DatabaseHelper helper, SQLiteDatabase db)724 private void ensureDefaultFolders(String volumeName, DatabaseHelper helper, SQLiteDatabase db) { 725 try { 726 final File path = getVolumePath(volumeName); 727 final StorageVolume vol = mStorageManager.getStorageVolume(path); 728 final String key; 729 if (VolumeInfo.ID_EMULATED_INTERNAL.equals(vol.getId())) { 730 key = "created_default_folders"; 731 } else { 732 key = "created_default_folders_" + vol.getNormalizedUuid(); 733 } 734 735 final SharedPreferences prefs = PreferenceManager 736 .getDefaultSharedPreferences(getContext()); 737 if (prefs.getInt(key, 0) == 0) { 738 for (String folderName : sDefaultFolderNames) { 739 final File folder = new File(vol.getPathFile(), folderName); 740 if (!folder.exists()) { 741 folder.mkdirs(); 742 insertDirectory(helper, db, folder.getAbsolutePath()); 743 } 744 } 745 746 SharedPreferences.Editor editor = prefs.edit(); 747 editor.putInt(key, 1); 748 editor.commit(); 749 } 750 } catch (IOException e) { 751 Log.w(TAG, "Failed to ensure default folders for " + volumeName, e); 752 } 753 } 754 getDatabaseVersion(Context context)755 public static int getDatabaseVersion(Context context) { 756 try { 757 return context.getPackageManager().getPackageInfo( 758 context.getPackageName(), 0).versionCode; 759 } catch (NameNotFoundException e) { 760 throw new RuntimeException("couldn't get version code for " + context); 761 } 762 } 763 764 @Override onCreate()765 public boolean onCreate() { 766 final Context context = getContext(); 767 768 // Enable verbose transport logging when requested 769 setTransportLoggingEnabled(LOCAL_LOGV); 770 771 // Shift call statistics back to the original caller 772 Binder.setProxyTransactListener( 773 new Binder.PropagateWorkSourceTransactListener()); 774 775 mStorageManager = context.getSystemService(StorageManager.class); 776 mAppOpsManager = context.getSystemService(AppOpsManager.class); 777 mPackageManager = context.getPackageManager(); 778 779 // Reasonable thumbnail size is half of the smallest screen edge width 780 final DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 781 final int thumbSize = Math.min(metrics.widthPixels, metrics.heightPixels) / 2; 782 mThumbSize = new Size(thumbSize, thumbSize); 783 784 mInternalDatabase = new DatabaseHelper(context, INTERNAL_DATABASE_NAME, true, 785 false, mObjectRemovedCallback); 786 mExternalDatabase = new DatabaseHelper(context, EXTERNAL_DATABASE_NAME, false, 787 false, mObjectRemovedCallback); 788 789 final IntentFilter filter = new IntentFilter(); 790 filter.setPriority(10); 791 filter.addDataScheme("file"); 792 filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED); 793 filter.addAction(Intent.ACTION_MEDIA_MOUNTED); 794 filter.addAction(Intent.ACTION_MEDIA_EJECT); 795 filter.addAction(Intent.ACTION_MEDIA_REMOVED); 796 filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL); 797 context.registerReceiver(mMediaReceiver, filter); 798 799 // Watch for invalidation of cached volumes 800 mStorageManager.registerListener(new StorageEventListener() { 801 @Override 802 public void onVolumeStateChanged(VolumeInfo vol, int oldState, int newState) { 803 updateVolumes(); 804 } 805 }); 806 updateVolumes(); 807 808 attachVolume(MediaStore.VOLUME_INTERNAL); 809 810 // Attach all currently mounted external volumes 811 for (String volumeName : getExternalVolumeNames()) { 812 attachVolume(volumeName); 813 } 814 815 // Watch for performance-sensitive activity 816 mAppOpsManager.startWatchingActive(new int[] { 817 AppOpsManager.OP_CAMERA 818 }, mActiveListener); 819 820 return true; 821 } 822 823 @Override onCallingPackageChanged()824 public void onCallingPackageChanged() { 825 // Identity of the current thread has changed, so invalidate caches 826 mCallingIdentity.remove(); 827 } 828 clearLocalCallingIdentity()829 public LocalCallingIdentity clearLocalCallingIdentity() { 830 final LocalCallingIdentity token = mCallingIdentity.get(); 831 mCallingIdentity.set(LocalCallingIdentity.fromSelf()); 832 return token; 833 } 834 restoreLocalCallingIdentity(LocalCallingIdentity token)835 public void restoreLocalCallingIdentity(LocalCallingIdentity token) { 836 mCallingIdentity.set(token); 837 } 838 onIdleMaintenance(@onNull CancellationSignal signal)839 public void onIdleMaintenance(@NonNull CancellationSignal signal) { 840 final DatabaseHelper helper = mExternalDatabase; 841 final SQLiteDatabase db = helper.getReadableDatabase(); 842 843 // Scan all volumes to resolve any staleness 844 for (String volumeName : getExternalVolumeNames()) { 845 // Possibly bail before digging into each volume 846 signal.throwIfCanceled(); 847 848 try { 849 final File file = getVolumePath(volumeName); 850 MediaService.onScanVolume(getContext(), Uri.fromFile(file)); 851 } catch (IOException e) { 852 Log.w(TAG, e); 853 } 854 } 855 856 // Delete any stale thumbnails 857 pruneThumbnails(signal); 858 859 // Finished orphaning any content whose package no longer exists 860 final ArraySet<String> unknownPackages = new ArraySet<>(); 861 try (Cursor c = db.query(true, "files", new String[] { "owner_package_name" }, 862 null, null, null, null, null, null, signal)) { 863 while (c.moveToNext()) { 864 final String packageName = c.getString(0); 865 if (TextUtils.isEmpty(packageName)) continue; 866 try { 867 getContext().getPackageManager().getPackageInfo(packageName, 868 PackageManager.MATCH_UNINSTALLED_PACKAGES); 869 } catch (NameNotFoundException e) { 870 unknownPackages.add(packageName); 871 } 872 } 873 } 874 875 Log.d(TAG, "Found " + unknownPackages.size() + " unknown packages"); 876 for (String packageName : unknownPackages) { 877 onPackageOrphaned(packageName); 878 } 879 880 // Delete any expired content; we're paranoid about wildly changing 881 // clocks, so only delete items within the last week 882 final long from = ((System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS) / 1000); 883 final long to = (System.currentTimeMillis() / 1000); 884 try (Cursor c = db.query(true, "files", new String[] { "volume_name", "_id" }, 885 FileColumns.DATE_EXPIRES + " BETWEEN " + from + " AND " + to, null, 886 null, null, null, null, signal)) { 887 while (c.moveToNext()) { 888 final String volumeName = c.getString(0); 889 final long id = c.getLong(1); 890 delete(Files.getContentUri(volumeName, id), null, null); 891 } 892 Log.d(TAG, "Deleted " + c.getCount() + " expired items on " + helper.mName); 893 } 894 895 // Forget any stale volumes 896 final long lastWeek = System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS; 897 for (VolumeRecord rec : mStorageManager.getVolumeRecords()) { 898 // Skip volumes without valid UUIDs 899 if (TextUtils.isEmpty(rec.fsUuid)) continue; 900 901 // Skip volumes that are currently mounted 902 final VolumeInfo vol = mStorageManager.findVolumeByUuid(rec.fsUuid); 903 if (vol != null && vol.isMountedReadable()) continue; 904 905 if (rec.lastSeenMillis > 0 && rec.lastSeenMillis < lastWeek) { 906 final int num = db.delete("files", FileColumns.VOLUME_NAME + "=?", 907 new String[] { rec.getNormalizedFsUuid() }); 908 Log.d(TAG, "Forgot " + num + " stale items from " + rec.fsUuid); 909 } 910 } 911 } 912 onPackageOrphaned(String packageName)913 public void onPackageOrphaned(String packageName) { 914 final DatabaseHelper helper = mExternalDatabase; 915 final SQLiteDatabase db = helper.getWritableDatabase(); 916 917 final ContentValues values = new ContentValues(); 918 values.putNull(FileColumns.OWNER_PACKAGE_NAME); 919 920 final int count = db.update("files", values, 921 "owner_package_name=?", new String[] { packageName }); 922 if (count > 0) { 923 Log.d(TAG, "Orphaned " + count + " items belonging to " 924 + packageName + " on " + helper.mName); 925 } 926 } 927 enforceShellRestrictions()928 private void enforceShellRestrictions() { 929 if (UserHandle.getCallingAppId() == android.os.Process.SHELL_UID 930 && getContext().getSystemService(UserManager.class) 931 .hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) { 932 throw new SecurityException( 933 "Shell user cannot access files for user " + UserHandle.myUserId()); 934 } 935 } 936 937 @Override enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)938 protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken) 939 throws SecurityException { 940 enforceShellRestrictions(); 941 return super.enforceReadPermissionInner(uri, callingPkg, callerToken); 942 } 943 944 @Override enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken)945 protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken) 946 throws SecurityException { 947 enforceShellRestrictions(); 948 return super.enforceWritePermissionInner(uri, callingPkg, callerToken); 949 } 950 951 @VisibleForTesting makePristineSchema(SQLiteDatabase db)952 static void makePristineSchema(SQLiteDatabase db) { 953 // drop all triggers 954 Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'trigger'", 955 null, null, null, null); 956 while (c.moveToNext()) { 957 if (c.getString(0).startsWith("sqlite_")) continue; 958 db.execSQL("DROP TRIGGER IF EXISTS " + c.getString(0)); 959 } 960 c.close(); 961 962 // drop all views 963 c = db.query("sqlite_master", new String[] {"name"}, "type is 'view'", 964 null, null, null, null); 965 while (c.moveToNext()) { 966 if (c.getString(0).startsWith("sqlite_")) continue; 967 db.execSQL("DROP VIEW IF EXISTS " + c.getString(0)); 968 } 969 c.close(); 970 971 // drop all indexes 972 c = db.query("sqlite_master", new String[] {"name"}, "type is 'index'", 973 null, null, null, null); 974 while (c.moveToNext()) { 975 if (c.getString(0).startsWith("sqlite_")) continue; 976 db.execSQL("DROP INDEX IF EXISTS " + c.getString(0)); 977 } 978 c.close(); 979 980 // drop all tables 981 c = db.query("sqlite_master", new String[] {"name"}, "type is 'table'", 982 null, null, null, null); 983 while (c.moveToNext()) { 984 if (c.getString(0).startsWith("sqlite_")) continue; 985 db.execSQL("DROP TABLE IF EXISTS " + c.getString(0)); 986 } 987 c.close(); 988 } 989 createLatestSchema(SQLiteDatabase db, boolean internal)990 private static void createLatestSchema(SQLiteDatabase db, boolean internal) { 991 // We're about to start all ID numbering from scratch, so revoke any 992 // outstanding permission grants to ensure we don't leak data 993 AppGlobals.getInitialApplication().revokeUriPermission(MediaStore.AUTHORITY_URI, 994 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 995 MediaDocumentsProvider.revokeAllUriGrants(AppGlobals.getInitialApplication()); 996 BackgroundThread.getHandler().post(() -> { 997 try (ContentProviderClient client = AppGlobals.getInitialApplication() 998 .getContentResolver().acquireContentProviderClient( 999 android.provider.Downloads.Impl.AUTHORITY)) { 1000 client.call(android.provider.Downloads.CALL_REVOKE_MEDIASTORE_URI_PERMS, 1001 null, null); 1002 } catch (RemoteException e) { 1003 // Should not happen 1004 } 1005 }); 1006 1007 makePristineSchema(db); 1008 1009 db.execSQL("CREATE TABLE android_metadata (locale TEXT)"); 1010 db.execSQL("CREATE TABLE thumbnails (_id INTEGER PRIMARY KEY,_data TEXT,image_id INTEGER," 1011 + "kind INTEGER,width INTEGER,height INTEGER)"); 1012 db.execSQL("CREATE TABLE artists (artist_id INTEGER PRIMARY KEY," 1013 + "artist_key TEXT NOT NULL UNIQUE,artist TEXT NOT NULL)"); 1014 db.execSQL("CREATE TABLE albums (album_id INTEGER PRIMARY KEY," 1015 + "album_key TEXT NOT NULL UNIQUE,album TEXT NOT NULL)"); 1016 db.execSQL("CREATE TABLE album_art (album_id INTEGER PRIMARY KEY,_data TEXT)"); 1017 db.execSQL("CREATE TABLE videothumbnails (_id INTEGER PRIMARY KEY,_data TEXT," 1018 + "video_id INTEGER,kind INTEGER,width INTEGER,height INTEGER)"); 1019 db.execSQL("CREATE TABLE files (_id INTEGER PRIMARY KEY AUTOINCREMENT," 1020 + "_data TEXT UNIQUE COLLATE NOCASE,_size INTEGER,format INTEGER,parent INTEGER," 1021 + "date_added INTEGER,date_modified INTEGER,mime_type TEXT,title TEXT," 1022 + "description TEXT,_display_name TEXT,picasa_id TEXT,orientation INTEGER," 1023 + "latitude DOUBLE,longitude DOUBLE,datetaken INTEGER,mini_thumb_magic INTEGER," 1024 + "bucket_id TEXT,bucket_display_name TEXT,isprivate INTEGER,title_key TEXT," 1025 + "artist_id INTEGER,album_id INTEGER,composer TEXT,track INTEGER," 1026 + "year INTEGER CHECK(year!=0),is_ringtone INTEGER,is_music INTEGER," 1027 + "is_alarm INTEGER,is_notification INTEGER,is_podcast INTEGER,album_artist TEXT," 1028 + "duration INTEGER,bookmark INTEGER,artist TEXT,album TEXT,resolution TEXT," 1029 + "tags TEXT,category TEXT,language TEXT,mini_thumb_data TEXT,name TEXT," 1030 + "media_type INTEGER,old_id INTEGER,is_drm INTEGER," 1031 + "width INTEGER, height INTEGER, title_resource_uri TEXT," 1032 + "owner_package_name TEXT DEFAULT NULL," 1033 + "color_standard INTEGER, color_transfer INTEGER, color_range INTEGER," 1034 + "_hash BLOB DEFAULT NULL, is_pending INTEGER DEFAULT 0," 1035 + "is_download INTEGER DEFAULT 0, download_uri TEXT DEFAULT NULL," 1036 + "referer_uri TEXT DEFAULT NULL, is_audiobook INTEGER DEFAULT 0," 1037 + "date_expires INTEGER DEFAULT NULL,is_trashed INTEGER DEFAULT 0," 1038 + "group_id INTEGER DEFAULT NULL,primary_directory TEXT DEFAULT NULL," 1039 + "secondary_directory TEXT DEFAULT NULL,document_id TEXT DEFAULT NULL," 1040 + "instance_id TEXT DEFAULT NULL,original_document_id TEXT DEFAULT NULL," 1041 + "relative_path TEXT DEFAULT NULL,volume_name TEXT DEFAULT NULL)"); 1042 1043 db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)"); 1044 if (!internal) { 1045 db.execSQL("CREATE TABLE audio_genres (_id INTEGER PRIMARY KEY,name TEXT NOT NULL)"); 1046 db.execSQL("CREATE TABLE audio_genres_map (_id INTEGER PRIMARY KEY," 1047 + "audio_id INTEGER NOT NULL,genre_id INTEGER NOT NULL," 1048 + "UNIQUE (audio_id,genre_id) ON CONFLICT IGNORE)"); 1049 db.execSQL("CREATE TABLE audio_playlists_map (_id INTEGER PRIMARY KEY," 1050 + "audio_id INTEGER NOT NULL,playlist_id INTEGER NOT NULL," 1051 + "play_order INTEGER NOT NULL)"); 1052 db.execSQL("CREATE TRIGGER audio_genres_cleanup DELETE ON audio_genres BEGIN DELETE" 1053 + " FROM audio_genres_map WHERE genre_id = old._id;END"); 1054 db.execSQL("CREATE TRIGGER audio_playlists_cleanup DELETE ON files" 1055 + " WHEN old.media_type=4" 1056 + " BEGIN DELETE FROM audio_playlists_map WHERE playlist_id = old._id;" 1057 + "SELECT _DELETE_FILE(old._data);END"); 1058 db.execSQL("CREATE TRIGGER files_cleanup DELETE ON files" 1059 + " BEGIN SELECT _OBJECT_REMOVED(old._id);END"); 1060 } 1061 1062 db.execSQL("CREATE INDEX image_id_index on thumbnails(image_id)"); 1063 db.execSQL("CREATE INDEX album_idx on albums(album)"); 1064 db.execSQL("CREATE INDEX albumkey_index on albums(album_key)"); 1065 db.execSQL("CREATE INDEX artist_idx on artists(artist)"); 1066 db.execSQL("CREATE INDEX artistkey_index on artists(artist_key)"); 1067 db.execSQL("CREATE INDEX video_id_index on videothumbnails(video_id)"); 1068 db.execSQL("CREATE INDEX album_id_idx ON files(album_id)"); 1069 db.execSQL("CREATE INDEX artist_id_idx ON files(artist_id)"); 1070 db.execSQL("CREATE INDEX bucket_index on files(bucket_id,media_type,datetaken, _id)"); 1071 db.execSQL("CREATE INDEX bucket_name on files(bucket_id,media_type,bucket_display_name)"); 1072 db.execSQL("CREATE INDEX format_index ON files(format)"); 1073 db.execSQL("CREATE INDEX media_type_index ON files(media_type)"); 1074 db.execSQL("CREATE INDEX parent_index ON files(parent)"); 1075 db.execSQL("CREATE INDEX path_index ON files(_data)"); 1076 db.execSQL("CREATE INDEX sort_index ON files(datetaken ASC, _id ASC)"); 1077 db.execSQL("CREATE INDEX title_idx ON files(title)"); 1078 db.execSQL("CREATE INDEX titlekey_index ON files(title_key)"); 1079 1080 db.execSQL("CREATE TRIGGER albumart_cleanup1 DELETE ON albums BEGIN DELETE FROM album_art" 1081 + " WHERE album_id = old.album_id;END"); 1082 db.execSQL("CREATE TRIGGER albumart_cleanup2 DELETE ON album_art" 1083 + " BEGIN SELECT _DELETE_FILE(old._data);END"); 1084 1085 createLatestViews(db, internal); 1086 } 1087 makePristineViews(SQLiteDatabase db)1088 private static void makePristineViews(SQLiteDatabase db) { 1089 // drop all views 1090 Cursor c = db.query("sqlite_master", new String[] {"name"}, "type is 'view'", 1091 null, null, null, null); 1092 while (c.moveToNext()) { 1093 db.execSQL("DROP VIEW IF EXISTS " + c.getString(0)); 1094 } 1095 c.close(); 1096 } 1097 createLatestViews(SQLiteDatabase db, boolean internal)1098 private static void createLatestViews(SQLiteDatabase db, boolean internal) { 1099 makePristineViews(db); 1100 1101 if (!internal) { 1102 db.execSQL("CREATE VIEW audio_playlists AS SELECT _id,_data,name,date_added," 1103 + "date_modified,owner_package_name,_hash,is_pending,date_expires,is_trashed," 1104 + "volume_name FROM files WHERE media_type=4"); 1105 } 1106 1107 db.execSQL("CREATE VIEW audio_meta AS SELECT _id,_data,_display_name,_size,mime_type," 1108 + "date_added,is_drm,date_modified,title,title_key,duration,artist_id,composer," 1109 + "album_id,track,year,is_ringtone,is_music,is_alarm,is_notification,is_podcast," 1110 + "bookmark,album_artist,owner_package_name,_hash,is_pending,is_audiobook," 1111 + "date_expires,is_trashed,group_id,primary_directory,secondary_directory," 1112 + "document_id,instance_id,original_document_id,title_resource_uri,relative_path," 1113 + "volume_name,datetaken,bucket_id,bucket_display_name,group_id,orientation" 1114 + " FROM files WHERE media_type=2"); 1115 1116 db.execSQL("CREATE VIEW artists_albums_map AS SELECT DISTINCT artist_id, album_id" 1117 + " FROM audio_meta"); 1118 db.execSQL("CREATE VIEW audio as SELECT *, NULL AS width, NULL as height" 1119 + " FROM audio_meta LEFT OUTER JOIN artists" 1120 + " ON audio_meta.artist_id=artists.artist_id LEFT OUTER JOIN albums" 1121 + " ON audio_meta.album_id=albums.album_id"); 1122 db.execSQL("CREATE VIEW album_info AS SELECT audio.album_id AS _id, album, album_key," 1123 + " MIN(year) AS minyear, MAX(year) AS maxyear, artist, artist_id, artist_key," 1124 + " count(*) AS numsongs,album_art._data AS album_art FROM audio" 1125 + " LEFT OUTER JOIN album_art ON audio.album_id=album_art.album_id WHERE is_music=1" 1126 + " GROUP BY audio.album_id"); 1127 db.execSQL("CREATE VIEW searchhelpertitle AS SELECT * FROM audio ORDER BY title_key"); 1128 db.execSQL("CREATE VIEW artist_info AS SELECT artist_id AS _id, artist, artist_key," 1129 + " COUNT(DISTINCT album_key) AS number_of_albums, COUNT(*) AS number_of_tracks" 1130 + " FROM audio" 1131 + " WHERE is_music=1 GROUP BY artist_key"); 1132 db.execSQL("CREATE VIEW search AS SELECT _id,'artist' AS mime_type,artist,NULL AS album," 1133 + "NULL AS title,artist AS text1,NULL AS text2,number_of_albums AS data1," 1134 + "number_of_tracks AS data2,artist_key AS match," 1135 + "'content://media/external/audio/artists/'||_id AS suggest_intent_data," 1136 + "1 AS grouporder FROM artist_info WHERE (artist!='<unknown>')" 1137 + " UNION ALL SELECT _id,'album' AS mime_type,artist,album," 1138 + "NULL AS title,album AS text1,artist AS text2,NULL AS data1," 1139 + "NULL AS data2,artist_key||' '||album_key AS match," 1140 + "'content://media/external/audio/albums/'||_id AS suggest_intent_data," 1141 + "2 AS grouporder FROM album_info" 1142 + " WHERE (album!='<unknown>')" 1143 + " UNION ALL SELECT searchhelpertitle._id AS _id,mime_type,artist,album,title," 1144 + "title AS text1,artist AS text2,NULL AS data1," 1145 + "NULL AS data2,artist_key||' '||album_key||' '||title_key AS match," 1146 + "'content://media/external/audio/media/'||searchhelpertitle._id" 1147 + " AS suggest_intent_data," 1148 + "3 AS grouporder FROM searchhelpertitle WHERE (title != '')"); 1149 db.execSQL("CREATE VIEW audio_genres_map_noid AS SELECT audio_id,genre_id" 1150 + " FROM audio_genres_map"); 1151 1152 db.execSQL("CREATE VIEW video AS SELECT " 1153 + String.join(",", getProjectionMap(Video.Media.class).keySet()) 1154 + " FROM files WHERE media_type=3"); 1155 db.execSQL("CREATE VIEW images AS SELECT " 1156 + String.join(",", getProjectionMap(Images.Media.class).keySet()) 1157 + " FROM files WHERE media_type=1"); 1158 db.execSQL("CREATE VIEW downloads AS SELECT " 1159 + String.join(",", getProjectionMap(Downloads.class).keySet()) 1160 + " FROM files WHERE is_download=1"); 1161 } 1162 updateCollationKeys(SQLiteDatabase db)1163 private static void updateCollationKeys(SQLiteDatabase db) { 1164 // Delete albums and artists, then clear the modification time on songs, which 1165 // will cause the media scanner to rescan everything, rebuilding the artist and 1166 // album tables along the way, while preserving playlists. 1167 // We need this rescan because ICU also changed, and now generates different 1168 // collation keys 1169 db.execSQL("DELETE from albums"); 1170 db.execSQL("DELETE from artists"); 1171 db.execSQL("UPDATE files SET date_modified=0;"); 1172 } 1173 updateAddTitleResource(SQLiteDatabase db)1174 private static void updateAddTitleResource(SQLiteDatabase db) { 1175 // Add the column used for title localization, and force a rescan of any 1176 // ringtones, alarms and notifications that may be using it. 1177 db.execSQL("ALTER TABLE files ADD COLUMN title_resource_uri TEXT"); 1178 db.execSQL("UPDATE files SET date_modified=0" 1179 + " WHERE (is_alarm IS 1) OR (is_ringtone IS 1) OR (is_notification IS 1)"); 1180 } 1181 updateAddOwnerPackageName(SQLiteDatabase db, boolean internal)1182 private static void updateAddOwnerPackageName(SQLiteDatabase db, boolean internal) { 1183 db.execSQL("ALTER TABLE files ADD COLUMN owner_package_name TEXT DEFAULT NULL"); 1184 1185 // Derive new column value based on well-known paths 1186 try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA }, 1187 FileColumns.DATA + " REGEXP '" + PATTERN_OWNED_PATH.pattern() + "'", 1188 null, null, null, null, null)) { 1189 Log.d(TAG, "Updating " + c.getCount() + " entries with well-known owners"); 1190 1191 final Matcher m = PATTERN_OWNED_PATH.matcher(""); 1192 final ContentValues values = new ContentValues(); 1193 1194 while (c.moveToNext()) { 1195 final long id = c.getLong(0); 1196 final String data = c.getString(1); 1197 m.reset(data); 1198 if (m.matches()) { 1199 final String packageName = m.group(1); 1200 values.clear(); 1201 values.put(FileColumns.OWNER_PACKAGE_NAME, packageName); 1202 db.update("files", values, "_id=" + id, null); 1203 } 1204 } 1205 } 1206 } 1207 updateAddColorSpaces(SQLiteDatabase db)1208 private static void updateAddColorSpaces(SQLiteDatabase db) { 1209 // Add the color aspects related column used for HDR detection etc. 1210 db.execSQL("ALTER TABLE files ADD COLUMN color_standard INTEGER;"); 1211 db.execSQL("ALTER TABLE files ADD COLUMN color_transfer INTEGER;"); 1212 db.execSQL("ALTER TABLE files ADD COLUMN color_range INTEGER;"); 1213 } 1214 updateAddHashAndPending(SQLiteDatabase db, boolean internal)1215 private static void updateAddHashAndPending(SQLiteDatabase db, boolean internal) { 1216 db.execSQL("ALTER TABLE files ADD COLUMN _hash BLOB DEFAULT NULL;"); 1217 db.execSQL("ALTER TABLE files ADD COLUMN is_pending INTEGER DEFAULT 0;"); 1218 } 1219 updateAddDownloadInfo(SQLiteDatabase db, boolean internal)1220 private static void updateAddDownloadInfo(SQLiteDatabase db, boolean internal) { 1221 db.execSQL("ALTER TABLE files ADD COLUMN is_download INTEGER DEFAULT 0;"); 1222 db.execSQL("ALTER TABLE files ADD COLUMN download_uri TEXT DEFAULT NULL;"); 1223 db.execSQL("ALTER TABLE files ADD COLUMN referer_uri TEXT DEFAULT NULL;"); 1224 } 1225 updateAddAudiobook(SQLiteDatabase db, boolean internal)1226 private static void updateAddAudiobook(SQLiteDatabase db, boolean internal) { 1227 db.execSQL("ALTER TABLE files ADD COLUMN is_audiobook INTEGER DEFAULT 0;"); 1228 } 1229 updateClearLocation(SQLiteDatabase db, boolean internal)1230 private static void updateClearLocation(SQLiteDatabase db, boolean internal) { 1231 db.execSQL("UPDATE files SET latitude=NULL, longitude=NULL;"); 1232 } 1233 updateSetIsDownload(SQLiteDatabase db, boolean internal)1234 private static void updateSetIsDownload(SQLiteDatabase db, boolean internal) { 1235 db.execSQL("UPDATE files SET is_download=1 WHERE _data REGEXP '" 1236 + PATTERN_DOWNLOADS_FILE + "'"); 1237 } 1238 updateAddExpiresAndTrashed(SQLiteDatabase db, boolean internal)1239 private static void updateAddExpiresAndTrashed(SQLiteDatabase db, boolean internal) { 1240 db.execSQL("ALTER TABLE files ADD COLUMN date_expires INTEGER DEFAULT NULL;"); 1241 db.execSQL("ALTER TABLE files ADD COLUMN is_trashed INTEGER DEFAULT 0;"); 1242 } 1243 updateAddGroupId(SQLiteDatabase db, boolean internal)1244 private static void updateAddGroupId(SQLiteDatabase db, boolean internal) { 1245 db.execSQL("ALTER TABLE files ADD COLUMN group_id INTEGER DEFAULT NULL;"); 1246 } 1247 updateAddDirectories(SQLiteDatabase db, boolean internal)1248 private static void updateAddDirectories(SQLiteDatabase db, boolean internal) { 1249 db.execSQL("ALTER TABLE files ADD COLUMN primary_directory TEXT DEFAULT NULL;"); 1250 db.execSQL("ALTER TABLE files ADD COLUMN secondary_directory TEXT DEFAULT NULL;"); 1251 } 1252 updateAddXmp(SQLiteDatabase db, boolean internal)1253 private static void updateAddXmp(SQLiteDatabase db, boolean internal) { 1254 db.execSQL("ALTER TABLE files ADD COLUMN document_id TEXT DEFAULT NULL;"); 1255 db.execSQL("ALTER TABLE files ADD COLUMN instance_id TEXT DEFAULT NULL;"); 1256 db.execSQL("ALTER TABLE files ADD COLUMN original_document_id TEXT DEFAULT NULL;"); 1257 } 1258 updateAddPath(SQLiteDatabase db, boolean internal)1259 private static void updateAddPath(SQLiteDatabase db, boolean internal) { 1260 db.execSQL("ALTER TABLE files ADD COLUMN relative_path TEXT DEFAULT NULL;"); 1261 } 1262 updateAddVolumeName(SQLiteDatabase db, boolean internal)1263 private static void updateAddVolumeName(SQLiteDatabase db, boolean internal) { 1264 db.execSQL("ALTER TABLE files ADD COLUMN volume_name TEXT DEFAULT NULL;"); 1265 } 1266 updateDirsMimeType(SQLiteDatabase db, boolean internal)1267 private static void updateDirsMimeType(SQLiteDatabase db, boolean internal) { 1268 db.execSQL("UPDATE files SET mime_type=NULL WHERE format=" 1269 + MtpConstants.FORMAT_ASSOCIATION); 1270 } 1271 updateRelativePath(SQLiteDatabase db, boolean internal)1272 private static void updateRelativePath(SQLiteDatabase db, boolean internal) { 1273 db.execSQL("UPDATE files" 1274 + " SET " + MediaColumns.RELATIVE_PATH + "=" + MediaColumns.RELATIVE_PATH + "||'/'" 1275 + " WHERE " + MediaColumns.RELATIVE_PATH + " IS NOT NULL" 1276 + " AND " + MediaColumns.RELATIVE_PATH + " NOT LIKE '%/';"); 1277 } 1278 recomputeDataValues(SQLiteDatabase db, boolean internal)1279 private static void recomputeDataValues(SQLiteDatabase db, boolean internal) { 1280 try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA }, 1281 null, null, null, null, null, null)) { 1282 Log.d(TAG, "Recomputing " + c.getCount() + " data values"); 1283 1284 final ContentValues values = new ContentValues(); 1285 while (c.moveToNext()) { 1286 values.clear(); 1287 final long id = c.getLong(0); 1288 final String data = c.getString(1); 1289 values.put(FileColumns.DATA, data); 1290 computeDataValues(values); 1291 values.remove(FileColumns.DATA); 1292 if (!values.isEmpty()) { 1293 db.update("files", values, "_id=" + id, null); 1294 } 1295 } 1296 } 1297 } 1298 1299 static final int VERSION_J = 509; 1300 static final int VERSION_K = 700; 1301 static final int VERSION_L = 700; 1302 static final int VERSION_M = 800; 1303 static final int VERSION_N = 800; 1304 static final int VERSION_O = 800; 1305 static final int VERSION_P = 900; 1306 static final int VERSION_Q = 1023; 1307 1308 /** 1309 * This method takes care of updating all the tables in the database to the 1310 * current version, creating them if necessary. 1311 * This method can only update databases at schema 700 or higher, which was 1312 * used by the KitKat release. Older database will be cleared and recreated. 1313 * @param db Database 1314 * @param internal True if this is the internal media database 1315 */ updateDatabase(Context context, SQLiteDatabase db, boolean internal, int fromVersion, int toVersion)1316 private static void updateDatabase(Context context, SQLiteDatabase db, boolean internal, 1317 int fromVersion, int toVersion) { 1318 final long startTime = SystemClock.elapsedRealtime(); 1319 1320 if (fromVersion < 700) { 1321 // Anything older than KK is recreated from scratch 1322 createLatestSchema(db, internal); 1323 } else { 1324 boolean recomputeDataValues = false; 1325 if (fromVersion < 800) { 1326 updateCollationKeys(db); 1327 } 1328 if (fromVersion < 900) { 1329 updateAddTitleResource(db); 1330 } 1331 if (fromVersion < 1000) { 1332 updateAddOwnerPackageName(db, internal); 1333 } 1334 if (fromVersion < 1003) { 1335 updateAddColorSpaces(db); 1336 } 1337 if (fromVersion < 1004) { 1338 updateAddHashAndPending(db, internal); 1339 } 1340 if (fromVersion < 1005) { 1341 updateAddDownloadInfo(db, internal); 1342 } 1343 if (fromVersion < 1006) { 1344 updateAddAudiobook(db, internal); 1345 } 1346 if (fromVersion < 1007) { 1347 updateClearLocation(db, internal); 1348 } 1349 if (fromVersion < 1008) { 1350 updateSetIsDownload(db, internal); 1351 } 1352 if (fromVersion < 1009) { 1353 // This database version added "secondary_bucket_id", but that 1354 // column name was refactored in version 1013 below, so this 1355 // update step is no longer needed. 1356 } 1357 if (fromVersion < 1010) { 1358 updateAddExpiresAndTrashed(db, internal); 1359 } 1360 if (fromVersion < 1012) { 1361 recomputeDataValues = true; 1362 } 1363 if (fromVersion < 1013) { 1364 updateAddGroupId(db, internal); 1365 updateAddDirectories(db, internal); 1366 recomputeDataValues = true; 1367 } 1368 if (fromVersion < 1014) { 1369 updateAddXmp(db, internal); 1370 } 1371 if (fromVersion < 1015) { 1372 // Empty version bump to ensure views are recreated 1373 } 1374 if (fromVersion < 1016) { 1375 // Empty version bump to ensure views are recreated 1376 } 1377 if (fromVersion < 1017) { 1378 updateSetIsDownload(db, internal); 1379 recomputeDataValues = true; 1380 } 1381 if (fromVersion < 1018) { 1382 updateAddPath(db, internal); 1383 recomputeDataValues = true; 1384 } 1385 if (fromVersion < 1019) { 1386 // Only trigger during "external", so that it runs only once. 1387 if (!internal) { 1388 deleteLegacyThumbnailData(); 1389 } 1390 } 1391 if (fromVersion < 1020) { 1392 updateAddVolumeName(db, internal); 1393 recomputeDataValues = true; 1394 } 1395 if (fromVersion < 1021) { 1396 // Empty version bump to ensure views are recreated 1397 } 1398 if (fromVersion < 1022) { 1399 updateDirsMimeType(db, internal); 1400 } 1401 if (fromVersion < 1023) { 1402 updateRelativePath(db, internal); 1403 } 1404 1405 if (recomputeDataValues) { 1406 recomputeDataValues(db, internal); 1407 } 1408 } 1409 1410 // Always recreate latest views during upgrade; they're cheap and it's 1411 // an easy way to ensure they're defined consistently 1412 createLatestViews(db, internal); 1413 1414 sanityCheck(db, fromVersion); 1415 1416 getOrCreateUuid(db); 1417 1418 final long elapsedSeconds = (SystemClock.elapsedRealtime() - startTime) 1419 / DateUtils.SECOND_IN_MILLIS; 1420 logToDb(db, "Database upgraded from version " + fromVersion + " to " + toVersion 1421 + " in " + elapsedSeconds + " seconds"); 1422 } 1423 downgradeDatabase(Context context, SQLiteDatabase db, boolean internal, int fromVersion, int toVersion)1424 private static void downgradeDatabase(Context context, SQLiteDatabase db, boolean internal, 1425 int fromVersion, int toVersion) { 1426 final long startTime = SystemClock.elapsedRealtime(); 1427 1428 // The best we can do is wipe and start over 1429 createLatestSchema(db, internal); 1430 1431 final long elapsedSeconds = (SystemClock.elapsedRealtime() - startTime) 1432 / DateUtils.SECOND_IN_MILLIS; 1433 logToDb(db, "Database downgraded from version " + fromVersion + " to " + toVersion 1434 + " in " + elapsedSeconds + " seconds"); 1435 } 1436 1437 /** 1438 * Write a persistent diagnostic message to the log table. 1439 */ logToDb(SQLiteDatabase db, String message)1440 static void logToDb(SQLiteDatabase db, String message) { 1441 db.execSQL("INSERT OR REPLACE" + 1442 " INTO log (time,message) VALUES (strftime('%Y-%m-%d %H:%M:%f','now'),?);", 1443 new String[] { message }); 1444 // delete all but the last 500 rows 1445 db.execSQL("DELETE FROM log WHERE rowid IN" + 1446 " (SELECT rowid FROM log ORDER BY rowid DESC LIMIT 500,-1);"); 1447 } 1448 1449 /** 1450 * Perform a simple sanity check on the database. Currently this tests 1451 * whether all the _data entries in audio_meta are unique 1452 */ sanityCheck(SQLiteDatabase db, int fromVersion)1453 private static void sanityCheck(SQLiteDatabase db, int fromVersion) { 1454 Cursor c1 = null; 1455 Cursor c2 = null; 1456 try { 1457 c1 = db.query("audio_meta", new String[] {"count(*)"}, 1458 null, null, null, null, null); 1459 c2 = db.query("audio_meta", new String[] {"count(distinct _data)"}, 1460 null, null, null, null, null); 1461 c1.moveToFirst(); 1462 c2.moveToFirst(); 1463 int num1 = c1.getInt(0); 1464 int num2 = c2.getInt(0); 1465 if (num1 != num2) { 1466 Log.e(TAG, "audio_meta._data column is not unique while upgrading" + 1467 " from schema " +fromVersion + " : " + num1 +"/" + num2); 1468 // Delete all audio_meta rows so they will be rebuilt by the media scanner 1469 db.execSQL("DELETE FROM audio_meta;"); 1470 } 1471 } finally { 1472 IoUtils.closeQuietly(c1); 1473 IoUtils.closeQuietly(c2); 1474 } 1475 } 1476 1477 private static final String XATTR_UUID = "user.uuid"; 1478 1479 /** 1480 * Return a UUID for the given database. If the database is deleted or 1481 * otherwise corrupted, then a new UUID will automatically be generated. 1482 */ getOrCreateUuid(@onNull SQLiteDatabase db)1483 private static @NonNull String getOrCreateUuid(@NonNull SQLiteDatabase db) { 1484 try { 1485 return new String(Os.getxattr(db.getPath(), XATTR_UUID)); 1486 } catch (ErrnoException e) { 1487 if (e.errno == OsConstants.ENODATA) { 1488 // Doesn't exist yet, so generate and persist a UUID 1489 final String uuid = UUID.randomUUID().toString(); 1490 try { 1491 Os.setxattr(db.getPath(), XATTR_UUID, uuid.getBytes(), 0); 1492 } catch (ErrnoException e2) { 1493 throw new RuntimeException(e); 1494 } 1495 return uuid; 1496 } else { 1497 throw new RuntimeException(e); 1498 } 1499 } 1500 } 1501 1502 @VisibleForTesting computeDataValues(ContentValues values)1503 static void computeDataValues(ContentValues values) { 1504 // Worst case we have to assume no bucket details 1505 values.remove(ImageColumns.BUCKET_ID); 1506 values.remove(ImageColumns.BUCKET_DISPLAY_NAME); 1507 values.remove(ImageColumns.GROUP_ID); 1508 values.remove(ImageColumns.VOLUME_NAME); 1509 values.remove(ImageColumns.RELATIVE_PATH); 1510 values.remove(ImageColumns.PRIMARY_DIRECTORY); 1511 values.remove(ImageColumns.SECONDARY_DIRECTORY); 1512 1513 final String data = values.getAsString(MediaColumns.DATA); 1514 if (TextUtils.isEmpty(data)) return; 1515 1516 final File file = new File(data); 1517 final File fileLower = new File(data.toLowerCase()); 1518 1519 values.put(ImageColumns.VOLUME_NAME, extractVolumeName(data)); 1520 values.put(ImageColumns.RELATIVE_PATH, extractRelativePath(data)); 1521 values.put(ImageColumns.DISPLAY_NAME, extractDisplayName(data)); 1522 1523 // Buckets are the parent directory 1524 final String parent = fileLower.getParent(); 1525 if (parent != null) { 1526 values.put(ImageColumns.BUCKET_ID, parent.hashCode()); 1527 // The relative path for files in the top directory is "/" 1528 if (!"/".equals(values.getAsString(ImageColumns.RELATIVE_PATH))) { 1529 values.put(ImageColumns.BUCKET_DISPLAY_NAME, file.getParentFile().getName()); 1530 } 1531 } 1532 1533 // Groups are the first part of name 1534 final String name = fileLower.getName(); 1535 final int firstDot = name.indexOf('.'); 1536 if (firstDot > 0) { 1537 values.put(ImageColumns.GROUP_ID, 1538 name.substring(0, firstDot).hashCode()); 1539 } 1540 1541 // Directories are first two levels of storage paths 1542 final String relativePath = values.getAsString(ImageColumns.RELATIVE_PATH); 1543 if (TextUtils.isEmpty(relativePath)) return; 1544 1545 final String[] segments = relativePath.split("/"); 1546 if (segments.length > 0) { 1547 values.put(ImageColumns.PRIMARY_DIRECTORY, segments[0]); 1548 } 1549 if (segments.length > 1) { 1550 values.put(ImageColumns.SECONDARY_DIRECTORY, segments[1]); 1551 } 1552 } 1553 1554 @Override canonicalize(Uri uri)1555 public Uri canonicalize(Uri uri) { 1556 final boolean allowHidden = isCallingPackageAllowedHidden(); 1557 final int match = matchUri(uri, allowHidden); 1558 1559 // Skip when we have nothing to canonicalize 1560 if ("1".equals(uri.getQueryParameter(CANONICAL))) { 1561 return uri; 1562 } 1563 1564 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 1565 switch (match) { 1566 case AUDIO_MEDIA_ID: { 1567 final String title = getDefaultTitleFromCursor(c); 1568 if (!TextUtils.isEmpty(title)) { 1569 final Uri.Builder builder = uri.buildUpon(); 1570 builder.appendQueryParameter(AudioColumns.TITLE, title); 1571 builder.appendQueryParameter(CANONICAL, "1"); 1572 return builder.build(); 1573 } 1574 } 1575 case VIDEO_MEDIA_ID: 1576 case IMAGES_MEDIA_ID: { 1577 final String documentId = c 1578 .getString(c.getColumnIndexOrThrow(MediaColumns.DOCUMENT_ID)); 1579 if (!TextUtils.isEmpty(documentId)) { 1580 final Uri.Builder builder = uri.buildUpon(); 1581 builder.appendQueryParameter(MediaColumns.DOCUMENT_ID, documentId); 1582 builder.appendQueryParameter(CANONICAL, "1"); 1583 return builder.build(); 1584 } 1585 } 1586 } 1587 } catch (FileNotFoundException e) { 1588 Log.w(TAG, e.getMessage()); 1589 } 1590 return null; 1591 } 1592 1593 @Override uncanonicalize(Uri uri)1594 public Uri uncanonicalize(Uri uri) { 1595 final boolean allowHidden = isCallingPackageAllowedHidden(); 1596 final int match = matchUri(uri, allowHidden); 1597 1598 // Skip when we have nothing to uncanonicalize 1599 if (!"1".equals(uri.getQueryParameter(CANONICAL))) { 1600 return uri; 1601 } 1602 1603 // Extract values and then clear to avoid recursive lookups 1604 final String title = uri.getQueryParameter(AudioColumns.TITLE); 1605 final String documentId = uri.getQueryParameter(MediaColumns.DOCUMENT_ID); 1606 uri = uri.buildUpon().clearQuery().build(); 1607 1608 switch (match) { 1609 case AUDIO_MEDIA_ID: { 1610 // First check for an exact match 1611 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 1612 if (Objects.equals(title, getDefaultTitleFromCursor(c))) { 1613 return uri; 1614 } 1615 } catch (FileNotFoundException e) { 1616 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e); 1617 } 1618 1619 // Otherwise fallback to searching 1620 final Uri baseUri = ContentUris.removeId(uri); 1621 try (Cursor c = queryForSingleItem(baseUri, 1622 new String[] { BaseColumns._ID }, 1623 AudioColumns.TITLE + "=?", new String[] { title }, null)) { 1624 return ContentUris.withAppendedId(baseUri, c.getLong(0)); 1625 } catch (FileNotFoundException e) { 1626 Log.w(TAG, "Failed to resolve " + uri + ": " + e); 1627 return null; 1628 } 1629 } 1630 case VIDEO_MEDIA_ID: 1631 case IMAGES_MEDIA_ID: { 1632 // First check for an exact match 1633 try (Cursor c = queryForSingleItem(uri, null, null, null, null)) { 1634 if (Objects.equals(title, getDefaultTitleFromCursor(c))) { 1635 return uri; 1636 } 1637 } catch (FileNotFoundException e) { 1638 Log.w(TAG, "Trouble resolving " + uri + "; falling back to search: " + e); 1639 } 1640 1641 // Otherwise fallback to searching 1642 final Uri baseUri = ContentUris.removeId(uri); 1643 try (Cursor c = queryForSingleItem(baseUri, 1644 new String[] { BaseColumns._ID }, 1645 MediaColumns.DOCUMENT_ID + "=?", new String[] { documentId }, null)) { 1646 return ContentUris.withAppendedId(baseUri, c.getLong(0)); 1647 } catch (FileNotFoundException e) { 1648 Log.w(TAG, "Failed to resolve " + uri + ": " + e); 1649 return null; 1650 } 1651 } 1652 } 1653 1654 return uri; 1655 } 1656 safeUncanonicalize(Uri uri)1657 private Uri safeUncanonicalize(Uri uri) { 1658 Uri newUri = uncanonicalize(uri); 1659 if (newUri != null) { 1660 return newUri; 1661 } 1662 return uri; 1663 } 1664 1665 @Override query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)1666 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, 1667 String sortOrder) { 1668 return query(uri, projection, 1669 ContentResolver.createSqlQueryBundle(selection, selectionArgs, sortOrder), null); 1670 } 1671 1672 @Override query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)1673 public Cursor query(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal) { 1674 Trace.traceBegin(TRACE_TAG_DATABASE, "query"); 1675 try { 1676 return queryInternal(uri, projection, queryArgs, signal); 1677 } finally { 1678 Trace.traceEnd(TRACE_TAG_DATABASE); 1679 } 1680 } 1681 queryInternal(Uri uri, String[] projection, Bundle queryArgs, CancellationSignal signal)1682 private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs, 1683 CancellationSignal signal) { 1684 String selection = null; 1685 String[] selectionArgs = null; 1686 String sortOrder = null; 1687 1688 if (queryArgs != null) { 1689 selection = queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SELECTION); 1690 selectionArgs = queryArgs.getStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS); 1691 sortOrder = queryArgs.getString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER); 1692 if (sortOrder == null 1693 && queryArgs.containsKey(ContentResolver.QUERY_ARG_SORT_COLUMNS)) { 1694 sortOrder = ContentResolver.createSqlSortClause(queryArgs); 1695 } 1696 } 1697 1698 uri = safeUncanonicalize(uri); 1699 1700 final String volumeName = getVolumeName(uri); 1701 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 1702 final boolean allowHidden = isCallingPackageAllowedHidden(); 1703 final int table = matchUri(uri, allowHidden); 1704 1705 //Log.v(TAG, "query: uri="+uri+", selection="+selection); 1706 // handle MEDIA_SCANNER before calling getDatabaseForUri() 1707 if (table == MEDIA_SCANNER) { 1708 // create a cursor to return volume currently being scanned by the media scanner 1709 MatrixCursor c = new MatrixCursor(new String[] {MediaStore.MEDIA_SCANNER_VOLUME}); 1710 c.addRow(new String[] {mMediaScannerVolume}); 1711 return c; 1712 } 1713 1714 // Used temporarily (until we have unique media IDs) to get an identifier 1715 // for the current sd card, so that the music app doesn't have to use the 1716 // non-public getFatVolumeId method 1717 if (table == FS_ID) { 1718 MatrixCursor c = new MatrixCursor(new String[] {"fsid"}); 1719 c.addRow(new Integer[] {mVolumeId}); 1720 return c; 1721 } 1722 1723 if (table == VERSION) { 1724 MatrixCursor c = new MatrixCursor(new String[] {"version"}); 1725 c.addRow(new Integer[] {getDatabaseVersion(getContext())}); 1726 return c; 1727 } 1728 1729 final DatabaseHelper helper; 1730 final SQLiteDatabase db; 1731 try { 1732 helper = getDatabaseForUri(uri); 1733 db = helper.getReadableDatabase(); 1734 } catch (VolumeNotFoundException e) { 1735 return e.translateForQuery(targetSdkVersion); 1736 } 1737 1738 if (table == MTP_OBJECT_REFERENCES) { 1739 final int handle = Integer.parseInt(uri.getPathSegments().get(2)); 1740 return getObjectReferences(helper, db, handle); 1741 } 1742 1743 SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, uri, table, queryArgs); 1744 String limit = uri.getQueryParameter(MediaStore.PARAM_LIMIT); 1745 String filter = uri.getQueryParameter("filter"); 1746 String [] keywords = null; 1747 if (filter != null) { 1748 filter = Uri.decode(filter).trim(); 1749 if (!TextUtils.isEmpty(filter)) { 1750 String [] searchWords = filter.split(" "); 1751 keywords = new String[searchWords.length]; 1752 for (int i = 0; i < searchWords.length; i++) { 1753 String key = MediaStore.Audio.keyFor(searchWords[i]); 1754 key = key.replace("\\", "\\\\"); 1755 key = key.replace("%", "\\%"); 1756 key = key.replace("_", "\\_"); 1757 keywords[i] = key; 1758 } 1759 } 1760 } 1761 1762 String keywordColumn = null; 1763 switch (table) { 1764 case AUDIO_MEDIA: 1765 case AUDIO_GENRES_ALL_MEMBERS: 1766 case AUDIO_GENRES_ID_MEMBERS: 1767 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1768 case AUDIO_PLAYLISTS_ID_MEMBERS: 1769 keywordColumn = MediaStore.Audio.Media.ARTIST_KEY + 1770 "||" + MediaStore.Audio.Media.ALBUM_KEY + 1771 "||" + MediaStore.Audio.Media.TITLE_KEY; 1772 break; 1773 case AUDIO_ARTISTS_ID_ALBUMS: 1774 case AUDIO_ALBUMS: 1775 keywordColumn = MediaStore.Audio.Media.ARTIST_KEY + "||" 1776 + MediaStore.Audio.Media.ALBUM_KEY; 1777 break; 1778 case AUDIO_ARTISTS: 1779 keywordColumn = MediaStore.Audio.Media.ARTIST_KEY; 1780 break; 1781 } 1782 1783 if (keywordColumn != null) { 1784 for (int i = 0; keywords != null && i < keywords.length; i++) { 1785 appendWhereStandalone(qb, keywordColumn + " LIKE ? ESCAPE '\\'", 1786 "%" + keywords[i] + "%"); 1787 } 1788 } 1789 1790 String groupBy = null; 1791 if (table == AUDIO_ARTISTS_ID_ALBUMS) { 1792 groupBy = "audio.album_id"; 1793 } 1794 1795 if (getCallingPackageTargetSdkVersion() < Build.VERSION_CODES.Q) { 1796 // Some apps are abusing the "WHERE" clause by injecting "GROUP BY" 1797 // clauses; gracefully lift them out. 1798 final Pair<String, String> selectionAndGroupBy = recoverAbusiveGroupBy( 1799 Pair.create(selection, groupBy)); 1800 selection = selectionAndGroupBy.first; 1801 groupBy = selectionAndGroupBy.second; 1802 1803 // Some apps are abusing the first column to inject "DISTINCT"; 1804 // gracefully lift them out. 1805 if (!ArrayUtils.isEmpty(projection) && projection[0].startsWith("DISTINCT ")) { 1806 projection[0] = projection[0].substring("DISTINCT ".length()); 1807 qb.setDistinct(true); 1808 } 1809 1810 // Some apps are generating thumbnails with getThumbnail(), but then 1811 // ignoring the returned Bitmap and querying the raw table; give 1812 // them a row with enough information to find the original image. 1813 if ((table == IMAGES_THUMBNAILS || table == VIDEO_THUMBNAILS) 1814 && !TextUtils.isEmpty(selection)) { 1815 final Matcher matcher = PATTERN_SELECTION_ID.matcher(selection); 1816 if (matcher.matches()) { 1817 final long id = Long.parseLong(matcher.group(1)); 1818 1819 final Uri fullUri; 1820 if (table == IMAGES_THUMBNAILS) { 1821 fullUri = ContentUris.withAppendedId( 1822 Images.Media.getContentUri(volumeName), id); 1823 } else if (table == VIDEO_THUMBNAILS) { 1824 fullUri = ContentUris.withAppendedId( 1825 Video.Media.getContentUri(volumeName), id); 1826 } else { 1827 throw new IllegalArgumentException(); 1828 } 1829 1830 final MatrixCursor cursor = new MatrixCursor(projection); 1831 try { 1832 String data = null; 1833 if (ContentResolver.DEPRECATE_DATA_COLUMNS) { 1834 // Go through provider to escape sandbox 1835 data = ContentResolver.translateDeprecatedDataPath( 1836 fullUri.buildUpon().appendPath("thumbnail").build()); 1837 } else { 1838 // Go directly to thumbnail file on disk 1839 data = ensureThumbnail(fullUri, signal).getAbsolutePath(); 1840 } 1841 cursor.newRow().add(MediaColumns._ID, null) 1842 .add(Images.Thumbnails.IMAGE_ID, id) 1843 .add(Video.Thumbnails.VIDEO_ID, id) 1844 .add(MediaColumns.DATA, data); 1845 } catch (FileNotFoundException ignored) { 1846 // Return empty cursor if we had thumbnail trouble 1847 } 1848 return cursor; 1849 } 1850 } 1851 } 1852 1853 final String having = null; 1854 final Cursor c = qb.query(db, projection, 1855 selection, selectionArgs, groupBy, having, sortOrder, limit, signal); 1856 1857 if (c != null) { 1858 ((AbstractCursor) c).setNotificationUris(getContext().getContentResolver(), 1859 Arrays.asList(uri), UserHandle.myUserId(), false); 1860 } 1861 1862 return c; 1863 } 1864 1865 @Override getType(Uri url)1866 public String getType(Uri url) { 1867 final int match = matchUri(url, true); 1868 switch (match) { 1869 case IMAGES_MEDIA_ID: 1870 case AUDIO_MEDIA_ID: 1871 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 1872 case VIDEO_MEDIA_ID: 1873 case DOWNLOADS_ID: 1874 case FILES_ID: 1875 final LocalCallingIdentity token = clearLocalCallingIdentity(); 1876 try (Cursor cursor = queryForSingleItem(url, 1877 new String[] { MediaColumns.MIME_TYPE }, null, null, null)) { 1878 return cursor.getString(0); 1879 } catch (FileNotFoundException e) { 1880 throw new IllegalArgumentException(e.getMessage()); 1881 } finally { 1882 restoreLocalCallingIdentity(token); 1883 } 1884 1885 case IMAGES_MEDIA: 1886 case IMAGES_THUMBNAILS: 1887 return Images.Media.CONTENT_TYPE; 1888 1889 case AUDIO_ALBUMART_ID: 1890 case AUDIO_ALBUMART_FILE_ID: 1891 case IMAGES_THUMBNAILS_ID: 1892 case VIDEO_THUMBNAILS_ID: 1893 return "image/jpeg"; 1894 1895 case AUDIO_MEDIA: 1896 case AUDIO_GENRES_ID_MEMBERS: 1897 case AUDIO_PLAYLISTS_ID_MEMBERS: 1898 return Audio.Media.CONTENT_TYPE; 1899 1900 case AUDIO_GENRES: 1901 case AUDIO_MEDIA_ID_GENRES: 1902 return Audio.Genres.CONTENT_TYPE; 1903 case AUDIO_GENRES_ID: 1904 case AUDIO_MEDIA_ID_GENRES_ID: 1905 return Audio.Genres.ENTRY_CONTENT_TYPE; 1906 case AUDIO_PLAYLISTS: 1907 case AUDIO_MEDIA_ID_PLAYLISTS: 1908 return Audio.Playlists.CONTENT_TYPE; 1909 case AUDIO_PLAYLISTS_ID: 1910 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 1911 return Audio.Playlists.ENTRY_CONTENT_TYPE; 1912 1913 case VIDEO_MEDIA: 1914 return Video.Media.CONTENT_TYPE; 1915 case DOWNLOADS: 1916 return Downloads.CONTENT_TYPE; 1917 } 1918 throw new IllegalStateException("Unknown URL : " + url); 1919 } 1920 1921 @VisibleForTesting ensureFileColumns(Uri uri, ContentValues values)1922 static void ensureFileColumns(Uri uri, ContentValues values) throws VolumeArgumentException { 1923 ensureNonUniqueFileColumns(matchUri(uri, true), uri, values, null /* currentPath */); 1924 } 1925 ensureUniqueFileColumns(int match, Uri uri, ContentValues values)1926 private static void ensureUniqueFileColumns(int match, Uri uri, ContentValues values) 1927 throws VolumeArgumentException { 1928 ensureFileColumns(match, uri, values, true, null /* currentPath */); 1929 } 1930 ensureNonUniqueFileColumns(int match, Uri uri, ContentValues values, @Nullable String currentPath)1931 private static void ensureNonUniqueFileColumns(int match, Uri uri, ContentValues values, 1932 @Nullable String currentPath) throws VolumeArgumentException { 1933 ensureFileColumns(match, uri, values, false, currentPath); 1934 } 1935 1936 /** 1937 * Get the various file-related {@link MediaColumns} in the given 1938 * {@link ContentValues} into sane condition. Also validates that defined 1939 * columns are valid for the given {@link Uri}, such as ensuring that only 1940 * {@code image/*} can be inserted into 1941 * {@link android.provider.MediaStore.Images}. 1942 */ ensureFileColumns(int match, Uri uri, ContentValues values, boolean makeUnique, @Nullable String currentPath)1943 private static void ensureFileColumns(int match, Uri uri, ContentValues values, 1944 boolean makeUnique, @Nullable String currentPath) throws VolumeArgumentException { 1945 Trace.traceBegin(TRACE_TAG_DATABASE, "ensureFileColumns"); 1946 1947 // Figure out defaults based on Uri being modified 1948 String defaultMimeType = ContentResolver.MIME_TYPE_DEFAULT; 1949 String defaultPrimary = Environment.DIRECTORY_DOWNLOADS; 1950 String defaultSecondary = null; 1951 List<String> allowedPrimary = Arrays.asList( 1952 Environment.DIRECTORY_DOWNLOADS, 1953 Environment.DIRECTORY_DOCUMENTS); 1954 switch (match) { 1955 case AUDIO_MEDIA: 1956 case AUDIO_MEDIA_ID: 1957 defaultMimeType = "audio/mpeg"; 1958 defaultPrimary = Environment.DIRECTORY_MUSIC; 1959 allowedPrimary = Arrays.asList( 1960 Environment.DIRECTORY_ALARMS, 1961 Environment.DIRECTORY_MUSIC, 1962 Environment.DIRECTORY_NOTIFICATIONS, 1963 Environment.DIRECTORY_PODCASTS, 1964 Environment.DIRECTORY_RINGTONES); 1965 break; 1966 case VIDEO_MEDIA: 1967 case VIDEO_MEDIA_ID: 1968 defaultMimeType = "video/mp4"; 1969 defaultPrimary = Environment.DIRECTORY_MOVIES; 1970 allowedPrimary = Arrays.asList( 1971 Environment.DIRECTORY_DCIM, 1972 Environment.DIRECTORY_MOVIES); 1973 break; 1974 case IMAGES_MEDIA: 1975 case IMAGES_MEDIA_ID: 1976 defaultMimeType = "image/jpeg"; 1977 defaultPrimary = Environment.DIRECTORY_PICTURES; 1978 allowedPrimary = Arrays.asList( 1979 Environment.DIRECTORY_DCIM, 1980 Environment.DIRECTORY_PICTURES); 1981 break; 1982 case AUDIO_ALBUMART: 1983 case AUDIO_ALBUMART_ID: 1984 defaultMimeType = "image/jpeg"; 1985 defaultPrimary = Environment.DIRECTORY_MUSIC; 1986 allowedPrimary = Arrays.asList(defaultPrimary); 1987 defaultSecondary = ".thumbnails"; 1988 break; 1989 case VIDEO_THUMBNAILS: 1990 case VIDEO_THUMBNAILS_ID: 1991 defaultMimeType = "image/jpeg"; 1992 defaultPrimary = Environment.DIRECTORY_MOVIES; 1993 allowedPrimary = Arrays.asList(defaultPrimary); 1994 defaultSecondary = ".thumbnails"; 1995 break; 1996 case IMAGES_THUMBNAILS: 1997 case IMAGES_THUMBNAILS_ID: 1998 defaultMimeType = "image/jpeg"; 1999 defaultPrimary = Environment.DIRECTORY_PICTURES; 2000 allowedPrimary = Arrays.asList(defaultPrimary); 2001 defaultSecondary = ".thumbnails"; 2002 break; 2003 case AUDIO_PLAYLISTS: 2004 case AUDIO_PLAYLISTS_ID: 2005 defaultPrimary = Environment.DIRECTORY_MUSIC; 2006 allowedPrimary = Arrays.asList(defaultPrimary); 2007 break; 2008 case DOWNLOADS: 2009 case DOWNLOADS_ID: 2010 defaultPrimary = Environment.DIRECTORY_DOWNLOADS; 2011 allowedPrimary = Arrays.asList(defaultPrimary); 2012 break; 2013 case FILES: 2014 case FILES_ID: 2015 // Use defaults above 2016 break; 2017 default: 2018 Log.w(TAG, "Unhandled location " + uri + "; assuming generic files"); 2019 break; 2020 } 2021 2022 final String resolvedVolumeName = resolveVolumeName(uri); 2023 2024 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA)) 2025 && MediaStore.VOLUME_INTERNAL.equals(resolvedVolumeName)) { 2026 // TODO: promote this to top-level check 2027 throw new UnsupportedOperationException( 2028 "Writing to internal storage is not supported."); 2029 } 2030 2031 // Force values when raw path provided 2032 if (!TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { 2033 final String data = values.getAsString(MediaColumns.DATA); 2034 2035 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) { 2036 values.put(MediaColumns.DISPLAY_NAME, extractDisplayName(data)); 2037 } 2038 if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) { 2039 values.put(MediaColumns.MIME_TYPE, MediaFile.getMimeTypeForFile(data)); 2040 } 2041 } 2042 2043 // Give ourselves sane defaults when missing 2044 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DISPLAY_NAME))) { 2045 values.put(MediaColumns.DISPLAY_NAME, 2046 String.valueOf(System.currentTimeMillis())); 2047 } 2048 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 2049 final int format = formatObject == null ? 0 : formatObject.intValue(); 2050 if (format == MtpConstants.FORMAT_ASSOCIATION) { 2051 values.putNull(MediaColumns.MIME_TYPE); 2052 } else if (TextUtils.isEmpty(values.getAsString(MediaColumns.MIME_TYPE))) { 2053 values.put(MediaColumns.MIME_TYPE, defaultMimeType); 2054 } 2055 2056 // Sanity check MIME type against table 2057 final String mimeType = values.getAsString(MediaColumns.MIME_TYPE); 2058 if (mimeType != null && !defaultMimeType.equals(ContentResolver.MIME_TYPE_DEFAULT)) { 2059 final String[] split = defaultMimeType.split("/"); 2060 if (!mimeType.startsWith(split[0])) { 2061 throw new IllegalArgumentException( 2062 "MIME type " + mimeType + " cannot be inserted into " + uri 2063 + "; expected MIME type under " + split[0] + "/*"); 2064 } 2065 } 2066 2067 // Generate path when undefined 2068 if (TextUtils.isEmpty(values.getAsString(MediaColumns.DATA))) { 2069 // Combine together deprecated columns when path undefined 2070 if (TextUtils.isEmpty(values.getAsString(MediaColumns.RELATIVE_PATH))) { 2071 String primary = values.getAsString(MediaColumns.PRIMARY_DIRECTORY); 2072 String secondary = values.getAsString(MediaColumns.SECONDARY_DIRECTORY); 2073 2074 // Fall back to defaults when caller left undefined 2075 if (TextUtils.isEmpty(primary)) primary = defaultPrimary; 2076 if (TextUtils.isEmpty(secondary)) secondary = defaultSecondary; 2077 2078 if (primary != null) { 2079 if (secondary != null) { 2080 values.put(MediaColumns.RELATIVE_PATH, primary + '/' + secondary + '/'); 2081 } else { 2082 values.put(MediaColumns.RELATIVE_PATH, primary + '/'); 2083 } 2084 } 2085 } 2086 2087 final String[] relativePath = sanitizePath( 2088 values.getAsString(MediaColumns.RELATIVE_PATH)); 2089 final String displayName = sanitizeDisplayName( 2090 values.getAsString(MediaColumns.DISPLAY_NAME)); 2091 2092 // Create result file 2093 File res; 2094 try { 2095 res = getVolumePath(resolvedVolumeName); 2096 } catch (FileNotFoundException e) { 2097 throw new IllegalArgumentException(e); 2098 } 2099 res = Environment.buildPath(res, relativePath); 2100 try { 2101 if (makeUnique) { 2102 res = FileUtils.buildUniqueFile(res, mimeType, displayName); 2103 } else { 2104 res = FileUtils.buildNonUniqueFile(res, mimeType, displayName); 2105 } 2106 } catch (FileNotFoundException e) { 2107 throw new IllegalStateException( 2108 "Failed to build unique file: " + res + " " + displayName + " " + mimeType); 2109 } 2110 2111 // Check for shady looking paths 2112 2113 // Require content live under specific directories, but allow in-place updates of 2114 // existing content that lives in the invalid directory. 2115 final String primary = relativePath[0]; 2116 if (!res.getAbsolutePath().equals(currentPath) && !allowedPrimary.contains(primary)) { 2117 throw new IllegalArgumentException( 2118 "Primary directory " + primary + " not allowed for " + uri 2119 + "; allowed directories are " + allowedPrimary); 2120 } 2121 2122 // Ensure all parent folders of result file exist 2123 res.getParentFile().mkdirs(); 2124 if (!res.getParentFile().exists()) { 2125 throw new IllegalStateException("Failed to create directory: " + res); 2126 } 2127 values.put(MediaColumns.DATA, res.getAbsolutePath()); 2128 } else { 2129 assertFileColumnsSane(match, uri, values); 2130 } 2131 2132 // Drop columns that aren't relevant for special tables 2133 switch (match) { 2134 case AUDIO_ALBUMART: 2135 case VIDEO_THUMBNAILS: 2136 case IMAGES_THUMBNAILS: 2137 case AUDIO_PLAYLISTS: 2138 values.remove(MediaColumns.DISPLAY_NAME); 2139 values.remove(MediaColumns.MIME_TYPE); 2140 break; 2141 } 2142 2143 Trace.traceEnd(TRACE_TAG_DATABASE); 2144 } 2145 sanitizePath(@ullable String path)2146 private static @NonNull String[] sanitizePath(@Nullable String path) { 2147 if (path == null) { 2148 return EmptyArray.STRING; 2149 } else { 2150 final String[] segments = path.split("/"); 2151 for (int i = 0; i < segments.length; i++) { 2152 segments[i] = sanitizeDisplayName(segments[i]); 2153 } 2154 return segments; 2155 } 2156 } 2157 sanitizeDisplayName(@ullable String name)2158 private static @Nullable String sanitizeDisplayName(@Nullable String name) { 2159 if (name == null) { 2160 return null; 2161 } else if (name.startsWith(".")) { 2162 // The resulting file must not be hidden. 2163 return FileUtils.buildValidFatFilename("_" + name); 2164 } else { 2165 return FileUtils.buildValidFatFilename(name); 2166 } 2167 } 2168 2169 /** 2170 * Sanity check that any requested {@link MediaColumns#DATA} paths actually 2171 * live on the storage volume being targeted. 2172 */ assertFileColumnsSane(int match, Uri uri, ContentValues values)2173 private static void assertFileColumnsSane(int match, Uri uri, ContentValues values) 2174 throws VolumeArgumentException { 2175 if (!values.containsKey(MediaColumns.DATA)) return; 2176 try { 2177 // Sanity check that the requested path actually lives on volume 2178 final String volumeName = resolveVolumeName(uri); 2179 final Collection<File> allowed = getVolumeScanPaths(volumeName); 2180 final File actual = new File(values.getAsString(MediaColumns.DATA)) 2181 .getCanonicalFile(); 2182 if (!FileUtils.contains(allowed, actual)) { 2183 throw new VolumeArgumentException(actual, allowed); 2184 } 2185 } catch (IOException e) { 2186 throw new IllegalArgumentException(e); 2187 } 2188 } 2189 2190 @Override bulkInsert(Uri uri, ContentValues values[])2191 public int bulkInsert(Uri uri, ContentValues values[]) { 2192 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 2193 final boolean allowHidden = isCallingPackageAllowedHidden(); 2194 final int match = matchUri(uri, allowHidden); 2195 2196 if (match == VOLUMES) { 2197 return super.bulkInsert(uri, values); 2198 } 2199 2200 final DatabaseHelper helper; 2201 final SQLiteDatabase db; 2202 try { 2203 helper = getDatabaseForUri(uri); 2204 db = helper.getWritableDatabase(); 2205 } catch (VolumeNotFoundException e) { 2206 return e.translateForUpdateDelete(targetSdkVersion); 2207 } 2208 2209 if (match == MTP_OBJECT_REFERENCES) { 2210 int handle = Integer.parseInt(uri.getPathSegments().get(2)); 2211 return setObjectReferences(helper, db, handle, values); 2212 } 2213 2214 helper.beginTransaction(); 2215 try { 2216 final int result = super.bulkInsert(uri, values); 2217 helper.setTransactionSuccessful(); 2218 return result; 2219 } finally { 2220 helper.endTransaction(); 2221 } 2222 } 2223 playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[])2224 private int playlistBulkInsert(SQLiteDatabase db, Uri uri, ContentValues values[]) { 2225 DatabaseUtils.InsertHelper helper = 2226 new DatabaseUtils.InsertHelper(db, "audio_playlists_map"); 2227 int audioidcolidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.AUDIO_ID); 2228 int playlistididx = helper.getColumnIndex(Audio.Playlists.Members.PLAYLIST_ID); 2229 int playorderidx = helper.getColumnIndex(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 2230 long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 2231 2232 db.beginTransaction(); 2233 int numInserted = 0; 2234 try { 2235 int len = values.length; 2236 for (int i = 0; i < len; i++) { 2237 helper.prepareForInsert(); 2238 // getting the raw Object and converting it long ourselves saves 2239 // an allocation (the alternative is ContentValues.getAsLong, which 2240 // returns a Long object) 2241 long audioid = ((Number) values[i].get( 2242 MediaStore.Audio.Playlists.Members.AUDIO_ID)).longValue(); 2243 helper.bind(audioidcolidx, audioid); 2244 helper.bind(playlistididx, playlistId); 2245 // convert to int ourselves to save an allocation. 2246 int playorder = ((Number) values[i].get( 2247 MediaStore.Audio.Playlists.Members.PLAY_ORDER)).intValue(); 2248 helper.bind(playorderidx, playorder); 2249 helper.execute(); 2250 } 2251 numInserted = len; 2252 db.setTransactionSuccessful(); 2253 } finally { 2254 db.endTransaction(); 2255 helper.close(); 2256 } 2257 getContext().getContentResolver().notifyChange(uri, null); 2258 return numInserted; 2259 } 2260 insertDirectory(DatabaseHelper helper, SQLiteDatabase db, String path)2261 private long insertDirectory(DatabaseHelper helper, SQLiteDatabase db, String path) { 2262 if (LOCAL_LOGV) Log.v(TAG, "inserting directory " + path); 2263 ContentValues values = new ContentValues(); 2264 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 2265 values.put(FileColumns.DATA, path); 2266 values.put(FileColumns.PARENT, getParent(helper, db, path)); 2267 values.put(FileColumns.OWNER_PACKAGE_NAME, extractPathOwnerPackageName(path)); 2268 values.put(FileColumns.VOLUME_NAME, extractVolumeName(path)); 2269 values.put(FileColumns.RELATIVE_PATH, extractRelativePath(path)); 2270 values.put(FileColumns.DISPLAY_NAME, extractDisplayName(path)); 2271 values.put(FileColumns.IS_DOWNLOAD, isDownload(path)); 2272 File file = new File(path); 2273 if (file.exists()) { 2274 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 2275 } 2276 long rowId = db.insert("files", FileColumns.DATE_MODIFIED, values); 2277 return rowId; 2278 } 2279 extractVolumeName(@ullable String data)2280 private static @Nullable String extractVolumeName(@Nullable String data) { 2281 if (data == null) return null; 2282 final Matcher matcher = PATTERN_VOLUME_NAME.matcher(data); 2283 if (matcher.find()) { 2284 final String volumeName = matcher.group(1); 2285 if (volumeName.equals("emulated")) { 2286 return MediaStore.VOLUME_EXTERNAL_PRIMARY; 2287 } else { 2288 return StorageVolume.normalizeUuid(volumeName); 2289 } 2290 } else { 2291 return MediaStore.VOLUME_INTERNAL; 2292 } 2293 } 2294 extractRelativePath(@ullable String data)2295 private static @Nullable String extractRelativePath(@Nullable String data) { 2296 if (data == null) return null; 2297 final Matcher matcher = PATTERN_RELATIVE_PATH.matcher(data); 2298 if (matcher.find()) { 2299 final int lastSlash = data.lastIndexOf('/'); 2300 if (lastSlash == -1 || lastSlash < matcher.end()) { 2301 // This is a file in the top-level directory, so relative path is "/" 2302 // which is different than null, which means unknown path 2303 return "/"; 2304 } else { 2305 return data.substring(matcher.end(), lastSlash + 1); 2306 } 2307 } else { 2308 return null; 2309 } 2310 } 2311 extractDisplayName(@ullable String data)2312 private static @Nullable String extractDisplayName(@Nullable String data) { 2313 if (data == null) return null; 2314 if (data.endsWith("/")) { 2315 data = data.substring(0, data.length() - 1); 2316 } 2317 return data.substring(data.lastIndexOf('/') + 1); 2318 } 2319 getParent(DatabaseHelper helper, SQLiteDatabase db, String path)2320 private long getParent(DatabaseHelper helper, SQLiteDatabase db, String path) { 2321 final String parentPath = new File(path).getParent(); 2322 if (Objects.equals("/", parentPath)) { 2323 return -1; 2324 } else { 2325 synchronized (mDirectoryCache) { 2326 Long id = mDirectoryCache.get(parentPath); 2327 if (id != null) { 2328 return id; 2329 } 2330 } 2331 2332 final long id; 2333 try (Cursor c = db.query("files", new String[] { FileColumns._ID }, 2334 FileColumns.DATA + "=?", new String[] { parentPath }, null, null, null)) { 2335 if (c.moveToFirst()) { 2336 id = c.getLong(0); 2337 } else { 2338 id = insertDirectory(helper, db, parentPath); 2339 } 2340 } 2341 2342 synchronized (mDirectoryCache) { 2343 mDirectoryCache.put(parentPath, id); 2344 } 2345 return id; 2346 } 2347 } 2348 2349 /** 2350 * @param c the Cursor whose title to retrieve 2351 * @return the result of {@link #getDefaultTitle(String)} if the result is valid; otherwise 2352 * the value of the {@code MediaStore.Audio.Media.TITLE} column 2353 */ getDefaultTitleFromCursor(Cursor c)2354 private String getDefaultTitleFromCursor(Cursor c) { 2355 String title = null; 2356 final int columnIndex = c.getColumnIndex("title_resource_uri"); 2357 // Necessary to check for existence because we may be reading from an old DB version 2358 if (columnIndex > -1) { 2359 final String titleResourceUri = c.getString(columnIndex); 2360 if (titleResourceUri != null) { 2361 try { 2362 title = getDefaultTitle(titleResourceUri); 2363 } catch (Exception e) { 2364 // Best attempt only 2365 } 2366 } 2367 } 2368 if (title == null) { 2369 title = c.getString(c.getColumnIndex(MediaStore.Audio.Media.TITLE)); 2370 } 2371 return title; 2372 } 2373 2374 /** 2375 * @param title_resource_uri The title resource for which to retrieve the default localization 2376 * @return The title localized to {@code Locale.US}, or {@code null} if unlocalizable 2377 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 2378 * for any reason. For example, the application from which the localized title is fetched is not 2379 * installed, or it does not have the resource which needs to be localized 2380 */ getDefaultTitle(String title_resource_uri)2381 private String getDefaultTitle(String title_resource_uri) throws Exception{ 2382 try { 2383 return getTitleFromResourceUri(title_resource_uri, false); 2384 } catch (Exception e) { 2385 Log.e(TAG, "Error getting default title for " + title_resource_uri, e); 2386 throw e; 2387 } 2388 } 2389 2390 /** 2391 * @param title_resource_uri The title resource to localize 2392 * @return The localized title, or {@code null} if unlocalizable 2393 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 2394 * for any reason. For example, the application from which the localized title is fetched is not 2395 * installed, or it does not have the resource which needs to be localized 2396 */ getLocalizedTitle(String title_resource_uri)2397 private String getLocalizedTitle(String title_resource_uri) throws Exception { 2398 try { 2399 return getTitleFromResourceUri(title_resource_uri, true); 2400 } catch (Exception e) { 2401 Log.e(TAG, "Error getting localized title for " + title_resource_uri, e); 2402 throw e; 2403 } 2404 } 2405 2406 /** 2407 * Localizable titles conform to this URI pattern: 2408 * Scheme: {@link ContentResolver.SCHEME_ANDROID_RESOURCE} 2409 * Authority: Package Name of ringtone title provider 2410 * First Path Segment: Type of resource (must be "string") 2411 * Second Path Segment: Resource name of title 2412 * 2413 * @param title_resource_uri The title resource to retrieve 2414 * @param localize Whether or not to localize the title 2415 * @return The title, or {@code null} if unlocalizable 2416 * @throws Exception Thrown if the title appears to be localizable, but the localization failed 2417 * for any reason. For example, the application from which the localized title is fetched is not 2418 * installed, or it does not have the resource which needs to be localized 2419 */ getTitleFromResourceUri(String title_resource_uri, boolean localize)2420 private String getTitleFromResourceUri(String title_resource_uri, boolean localize) 2421 throws Exception { 2422 if (TextUtils.isEmpty(title_resource_uri)) { 2423 return null; 2424 } 2425 final Uri titleUri = Uri.parse(title_resource_uri); 2426 final String scheme = titleUri.getScheme(); 2427 if (!ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { 2428 return null; 2429 } 2430 final List<String> pathSegments = titleUri.getPathSegments(); 2431 if (pathSegments.size() != 2) { 2432 Log.e(TAG, "Error getting localized title for " + title_resource_uri 2433 + ", must have 2 path segments"); 2434 return null; 2435 } 2436 final String type = pathSegments.get(0); 2437 if (!"string".equals(type)) { 2438 Log.e(TAG, "Error getting localized title for " + title_resource_uri 2439 + ", first path segment must be \"string\""); 2440 return null; 2441 } 2442 final String packageName = titleUri.getAuthority(); 2443 final Resources resources; 2444 if (localize) { 2445 resources = mPackageManager.getResourcesForApplication(packageName); 2446 } else { 2447 final Context packageContext = getContext().createPackageContext(packageName, 0); 2448 final Configuration configuration = packageContext.getResources().getConfiguration(); 2449 configuration.setLocale(Locale.US); 2450 resources = packageContext.createConfigurationContext(configuration).getResources(); 2451 } 2452 final String resourceIdentifier = pathSegments.get(1); 2453 final int id = resources.getIdentifier(resourceIdentifier, type, packageName); 2454 return resources.getString(id); 2455 } 2456 onLocaleChanged()2457 public void onLocaleChanged() { 2458 localizeTitles(); 2459 } 2460 localizeTitles()2461 private void localizeTitles() { 2462 final DatabaseHelper helper = mInternalDatabase; 2463 final SQLiteDatabase db = helper.getWritableDatabase(); 2464 2465 try (Cursor c = db.query("files", new String[]{"_id", "title_resource_uri"}, 2466 "title_resource_uri IS NOT NULL", null, null, null, null)) { 2467 while (c.moveToNext()) { 2468 final String id = c.getString(0); 2469 final String titleResourceUri = c.getString(1); 2470 final ContentValues values = new ContentValues(); 2471 try { 2472 final String localizedTitle = getLocalizedTitle(titleResourceUri); 2473 values.put("title_key", MediaStore.Audio.keyFor(localizedTitle)); 2474 // do a final trim of the title, in case it started with the special 2475 // "sort first" character (ascii \001) 2476 values.put("title", localizedTitle.trim()); 2477 db.update("files", values, "_id=?", new String[]{id}); 2478 } catch (Exception e) { 2479 Log.e(TAG, "Error updating localized title for " + titleResourceUri 2480 + ", keeping old localization"); 2481 } 2482 } 2483 } 2484 } 2485 insertFile(DatabaseHelper helper, int match, Uri uri, ContentValues values, int mediaType, boolean notify)2486 private long insertFile(DatabaseHelper helper, int match, Uri uri, ContentValues values, 2487 int mediaType, boolean notify) { 2488 final SQLiteDatabase db = helper.getWritableDatabase(); 2489 2490 boolean wasPathEmpty = !values.containsKey(MediaStore.MediaColumns.DATA) 2491 || TextUtils.isEmpty(values.getAsString(MediaStore.MediaColumns.DATA)); 2492 2493 // Make sure all file-related columns are defined 2494 try { 2495 ensureUniqueFileColumns(match, uri, values); 2496 } catch (VolumeArgumentException e) { 2497 if (getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.Q) { 2498 throw new IllegalArgumentException(e.getMessage()); 2499 } else { 2500 Log.w(TAG, e.getMessage()); 2501 return 0; 2502 } 2503 } 2504 2505 switch (mediaType) { 2506 case FileColumns.MEDIA_TYPE_IMAGE: { 2507 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 2508 break; 2509 } 2510 2511 case FileColumns.MEDIA_TYPE_AUDIO: { 2512 // SQLite Views are read-only, so we need to deconstruct this 2513 // insert and do inserts into the underlying tables. 2514 // If doing this here turns out to be a performance bottleneck, 2515 // consider moving this to native code and using triggers on 2516 // the view. 2517 String albumartist = values.getAsString(MediaStore.Audio.Media.ALBUM_ARTIST); 2518 String compilation = values.getAsString(MediaStore.Audio.Media.COMPILATION); 2519 values.remove(MediaStore.Audio.Media.COMPILATION); 2520 2521 // Insert the artist into the artist table and remove it from 2522 // the input values 2523 Object so = values.get("artist"); 2524 String s = (so == null ? "" : so.toString()); 2525 values.remove("artist"); 2526 long artistRowId; 2527 ArrayMap<String, Long> artistCache = helper.mArtistCache; 2528 String path = values.getAsString(MediaStore.MediaColumns.DATA); 2529 synchronized(artistCache) { 2530 Long temp = artistCache.get(s); 2531 if (temp == null) { 2532 artistRowId = getKeyIdForName(helper, db, 2533 "artists", "artist_key", "artist", 2534 s, s, path, 0, null, artistCache, uri); 2535 } else { 2536 artistRowId = temp.longValue(); 2537 } 2538 } 2539 String artist = s; 2540 2541 // Do the same for the album field 2542 so = values.get("album"); 2543 s = (so == null ? "" : so.toString()); 2544 values.remove("album"); 2545 long albumRowId; 2546 ArrayMap<String, Long> albumCache = helper.mAlbumCache; 2547 synchronized(albumCache) { 2548 int albumhash = 0; 2549 if (albumartist != null) { 2550 albumhash = albumartist.hashCode(); 2551 } else if (compilation != null && compilation.equals("1")) { 2552 // nothing to do, hash already set 2553 } else { 2554 albumhash = path.substring(0, path.lastIndexOf('/')).hashCode(); 2555 } 2556 String cacheName = s + albumhash; 2557 Long temp = albumCache.get(cacheName); 2558 if (temp == null) { 2559 albumRowId = getKeyIdForName(helper, db, 2560 "albums", "album_key", "album", 2561 s, cacheName, path, albumhash, artist, albumCache, uri); 2562 } else { 2563 albumRowId = temp; 2564 } 2565 } 2566 2567 values.put("artist_id", Integer.toString((int)artistRowId)); 2568 values.put("album_id", Integer.toString((int)albumRowId)); 2569 so = values.getAsString("title"); 2570 s = (so == null ? "" : so.toString()); 2571 2572 try { 2573 final String localizedTitle = getLocalizedTitle(s); 2574 if (localizedTitle != null) { 2575 values.put("title_resource_uri", s); 2576 s = localizedTitle; 2577 } else { 2578 values.putNull("title_resource_uri"); 2579 } 2580 } catch (Exception e) { 2581 values.put("title_resource_uri", s); 2582 } 2583 values.put("title_key", MediaStore.Audio.keyFor(s)); 2584 // do a final trim of the title, in case it started with the special 2585 // "sort first" character (ascii \001) 2586 values.put("title", s.trim()); 2587 break; 2588 } 2589 2590 case FileColumns.MEDIA_TYPE_VIDEO: { 2591 break; 2592 } 2593 } 2594 2595 // compute bucket_id and bucket_display_name for all files 2596 String path = values.getAsString(MediaStore.MediaColumns.DATA); 2597 computeDataValues(values); 2598 values.put(MediaStore.MediaColumns.DATE_ADDED, System.currentTimeMillis() / 1000); 2599 2600 long rowId = 0; 2601 Integer i = values.getAsInteger( 2602 MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID); 2603 if (i != null) { 2604 rowId = i.intValue(); 2605 values = new ContentValues(values); 2606 values.remove(MediaStore.MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID); 2607 } 2608 2609 String title = values.getAsString(MediaStore.MediaColumns.TITLE); 2610 if (title == null && path != null) { 2611 title = MediaFile.getFileTitle(path); 2612 } 2613 values.put(FileColumns.TITLE, title); 2614 2615 String mimeType = null; 2616 int format = MtpConstants.FORMAT_ASSOCIATION; 2617 if (path != null && new File(path).isDirectory()) { 2618 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ASSOCIATION); 2619 values.putNull(MediaStore.MediaColumns.MIME_TYPE); 2620 } else { 2621 mimeType = values.getAsString(MediaStore.MediaColumns.MIME_TYPE); 2622 final Integer formatObject = values.getAsInteger(FileColumns.FORMAT); 2623 format = (formatObject == null ? 0 : formatObject.intValue()); 2624 } 2625 2626 if (format == 0) { 2627 if (TextUtils.isEmpty(path) || wasPathEmpty) { 2628 // special case device created playlists 2629 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 2630 values.put(FileColumns.FORMAT, MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST); 2631 // create a file path for the benefit of MTP 2632 path = Environment.getExternalStorageDirectory() 2633 + "/Playlists/" + values.getAsString(Audio.Playlists.NAME); 2634 values.put(MediaStore.MediaColumns.DATA, path); 2635 values.put(FileColumns.PARENT, 0); 2636 } 2637 } else { 2638 format = MediaFile.getFormatCode(path, mimeType); 2639 } 2640 } 2641 if (path != null && path.endsWith("/")) { 2642 Log.e(TAG, "directory has trailing slash: " + path); 2643 return 0; 2644 } 2645 if (format != 0) { 2646 values.put(FileColumns.FORMAT, format); 2647 if (mimeType == null && format != MtpConstants.FORMAT_ASSOCIATION) { 2648 mimeType = MediaFile.getMimeTypeForFormatCode(format); 2649 } 2650 } 2651 2652 if (mimeType == null && path != null && format != MtpConstants.FORMAT_ASSOCIATION) { 2653 mimeType = MediaFile.getMimeTypeForFile(path); 2654 } 2655 2656 if (mimeType != null) { 2657 values.put(FileColumns.MIME_TYPE, mimeType); 2658 2659 // If 'values' contained the media type, then the caller wants us 2660 // to use that exact type, so don't override it based on mimetype 2661 if (!values.containsKey(FileColumns.MEDIA_TYPE) && 2662 mediaType == FileColumns.MEDIA_TYPE_NONE && 2663 !android.media.MediaScanner.isNoMediaPath(path)) { 2664 if (MediaFile.isAudioMimeType(mimeType)) { 2665 mediaType = FileColumns.MEDIA_TYPE_AUDIO; 2666 } else if (MediaFile.isVideoMimeType(mimeType)) { 2667 mediaType = FileColumns.MEDIA_TYPE_VIDEO; 2668 } else if (MediaFile.isImageMimeType(mimeType)) { 2669 mediaType = FileColumns.MEDIA_TYPE_IMAGE; 2670 } else if (MediaFile.isPlayListMimeType(mimeType)) { 2671 mediaType = FileColumns.MEDIA_TYPE_PLAYLIST; 2672 } 2673 } 2674 } 2675 values.put(FileColumns.MEDIA_TYPE, mediaType); 2676 2677 if (rowId == 0) { 2678 if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 2679 String name = values.getAsString(Audio.Playlists.NAME); 2680 if (name == null && path == null) { 2681 // MediaScanner will compute the name from the path if we have one 2682 throw new IllegalArgumentException( 2683 "no name was provided when inserting abstract playlist"); 2684 } 2685 } else { 2686 if (path == null) { 2687 // path might be null for playlists created on the device 2688 // or transfered via MTP 2689 throw new IllegalArgumentException( 2690 "no path was provided when inserting new file"); 2691 } 2692 } 2693 2694 // make sure modification date and size are set 2695 if (path != null) { 2696 File file = new File(path); 2697 if (file.exists()) { 2698 values.put(FileColumns.DATE_MODIFIED, file.lastModified() / 1000); 2699 if (!values.containsKey(FileColumns.SIZE)) { 2700 values.put(FileColumns.SIZE, file.length()); 2701 } 2702 } 2703 } 2704 2705 Long parent = values.getAsLong(FileColumns.PARENT); 2706 if (parent == null) { 2707 if (path != null) { 2708 long parentId = getParent(helper, db, path); 2709 values.put(FileColumns.PARENT, parentId); 2710 } 2711 } 2712 2713 rowId = db.insert("files", FileColumns.DATE_MODIFIED, values); 2714 } else { 2715 db.update("files", values, FileColumns._ID + "=?", 2716 new String[] { Long.toString(rowId) }); 2717 } 2718 if (format == MtpConstants.FORMAT_ASSOCIATION) { 2719 synchronized (mDirectoryCache) { 2720 mDirectoryCache.put(path, rowId); 2721 } 2722 } 2723 2724 return rowId; 2725 } 2726 getObjectReferences(DatabaseHelper helper, SQLiteDatabase db, int handle)2727 private Cursor getObjectReferences(DatabaseHelper helper, SQLiteDatabase db, int handle) { 2728 Cursor c = db.query("files", sMediaTableColumns, "_id=?", 2729 new String[] { Integer.toString(handle) }, 2730 null, null, null); 2731 try { 2732 if (c != null && c.moveToNext()) { 2733 long playlistId = c.getLong(0); 2734 int mediaType = c.getInt(1); 2735 if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) { 2736 // we only support object references for playlist objects 2737 return null; 2738 } 2739 return db.rawQuery(OBJECT_REFERENCES_QUERY, 2740 new String[] { Long.toString(playlistId) } ); 2741 } 2742 } finally { 2743 IoUtils.closeQuietly(c); 2744 } 2745 return null; 2746 } 2747 setObjectReferences(DatabaseHelper helper, SQLiteDatabase db, int handle, ContentValues values[])2748 private int setObjectReferences(DatabaseHelper helper, SQLiteDatabase db, 2749 int handle, ContentValues values[]) { 2750 // first look up the media table and media ID for the object 2751 long playlistId = 0; 2752 Cursor c = db.query("files", sMediaTableColumns, "_id=?", 2753 new String[] { Integer.toString(handle) }, 2754 null, null, null); 2755 try { 2756 if (c != null && c.moveToNext()) { 2757 int mediaType = c.getInt(1); 2758 if (mediaType != FileColumns.MEDIA_TYPE_PLAYLIST) { 2759 // we only support object references for playlist objects 2760 return 0; 2761 } 2762 playlistId = c.getLong(0); 2763 } 2764 } finally { 2765 IoUtils.closeQuietly(c); 2766 } 2767 if (playlistId == 0) { 2768 return 0; 2769 } 2770 2771 // next delete any existing entries 2772 db.delete("audio_playlists_map", "playlist_id=?", 2773 new String[] { Long.toString(playlistId) }); 2774 2775 // finally add the new entries 2776 int count = values.length; 2777 int added = 0; 2778 ContentValues[] valuesList = new ContentValues[count]; 2779 for (int i = 0; i < count; i++) { 2780 // convert object ID to audio ID 2781 long audioId = 0; 2782 long objectId = values[i].getAsLong(MediaStore.MediaColumns._ID); 2783 c = db.query("files", sMediaTableColumns, "_id=?", 2784 new String[] { Long.toString(objectId) }, 2785 null, null, null); 2786 try { 2787 if (c != null && c.moveToNext()) { 2788 int mediaType = c.getInt(1); 2789 if (mediaType != FileColumns.MEDIA_TYPE_AUDIO) { 2790 // we only allow audio files in playlists, so skip 2791 continue; 2792 } 2793 audioId = c.getLong(0); 2794 } 2795 } finally { 2796 IoUtils.closeQuietly(c); 2797 } 2798 if (audioId != 0) { 2799 ContentValues v = new ContentValues(); 2800 v.put(MediaStore.Audio.Playlists.Members.PLAYLIST_ID, playlistId); 2801 v.put(MediaStore.Audio.Playlists.Members.AUDIO_ID, audioId); 2802 v.put(MediaStore.Audio.Playlists.Members.PLAY_ORDER, added); 2803 valuesList[added++] = v; 2804 } 2805 } 2806 if (added < count) { 2807 // we weren't able to find everything on the list, so lets resize the array 2808 // and pass what we have. 2809 ContentValues[] newValues = new ContentValues[added]; 2810 System.arraycopy(valuesList, 0, newValues, 0, added); 2811 valuesList = newValues; 2812 } 2813 2814 int rowsChanged = playlistBulkInsert(db, 2815 Audio.Playlists.Members.getContentUri(MediaStore.VOLUME_EXTERNAL, playlistId), 2816 valuesList); 2817 2818 if (rowsChanged > 0) { 2819 updatePlaylistDateModifiedToNow(db, playlistId); 2820 } 2821 2822 return rowsChanged; 2823 } 2824 2825 private static final String[] GENRE_LOOKUP_PROJECTION = new String[] { 2826 Audio.Genres._ID, // 0 2827 Audio.Genres.NAME, // 1 2828 }; 2829 updateGenre(long rowId, String genre, String volumeName)2830 private void updateGenre(long rowId, String genre, String volumeName) { 2831 Uri uri = null; 2832 Cursor cursor = null; 2833 Uri genresUri = MediaStore.Audio.Genres.getContentUri(volumeName); 2834 try { 2835 // see if the genre already exists 2836 cursor = query(genresUri, GENRE_LOOKUP_PROJECTION, MediaStore.Audio.Genres.NAME + "=?", 2837 new String[] { genre }, null); 2838 if (cursor == null || cursor.getCount() == 0) { 2839 // genre does not exist, so create the genre in the genre table 2840 ContentValues values = new ContentValues(); 2841 values.put(MediaStore.Audio.Genres.NAME, genre); 2842 uri = insert(genresUri, values); 2843 } else { 2844 // genre already exists, so compute its Uri 2845 cursor.moveToNext(); 2846 uri = ContentUris.withAppendedId(genresUri, cursor.getLong(0)); 2847 } 2848 if (uri != null) { 2849 uri = Uri.withAppendedPath(uri, MediaStore.Audio.Genres.Members.CONTENT_DIRECTORY); 2850 } 2851 } finally { 2852 IoUtils.closeQuietly(cursor); 2853 } 2854 2855 if (uri != null) { 2856 // add entry to audio_genre_map 2857 ContentValues values = new ContentValues(); 2858 values.put(MediaStore.Audio.Genres.Members.AUDIO_ID, Long.valueOf(rowId)); 2859 insert(uri, values); 2860 } 2861 } 2862 2863 @VisibleForTesting extractPathOwnerPackageName(@ullable String path)2864 static @Nullable String extractPathOwnerPackageName(@Nullable String path) { 2865 if (path == null) return null; 2866 final Matcher m = PATTERN_OWNED_PATH.matcher(path); 2867 if (m.matches()) { 2868 return m.group(1); 2869 } else { 2870 return null; 2871 } 2872 } 2873 maybePut(@onNull ContentValues values, @NonNull String key, @Nullable String value)2874 private void maybePut(@NonNull ContentValues values, @NonNull String key, 2875 @Nullable String value) { 2876 if (value != null) { 2877 values.put(key, value); 2878 } 2879 } 2880 maybeMarkAsDownload(@onNull ContentValues values)2881 private boolean maybeMarkAsDownload(@NonNull ContentValues values) { 2882 final String path = values.getAsString(MediaColumns.DATA); 2883 if (path != null && isDownload(path)) { 2884 values.put(FileColumns.IS_DOWNLOAD, true); 2885 return true; 2886 } 2887 return false; 2888 } 2889 resolveVolumeName(@onNull Uri uri)2890 private static @NonNull String resolveVolumeName(@NonNull Uri uri) { 2891 final String volumeName = getVolumeName(uri); 2892 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) { 2893 return MediaStore.VOLUME_EXTERNAL_PRIMARY; 2894 } else { 2895 return volumeName; 2896 } 2897 } 2898 2899 @Override insert(Uri uri, ContentValues initialValues)2900 public Uri insert(Uri uri, ContentValues initialValues) { 2901 Trace.traceBegin(TRACE_TAG_DATABASE, "insert"); 2902 try { 2903 return insertInternal(uri, initialValues); 2904 } finally { 2905 Trace.traceEnd(TRACE_TAG_DATABASE); 2906 } 2907 } 2908 insertInternal(Uri uri, ContentValues initialValues)2909 private Uri insertInternal(Uri uri, ContentValues initialValues) { 2910 final boolean allowHidden = isCallingPackageAllowedHidden(); 2911 final int match = matchUri(uri, allowHidden); 2912 2913 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 2914 final String originalVolumeName = getVolumeName(uri); 2915 final String resolvedVolumeName = resolveVolumeName(uri); 2916 2917 // handle MEDIA_SCANNER before calling getDatabaseForUri() 2918 if (match == MEDIA_SCANNER) { 2919 mMediaScannerVolume = initialValues.getAsString(MediaStore.MEDIA_SCANNER_VOLUME); 2920 2921 final DatabaseHelper helper; 2922 try { 2923 helper = getDatabaseForUri(MediaStore.Files.getContentUri(mMediaScannerVolume)); 2924 } catch (VolumeNotFoundException e) { 2925 return e.translateForInsert(targetSdkVersion); 2926 } 2927 2928 helper.mScanStartTime = SystemClock.currentTimeMicro(); 2929 return MediaStore.getMediaScannerUri(); 2930 } 2931 2932 if (match == VOLUMES) { 2933 String name = initialValues.getAsString("name"); 2934 Uri attachedVolume = attachVolume(name); 2935 if (mMediaScannerVolume != null && mMediaScannerVolume.equals(name)) { 2936 final DatabaseHelper helper; 2937 try { 2938 helper = getDatabaseForUri( 2939 MediaStore.Files.getContentUri(mMediaScannerVolume)); 2940 } catch (VolumeNotFoundException e) { 2941 return e.translateForInsert(targetSdkVersion); 2942 } 2943 helper.mScanStartTime = SystemClock.currentTimeMicro(); 2944 } 2945 return attachedVolume; 2946 } 2947 2948 String genre = null; 2949 String path = null; 2950 String ownerPackageName = null; 2951 if (initialValues != null) { 2952 // Ignore or augment incoming raw filesystem paths 2953 for (String column : sDataColumns.keySet()) { 2954 if (!initialValues.containsKey(column)) continue; 2955 2956 if (isCallingPackageSystem() || isCallingPackageLegacy()) { 2957 // Mutation allowed 2958 } else { 2959 Log.w(TAG, "Ignoring mutation of " + column + " from " 2960 + getCallingPackageOrSelf()); 2961 initialValues.remove(column); 2962 } 2963 } 2964 2965 genre = initialValues.getAsString(Audio.AudioColumns.GENRE); 2966 initialValues.remove(Audio.AudioColumns.GENRE); 2967 path = initialValues.getAsString(MediaStore.MediaColumns.DATA); 2968 2969 if (!isCallingPackageSystem()) { 2970 initialValues.remove(FileColumns.IS_DOWNLOAD); 2971 } 2972 2973 // We no longer track location metadata 2974 if (initialValues.containsKey(ImageColumns.LATITUDE)) { 2975 initialValues.putNull(ImageColumns.LATITUDE); 2976 } 2977 if (initialValues.containsKey(ImageColumns.LONGITUDE)) { 2978 initialValues.putNull(ImageColumns.LONGITUDE); 2979 } 2980 2981 if (isCallingPackageSystem()) { 2982 // When media inserted by ourselves, the best we can do is guess 2983 // ownership based on path. 2984 ownerPackageName = initialValues.getAsString(FileColumns.OWNER_PACKAGE_NAME); 2985 if (TextUtils.isEmpty(ownerPackageName)) { 2986 ownerPackageName = extractPathOwnerPackageName(path); 2987 } 2988 } else { 2989 // Remote callers have no direct control over owner column; we force 2990 // it be whoever is creating the content. 2991 initialValues.remove(FileColumns.OWNER_PACKAGE_NAME); 2992 ownerPackageName = getCallingPackageOrSelf(); 2993 } 2994 } 2995 2996 long rowId = -1; 2997 Uri newUri = null; 2998 2999 final DatabaseHelper helper; 3000 final SQLiteDatabase db; 3001 try { 3002 helper = getDatabaseForUri(uri); 3003 db = helper.getWritableDatabase(); 3004 } catch (VolumeNotFoundException e) { 3005 return e.translateForInsert(targetSdkVersion); 3006 } 3007 3008 switch (match) { 3009 case IMAGES_MEDIA: { 3010 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3011 final boolean isDownload = maybeMarkAsDownload(initialValues); 3012 rowId = insertFile(helper, match, uri, initialValues, 3013 FileColumns.MEDIA_TYPE_IMAGE, true); 3014 if (rowId > 0) { 3015 MediaDocumentsProvider.onMediaStoreInsert( 3016 getContext(), resolvedVolumeName, FileColumns.MEDIA_TYPE_IMAGE, rowId); 3017 newUri = ContentUris.withAppendedId( 3018 Images.Media.getContentUri(originalVolumeName), rowId); 3019 } 3020 break; 3021 } 3022 3023 case IMAGES_THUMBNAILS: { 3024 if (helper.mInternal) { 3025 throw new UnsupportedOperationException( 3026 "Writing to internal storage is not supported."); 3027 } 3028 3029 // Require that caller has write access to underlying media 3030 final long imageId = initialValues.getAsLong(MediaStore.Images.Thumbnails.IMAGE_ID); 3031 enforceCallingPermission(ContentUris.withAppendedId( 3032 MediaStore.Images.Media.getContentUri(resolvedVolumeName), imageId), true); 3033 3034 try { 3035 ensureUniqueFileColumns(match, uri, initialValues); 3036 } catch (VolumeArgumentException e) { 3037 return e.translateForInsert(targetSdkVersion); 3038 } 3039 3040 rowId = db.insert("thumbnails", "name", initialValues); 3041 if (rowId > 0) { 3042 newUri = ContentUris.withAppendedId(Images.Thumbnails. 3043 getContentUri(originalVolumeName), rowId); 3044 } 3045 break; 3046 } 3047 3048 case VIDEO_THUMBNAILS: { 3049 if (helper.mInternal) { 3050 throw new UnsupportedOperationException( 3051 "Writing to internal storage is not supported."); 3052 } 3053 3054 // Require that caller has write access to underlying media 3055 final long videoId = initialValues.getAsLong(MediaStore.Video.Thumbnails.VIDEO_ID); 3056 enforceCallingPermission(ContentUris.withAppendedId( 3057 MediaStore.Video.Media.getContentUri(resolvedVolumeName), videoId), true); 3058 3059 try { 3060 ensureUniqueFileColumns(match, uri, initialValues); 3061 } catch (VolumeArgumentException e) { 3062 return e.translateForInsert(targetSdkVersion); 3063 } 3064 3065 rowId = db.insert("videothumbnails", "name", initialValues); 3066 if (rowId > 0) { 3067 newUri = ContentUris.withAppendedId(Video.Thumbnails. 3068 getContentUri(originalVolumeName), rowId); 3069 } 3070 break; 3071 } 3072 3073 case AUDIO_MEDIA: { 3074 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3075 final boolean isDownload = maybeMarkAsDownload(initialValues); 3076 rowId = insertFile(helper, match, uri, initialValues, 3077 FileColumns.MEDIA_TYPE_AUDIO, true); 3078 if (rowId > 0) { 3079 MediaDocumentsProvider.onMediaStoreInsert( 3080 getContext(), resolvedVolumeName, FileColumns.MEDIA_TYPE_AUDIO, rowId); 3081 newUri = ContentUris.withAppendedId( 3082 Audio.Media.getContentUri(originalVolumeName), rowId); 3083 if (genre != null) { 3084 updateGenre(rowId, genre, resolvedVolumeName); 3085 } 3086 } 3087 break; 3088 } 3089 3090 case AUDIO_MEDIA_ID_GENRES: { 3091 // Require that caller has write access to underlying media 3092 final long audioId = Long.parseLong(uri.getPathSegments().get(2)); 3093 enforceCallingPermission(ContentUris.withAppendedId( 3094 MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId), true); 3095 3096 ContentValues values = new ContentValues(initialValues); 3097 values.put(Audio.Genres.Members.AUDIO_ID, audioId); 3098 rowId = db.insert("audio_genres_map", "genre_id", values); 3099 if (rowId > 0) { 3100 newUri = ContentUris.withAppendedId(uri, rowId); 3101 } 3102 break; 3103 } 3104 3105 case AUDIO_MEDIA_ID_PLAYLISTS: { 3106 // Require that caller has write access to underlying media 3107 final long audioId = Long.parseLong(uri.getPathSegments().get(2)); 3108 enforceCallingPermission(ContentUris.withAppendedId( 3109 MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId), true); 3110 final long playlistId = initialValues 3111 .getAsLong(MediaStore.Audio.Playlists.Members.PLAYLIST_ID); 3112 enforceCallingPermission(ContentUris.withAppendedId( 3113 MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId), true); 3114 3115 ContentValues values = new ContentValues(initialValues); 3116 values.put(Audio.Playlists.Members.AUDIO_ID, audioId); 3117 rowId = db.insert("audio_playlists_map", "playlist_id", 3118 values); 3119 if (rowId > 0) { 3120 newUri = ContentUris.withAppendedId(uri, rowId); 3121 updatePlaylistDateModifiedToNow(db, playlistId); 3122 } 3123 break; 3124 } 3125 3126 case AUDIO_GENRES: { 3127 // NOTE: No permission enforcement on genres 3128 rowId = db.insert("audio_genres", "audio_id", initialValues); 3129 if (rowId > 0) { 3130 newUri = ContentUris.withAppendedId( 3131 Audio.Genres.getContentUri(originalVolumeName), rowId); 3132 } 3133 break; 3134 } 3135 3136 case AUDIO_GENRES_ID_MEMBERS: { 3137 // Require that caller has write access to underlying media 3138 final long audioId = initialValues 3139 .getAsLong(MediaStore.Audio.Genres.Members.AUDIO_ID); 3140 enforceCallingPermission(ContentUris.withAppendedId( 3141 MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId), true); 3142 3143 Long genreId = Long.parseLong(uri.getPathSegments().get(3)); 3144 ContentValues values = new ContentValues(initialValues); 3145 values.put(Audio.Genres.Members.GENRE_ID, genreId); 3146 rowId = db.insert("audio_genres_map", "genre_id", values); 3147 if (rowId > 0) { 3148 newUri = ContentUris.withAppendedId(uri, rowId); 3149 } 3150 break; 3151 } 3152 3153 case AUDIO_PLAYLISTS: { 3154 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3155 final boolean isDownload = maybeMarkAsDownload(initialValues); 3156 ContentValues values = new ContentValues(initialValues); 3157 values.put(MediaStore.Audio.Playlists.DATE_ADDED, System.currentTimeMillis() / 1000); 3158 rowId = insertFile(helper, match, uri, values, 3159 FileColumns.MEDIA_TYPE_PLAYLIST, true); 3160 if (rowId > 0) { 3161 newUri = ContentUris.withAppendedId( 3162 Audio.Playlists.getContentUri(originalVolumeName), rowId); 3163 } 3164 break; 3165 } 3166 3167 case AUDIO_PLAYLISTS_ID: 3168 case AUDIO_PLAYLISTS_ID_MEMBERS: { 3169 // Require that caller has write access to underlying media 3170 final long audioId = initialValues 3171 .getAsLong(MediaStore.Audio.Playlists.Members.AUDIO_ID); 3172 enforceCallingPermission(ContentUris.withAppendedId( 3173 MediaStore.Audio.Media.getContentUri(resolvedVolumeName), audioId), true); 3174 final long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 3175 enforceCallingPermission(ContentUris.withAppendedId( 3176 MediaStore.Audio.Playlists.getContentUri(resolvedVolumeName), playlistId), true); 3177 3178 ContentValues values = new ContentValues(initialValues); 3179 values.put(Audio.Playlists.Members.PLAYLIST_ID, playlistId); 3180 rowId = db.insert("audio_playlists_map", "playlist_id", values); 3181 if (rowId > 0) { 3182 newUri = ContentUris.withAppendedId(uri, rowId); 3183 updatePlaylistDateModifiedToNow(db, playlistId); 3184 } 3185 break; 3186 } 3187 3188 case VIDEO_MEDIA: { 3189 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3190 final boolean isDownload = maybeMarkAsDownload(initialValues); 3191 rowId = insertFile(helper, match, uri, initialValues, 3192 FileColumns.MEDIA_TYPE_VIDEO, true); 3193 if (rowId > 0) { 3194 MediaDocumentsProvider.onMediaStoreInsert( 3195 getContext(), resolvedVolumeName, FileColumns.MEDIA_TYPE_VIDEO, rowId); 3196 newUri = ContentUris.withAppendedId( 3197 Video.Media.getContentUri(originalVolumeName), rowId); 3198 } 3199 break; 3200 } 3201 3202 case AUDIO_ALBUMART: { 3203 if (helper.mInternal) { 3204 throw new UnsupportedOperationException("no internal album art allowed"); 3205 } 3206 3207 try { 3208 ensureUniqueFileColumns(match, uri, initialValues); 3209 } catch (VolumeArgumentException e) { 3210 return e.translateForInsert(targetSdkVersion); 3211 } 3212 3213 rowId = db.insert("album_art", MediaStore.MediaColumns.DATA, initialValues); 3214 if (rowId > 0) { 3215 newUri = ContentUris.withAppendedId(uri, rowId); 3216 } 3217 break; 3218 } 3219 3220 case FILES: { 3221 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3222 final boolean isDownload = maybeMarkAsDownload(initialValues); 3223 rowId = insertFile(helper, match, uri, initialValues, 3224 FileColumns.MEDIA_TYPE_NONE, true); 3225 if (rowId > 0) { 3226 MediaDocumentsProvider.onMediaStoreInsert( 3227 getContext(), resolvedVolumeName, FileColumns.MEDIA_TYPE_NONE, rowId); 3228 newUri = Files.getContentUri(originalVolumeName, rowId); 3229 } 3230 break; 3231 } 3232 3233 case MTP_OBJECTS: 3234 // We don't send a notification if the insert originated from MTP 3235 final boolean isDownload = maybeMarkAsDownload(initialValues); 3236 rowId = insertFile(helper, match, uri, initialValues, 3237 FileColumns.MEDIA_TYPE_NONE, false); 3238 if (rowId > 0) { 3239 newUri = Files.getMtpObjectsUri(originalVolumeName, rowId); 3240 } 3241 break; 3242 3243 case FILES_DIRECTORY: 3244 rowId = insertDirectory(helper, helper.getWritableDatabase(), 3245 initialValues.getAsString(FileColumns.DATA)); 3246 if (rowId > 0) { 3247 newUri = Files.getContentUri(originalVolumeName, rowId); 3248 } 3249 break; 3250 3251 case DOWNLOADS: 3252 maybePut(initialValues, FileColumns.OWNER_PACKAGE_NAME, ownerPackageName); 3253 initialValues.put(FileColumns.IS_DOWNLOAD, true); 3254 rowId = insertFile(helper, match, uri, initialValues, 3255 FileColumns.MEDIA_TYPE_NONE, false); 3256 if (rowId > 0) { 3257 final int mediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE); 3258 MediaDocumentsProvider.onMediaStoreInsert( 3259 getContext(), resolvedVolumeName, mediaType, rowId); 3260 newUri = ContentUris.withAppendedId( 3261 MediaStore.Downloads.getContentUri(originalVolumeName), rowId); 3262 } 3263 break; 3264 3265 default: 3266 throw new UnsupportedOperationException("Invalid URI " + uri); 3267 } 3268 3269 // Remember that caller is owner of this item, to speed up future 3270 // permission checks for this caller 3271 mCallingIdentity.get().setOwned(rowId, true); 3272 3273 if (path != null && path.toLowerCase(Locale.US).endsWith("/.nomedia")) { 3274 MediaScanner.instance(getContext()).scanFile(new File(path).getParentFile()); 3275 } 3276 3277 if (newUri != null) { 3278 acceptWithExpansion(helper::notifyChange, newUri); 3279 } 3280 return newUri; 3281 } 3282 3283 @Override applyBatch(ArrayList<ContentProviderOperation> operations)3284 public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) 3285 throws OperationApplicationException { 3286 // Open transactions on databases for requested volumes 3287 final ArrayMap<String, DatabaseHelper> transactions = new ArrayMap<>(); 3288 try { 3289 for (ContentProviderOperation op : operations) { 3290 final String volumeName = MediaStore.getVolumeName(op.getUri()); 3291 if (!transactions.containsKey(volumeName)) { 3292 try { 3293 final DatabaseHelper helper = getDatabaseForUri(op.getUri()); 3294 helper.beginTransaction(); 3295 transactions.put(volumeName, helper); 3296 } catch (VolumeNotFoundException e) { 3297 Log.w(TAG, e.getMessage()); 3298 } 3299 } 3300 } 3301 3302 final ContentProviderResult[] result = super.applyBatch(operations); 3303 for (DatabaseHelper helper : transactions.values()) { 3304 helper.setTransactionSuccessful(); 3305 } 3306 return result; 3307 } finally { 3308 for (DatabaseHelper helper : transactions.values()) { 3309 helper.endTransaction(); 3310 } 3311 } 3312 } 3313 appendWhereStandalone(@onNull SQLiteQueryBuilder qb, @Nullable String selection, @Nullable Object... selectionArgs)3314 private static void appendWhereStandalone(@NonNull SQLiteQueryBuilder qb, 3315 @Nullable String selection, @Nullable Object... selectionArgs) { 3316 qb.appendWhereStandalone(DatabaseUtils.bindSelection(selection, selectionArgs)); 3317 } 3318 bindList(@onNull Object... args)3319 static @NonNull String bindList(@NonNull Object... args) { 3320 final StringBuilder sb = new StringBuilder(); 3321 sb.append('('); 3322 for (int i = 0; i < args.length; i++) { 3323 sb.append('?'); 3324 if (i < args.length - 1) { 3325 sb.append(','); 3326 } 3327 } 3328 sb.append(')'); 3329 return DatabaseUtils.bindSelection(sb.toString(), args); 3330 } 3331 parseBoolean(String value)3332 private static boolean parseBoolean(String value) { 3333 if (value == null) return false; 3334 if ("1".equals(value)) return true; 3335 if ("true".equalsIgnoreCase(value)) return true; 3336 return false; 3337 } 3338 3339 @Deprecated getSharedPackages(String callingPackage)3340 private String getSharedPackages(String callingPackage) { 3341 final String[] sharedPackageNames = mCallingIdentity.get().getSharedPackageNames(); 3342 return bindList((Object[]) sharedPackageNames); 3343 } 3344 3345 private static final int TYPE_QUERY = 0; 3346 private static final int TYPE_UPDATE = 1; 3347 private static final int TYPE_DELETE = 2; 3348 3349 /** 3350 * Generate a {@link SQLiteQueryBuilder} that is filtered based on the 3351 * runtime permissions and/or {@link Uri} grants held by the caller. 3352 * <ul> 3353 * <li>If caller holds a {@link Uri} grant, access is allowed according to 3354 * that grant. 3355 * <li>If caller holds the write permission for a collection, they can 3356 * read/write all contents of that collection. 3357 * <li>If caller holds the read permission for a collection, they can read 3358 * all contents of that collection, but writes are limited to content they 3359 * own. 3360 * <li>If caller holds no permissions for a collection, all reads/write are 3361 * limited to content they own. 3362 * </ul> 3363 */ getQueryBuilder(int type, Uri uri, int match, Bundle queryArgs)3364 private SQLiteQueryBuilder getQueryBuilder(int type, Uri uri, int match, Bundle queryArgs) { 3365 Trace.traceBegin(TRACE_TAG_DATABASE, "getQueryBuilder"); 3366 try { 3367 return getQueryBuilderInternal(type, uri, match, queryArgs); 3368 } finally { 3369 Trace.traceEnd(TRACE_TAG_DATABASE); 3370 } 3371 } 3372 getQueryBuilderInternal(int type, Uri uri, int match, Bundle queryArgs)3373 private SQLiteQueryBuilder getQueryBuilderInternal(int type, Uri uri, int match, 3374 Bundle queryArgs) { 3375 final boolean forWrite; 3376 switch (type) { 3377 case TYPE_QUERY: forWrite = false; break; 3378 case TYPE_UPDATE: forWrite = true; break; 3379 case TYPE_DELETE: forWrite = true; break; 3380 default: throw new IllegalStateException(); 3381 } 3382 3383 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 3384 if (parseBoolean(uri.getQueryParameter("distinct"))) { 3385 qb.setDistinct(true); 3386 } 3387 qb.setProjectionAggregationAllowed(true); 3388 qb.setStrict(true); 3389 3390 final String callingPackage = getCallingPackageOrSelf(); 3391 3392 // TODO: throw when requesting a currently unmounted volume 3393 final String volumeName = MediaStore.getVolumeName(uri); 3394 final String includeVolumes; 3395 if (MediaStore.VOLUME_EXTERNAL.equals(volumeName)) { 3396 includeVolumes = bindList(getExternalVolumeNames().toArray()); 3397 } else { 3398 includeVolumes = bindList(volumeName); 3399 } 3400 final String sharedPackages = getSharedPackages(callingPackage); 3401 final boolean allowGlobal = checkCallingPermissionGlobal(uri, forWrite); 3402 final boolean allowLegacy = checkCallingPermissionLegacy(uri, forWrite, callingPackage); 3403 final boolean allowLegacyRead = allowLegacy && !forWrite; 3404 3405 boolean includePending = parseBoolean( 3406 uri.getQueryParameter(MediaStore.PARAM_INCLUDE_PENDING)); 3407 boolean includeTrashed = parseBoolean( 3408 uri.getQueryParameter(MediaStore.PARAM_INCLUDE_TRASHED)); 3409 boolean includeAllVolumes = false; 3410 3411 switch (match) { 3412 case IMAGES_MEDIA_ID: 3413 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3414 includePending = true; 3415 includeTrashed = true; 3416 // fall-through 3417 case IMAGES_MEDIA: 3418 if (type == TYPE_QUERY) { 3419 qb.setTables("images"); 3420 qb.setProjectionMap(getProjectionMap(Images.Media.class)); 3421 } else { 3422 qb.setTables("files"); 3423 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 3424 FileColumns.MEDIA_TYPE_IMAGE); 3425 } 3426 if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) { 3427 appendWhereStandalone(qb, FileColumns.OWNER_PACKAGE_NAME + " IN " 3428 + sharedPackages); 3429 } 3430 if (!includePending) { 3431 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3432 } 3433 if (!includeTrashed) { 3434 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3435 } 3436 if (!includeAllVolumes) { 3437 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3438 } 3439 break; 3440 3441 case IMAGES_THUMBNAILS_ID: 3442 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3443 // fall-through 3444 case IMAGES_THUMBNAILS: { 3445 qb.setTables("thumbnails"); 3446 3447 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3448 getProjectionMap(Images.Thumbnails.class)); 3449 projectionMap.put(Images.Thumbnails.THUMB_DATA, 3450 "NULL AS " + Images.Thumbnails.THUMB_DATA); 3451 qb.setProjectionMap(projectionMap); 3452 3453 if (!allowGlobal && !checkCallingPermissionImages(forWrite, callingPackage)) { 3454 appendWhereStandalone(qb, 3455 "image_id IN (SELECT _id FROM images WHERE owner_package_name IN " 3456 + sharedPackages + ")"); 3457 } 3458 break; 3459 } 3460 3461 case AUDIO_MEDIA_ID: 3462 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3463 includePending = true; 3464 includeTrashed = true; 3465 // fall-through 3466 case AUDIO_MEDIA: 3467 if (type == TYPE_QUERY) { 3468 qb.setTables("audio"); 3469 qb.setProjectionMap(getProjectionMap(Audio.Media.class)); 3470 } else { 3471 qb.setTables("files"); 3472 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 3473 FileColumns.MEDIA_TYPE_AUDIO); 3474 } 3475 if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) { 3476 // Apps without Audio permission can only see their own 3477 // media, but we also let them see ringtone-style media to 3478 // support legacy use-cases. 3479 appendWhereStandalone(qb, 3480 DatabaseUtils.bindSelection(FileColumns.OWNER_PACKAGE_NAME 3481 + " IN " + sharedPackages 3482 + " OR is_ringtone=1 OR is_alarm=1 OR is_notification=1")); 3483 } 3484 if (!includePending) { 3485 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3486 } 3487 if (!includeTrashed) { 3488 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3489 } 3490 if (!includeAllVolumes) { 3491 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3492 } 3493 break; 3494 3495 case AUDIO_MEDIA_ID_GENRES_ID: 3496 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5)); 3497 // fall-through 3498 case AUDIO_MEDIA_ID_GENRES: 3499 qb.setTables("audio_genres"); 3500 qb.setProjectionMap(getProjectionMap(Audio.Genres.class)); 3501 appendWhereStandalone(qb, "_id IN (SELECT genre_id FROM " + 3502 "audio_genres_map WHERE audio_id=?)", uri.getPathSegments().get(3)); 3503 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3504 // We don't have a great way to filter parsed metadata by 3505 // owner, so callers need to hold READ_MEDIA_AUDIO 3506 appendWhereStandalone(qb, "0"); 3507 } 3508 break; 3509 3510 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 3511 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(5)); 3512 // fall-through 3513 case AUDIO_MEDIA_ID_PLAYLISTS: 3514 qb.setTables("audio_playlists"); 3515 qb.setProjectionMap(getProjectionMap(Audio.Playlists.class)); 3516 appendWhereStandalone(qb, "_id IN (SELECT playlist_id FROM " + 3517 "audio_playlists_map WHERE audio_id=?)", uri.getPathSegments().get(3)); 3518 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3519 // We don't have a great way to filter parsed metadata by 3520 // owner, so callers need to hold READ_MEDIA_AUDIO 3521 appendWhereStandalone(qb, "0"); 3522 } 3523 break; 3524 3525 case AUDIO_GENRES_ID: 3526 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3527 // fall-through 3528 case AUDIO_GENRES: 3529 qb.setTables("audio_genres"); 3530 qb.setProjectionMap(getProjectionMap(Audio.Genres.class)); 3531 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3532 // We don't have a great way to filter parsed metadata by 3533 // owner, so callers need to hold READ_MEDIA_AUDIO 3534 appendWhereStandalone(qb, "0"); 3535 } 3536 break; 3537 3538 case AUDIO_GENRES_ID_MEMBERS: 3539 appendWhereStandalone(qb, "genre_id=?", uri.getPathSegments().get(3)); 3540 // fall-through 3541 case AUDIO_GENRES_ALL_MEMBERS: 3542 if (type == TYPE_QUERY) { 3543 qb.setTables("audio_genres_map_noid, audio"); 3544 qb.setProjectionMap(getProjectionMap(Audio.Genres.Members.class)); 3545 appendWhereStandalone(qb, "audio._id = audio_id"); 3546 } else { 3547 qb.setTables("audio_genres_map"); 3548 } 3549 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3550 // We don't have a great way to filter parsed metadata by 3551 // owner, so callers need to hold READ_MEDIA_AUDIO 3552 appendWhereStandalone(qb, "0"); 3553 } 3554 break; 3555 3556 case AUDIO_PLAYLISTS_ID: 3557 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3558 includePending = true; 3559 includeTrashed = true; 3560 // fall-through 3561 case AUDIO_PLAYLISTS: 3562 if (type == TYPE_QUERY) { 3563 qb.setTables("audio_playlists"); 3564 qb.setProjectionMap(getProjectionMap(Audio.Playlists.class)); 3565 } else { 3566 qb.setTables("files"); 3567 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 3568 FileColumns.MEDIA_TYPE_PLAYLIST); 3569 } 3570 if (!allowGlobal && !checkCallingPermissionAudio(forWrite, callingPackage)) { 3571 appendWhereStandalone(qb, FileColumns.OWNER_PACKAGE_NAME + " IN " 3572 + sharedPackages); 3573 } 3574 if (!includePending) { 3575 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3576 } 3577 if (!includeTrashed) { 3578 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3579 } 3580 if (!includeAllVolumes) { 3581 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3582 } 3583 break; 3584 3585 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 3586 appendWhereStandalone(qb, "audio_playlists_map._id=?", 3587 uri.getPathSegments().get(5)); 3588 // fall-through 3589 case AUDIO_PLAYLISTS_ID_MEMBERS: { 3590 appendWhereStandalone(qb, "playlist_id=?", uri.getPathSegments().get(3)); 3591 if (type == TYPE_QUERY) { 3592 qb.setTables("audio_playlists_map, audio"); 3593 3594 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3595 getProjectionMap(Audio.Playlists.Members.class)); 3596 projectionMap.put(Audio.Playlists.Members._ID, 3597 "audio_playlists_map._id AS " + Audio.Playlists.Members._ID); 3598 qb.setProjectionMap(projectionMap); 3599 3600 appendWhereStandalone(qb, "audio._id = audio_id"); 3601 } else { 3602 qb.setTables("audio_playlists_map"); 3603 } 3604 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3605 // We don't have a great way to filter parsed metadata by 3606 // owner, so callers need to hold READ_MEDIA_AUDIO 3607 appendWhereStandalone(qb, "0"); 3608 } 3609 break; 3610 } 3611 3612 case AUDIO_ALBUMART_ID: 3613 appendWhereStandalone(qb, "album_id=?", uri.getPathSegments().get(3)); 3614 // fall-through 3615 case AUDIO_ALBUMART: { 3616 qb.setTables("album_art"); 3617 3618 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3619 getProjectionMap(Audio.Thumbnails.class)); 3620 projectionMap.put(Audio.Thumbnails._ID, 3621 "album_id AS " + Audio.Thumbnails._ID); 3622 qb.setProjectionMap(projectionMap); 3623 3624 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3625 // We don't have a great way to filter parsed metadata by 3626 // owner, so callers need to hold READ_MEDIA_AUDIO 3627 appendWhereStandalone(qb, "0"); 3628 } 3629 break; 3630 } 3631 case AUDIO_ARTISTS_ID_ALBUMS: { 3632 if (type == TYPE_QUERY) { 3633 final String artistId = uri.getPathSegments().get(3); 3634 qb.setTables("audio LEFT OUTER JOIN album_art ON" + 3635 " audio.album_id=album_art.album_id"); 3636 appendWhereStandalone(qb, 3637 "is_music=1 AND audio.album_id IN (SELECT album_id FROM " + 3638 "artists_albums_map WHERE artist_id=?)", artistId); 3639 3640 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3641 getProjectionMap(Audio.Artists.Albums.class)); 3642 projectionMap.put(Audio.Artists.Albums.ALBUM_ART, 3643 "album_art._data AS " + Audio.Artists.Albums.ALBUM_ART); 3644 projectionMap.put(Audio.Artists.Albums.NUMBER_OF_SONGS, 3645 "count(*) AS " + Audio.Artists.Albums.NUMBER_OF_SONGS); 3646 projectionMap.put(Audio.Artists.Albums.NUMBER_OF_SONGS_FOR_ARTIST, 3647 "count(CASE WHEN artist_id==" + artistId 3648 + " THEN 'foo' ELSE NULL END) AS " 3649 + Audio.Artists.Albums.NUMBER_OF_SONGS_FOR_ARTIST); 3650 projectionMap.put(Audio.Artists.Albums.FIRST_YEAR, 3651 "MIN(year) AS " + Audio.Artists.Albums.FIRST_YEAR); 3652 projectionMap.put(Audio.Artists.Albums.LAST_YEAR, 3653 "MAX(year) AS " + Audio.Artists.Albums.LAST_YEAR); 3654 projectionMap.put(Audio.Artists.Albums.ALBUM_ID, 3655 "audio.album_id AS " + Audio.Artists.Albums.ALBUM_ID); 3656 qb.setProjectionMap(projectionMap); 3657 } else { 3658 throw new UnsupportedOperationException("Albums cannot be directly modified"); 3659 } 3660 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3661 // We don't have a great way to filter parsed metadata by 3662 // owner, so callers need to hold READ_MEDIA_AUDIO 3663 appendWhereStandalone(qb, "0"); 3664 } 3665 break; 3666 } 3667 3668 case AUDIO_ARTISTS_ID: 3669 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3670 // fall-through 3671 case AUDIO_ARTISTS: 3672 if (type == TYPE_QUERY) { 3673 qb.setTables("artist_info"); 3674 qb.setProjectionMap(getProjectionMap(Audio.Artists.class)); 3675 } else { 3676 throw new UnsupportedOperationException("Artists cannot be directly modified"); 3677 } 3678 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3679 // We don't have a great way to filter parsed metadata by 3680 // owner, so callers need to hold READ_MEDIA_AUDIO 3681 appendWhereStandalone(qb, "0"); 3682 } 3683 break; 3684 3685 case AUDIO_ALBUMS_ID: 3686 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3687 // fall-through 3688 case AUDIO_ALBUMS: { 3689 if (type == TYPE_QUERY) { 3690 qb.setTables("album_info"); 3691 3692 final ArrayMap<String, String> projectionMap = new ArrayMap<>( 3693 getProjectionMap(Audio.Albums.class)); 3694 projectionMap.put(Audio.Artists.Albums.NUMBER_OF_SONGS_FOR_ARTIST, 3695 "NULL AS " + Audio.Artists.Albums.NUMBER_OF_SONGS_FOR_ARTIST); 3696 projectionMap.put(Audio.Artists.Albums.ALBUM_ID, 3697 BaseColumns._ID + " AS " + Audio.Artists.Albums.ALBUM_ID); 3698 qb.setProjectionMap(projectionMap); 3699 } else { 3700 throw new UnsupportedOperationException("Albums cannot be directly modified"); 3701 } 3702 if (!allowGlobal && !checkCallingPermissionAudio(false, callingPackage)) { 3703 // We don't have a great way to filter parsed metadata by 3704 // owner, so callers need to hold READ_MEDIA_AUDIO 3705 appendWhereStandalone(qb, "0"); 3706 } 3707 break; 3708 } 3709 3710 case VIDEO_MEDIA_ID: 3711 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3712 includePending = true; 3713 includeTrashed = true; 3714 // fall-through 3715 case VIDEO_MEDIA: 3716 if (type == TYPE_QUERY) { 3717 qb.setTables("video"); 3718 qb.setProjectionMap(getProjectionMap(Video.Media.class)); 3719 } else { 3720 qb.setTables("files"); 3721 appendWhereStandalone(qb, FileColumns.MEDIA_TYPE + "=?", 3722 FileColumns.MEDIA_TYPE_VIDEO); 3723 } 3724 if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) { 3725 appendWhereStandalone(qb, FileColumns.OWNER_PACKAGE_NAME + " IN " 3726 + sharedPackages); 3727 } 3728 if (!includePending) { 3729 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3730 } 3731 if (!includeTrashed) { 3732 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3733 } 3734 if (!includeAllVolumes) { 3735 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3736 } 3737 break; 3738 3739 case VIDEO_THUMBNAILS_ID: 3740 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(3)); 3741 // fall-through 3742 case VIDEO_THUMBNAILS: 3743 qb.setTables("videothumbnails"); 3744 qb.setProjectionMap(getProjectionMap(Video.Thumbnails.class)); 3745 if (!allowGlobal && !checkCallingPermissionVideo(forWrite, callingPackage)) { 3746 appendWhereStandalone(qb, 3747 "video_id IN (SELECT _id FROM video WHERE owner_package_name IN " 3748 + sharedPackages + ")"); 3749 } 3750 break; 3751 3752 case FILES_ID: 3753 case MTP_OBJECTS_ID: 3754 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); 3755 includePending = true; 3756 includeTrashed = true; 3757 // fall-through 3758 case FILES: 3759 case FILES_DIRECTORY: 3760 case MTP_OBJECTS: { 3761 qb.setTables("files"); 3762 qb.setProjectionMap(getProjectionMap(Files.FileColumns.class)); 3763 3764 final ArrayList<String> options = new ArrayList<>(); 3765 if (!allowGlobal && !allowLegacyRead) { 3766 options.add(DatabaseUtils.bindSelection("owner_package_name IN " 3767 + sharedPackages)); 3768 if (allowLegacy) { 3769 options.add(DatabaseUtils.bindSelection("volume_name=?", 3770 MediaStore.VOLUME_EXTERNAL_PRIMARY)); 3771 } 3772 if (checkCallingPermissionAudio(forWrite, callingPackage)) { 3773 options.add(DatabaseUtils.bindSelection("media_type=?", 3774 FileColumns.MEDIA_TYPE_AUDIO)); 3775 options.add(DatabaseUtils.bindSelection("media_type=?", 3776 FileColumns.MEDIA_TYPE_PLAYLIST)); 3777 options.add("media_type=0 AND mime_type LIKE 'audio/%'"); 3778 } 3779 if (checkCallingPermissionVideo(forWrite, callingPackage)) { 3780 options.add(DatabaseUtils.bindSelection("media_type=?", 3781 FileColumns.MEDIA_TYPE_VIDEO)); 3782 options.add("media_type=0 AND mime_type LIKE 'video/%'"); 3783 } 3784 if (checkCallingPermissionImages(forWrite, callingPackage)) { 3785 options.add(DatabaseUtils.bindSelection("media_type=?", 3786 FileColumns.MEDIA_TYPE_IMAGE)); 3787 options.add("media_type=0 AND mime_type LIKE 'image/%'"); 3788 } 3789 } 3790 if (options.size() > 0) { 3791 appendWhereStandalone(qb, TextUtils.join(" OR ", options)); 3792 } 3793 3794 if (!includePending) { 3795 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3796 } 3797 if (!includeTrashed) { 3798 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3799 } 3800 if (!includeAllVolumes) { 3801 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3802 } 3803 break; 3804 } 3805 case DOWNLOADS_ID: 3806 appendWhereStandalone(qb, "_id=?", uri.getPathSegments().get(2)); 3807 includePending = true; 3808 includeTrashed = true; 3809 // fall-through 3810 case DOWNLOADS: { 3811 if (type == TYPE_QUERY) { 3812 qb.setTables("downloads"); 3813 qb.setProjectionMap(getProjectionMap(Downloads.class)); 3814 } else { 3815 qb.setTables("files"); 3816 appendWhereStandalone(qb, FileColumns.IS_DOWNLOAD + "=1"); 3817 } 3818 3819 final ArrayList<String> options = new ArrayList<>(); 3820 if (!allowGlobal && !allowLegacyRead) { 3821 options.add(DatabaseUtils.bindSelection("owner_package_name IN " 3822 + sharedPackages)); 3823 if (allowLegacy) { 3824 options.add(DatabaseUtils.bindSelection("volume_name=?", 3825 MediaStore.VOLUME_EXTERNAL_PRIMARY)); 3826 } 3827 } 3828 if (options.size() > 0) { 3829 appendWhereStandalone(qb, TextUtils.join(" OR ", options)); 3830 } 3831 3832 if (!includePending) { 3833 appendWhereStandalone(qb, FileColumns.IS_PENDING + "=?", 0); 3834 } 3835 if (!includeTrashed) { 3836 appendWhereStandalone(qb, FileColumns.IS_TRASHED + "=?", 0); 3837 } 3838 if (!includeAllVolumes) { 3839 appendWhereStandalone(qb, FileColumns.VOLUME_NAME + " IN " + includeVolumes); 3840 } 3841 break; 3842 } 3843 default: 3844 throw new UnsupportedOperationException( 3845 "Unknown or unsupported URL: " + uri.toString()); 3846 } 3847 3848 if (type == TYPE_QUERY) { 3849 // To ensure we're enforcing our security model, all queries must 3850 // have a projection map configured 3851 if (qb.getProjectionMap() == null) { 3852 throw new IllegalStateException("All queries must have a projection map"); 3853 } 3854 3855 // If caller is an older app, we're willing to let through a 3856 // greylist of technically invalid columns 3857 if (getCallingPackageTargetSdkVersion() < Build.VERSION_CODES.Q) { 3858 qb.setProjectionGreylist(sGreylist); 3859 } 3860 } 3861 3862 return qb; 3863 } 3864 3865 /** 3866 * Determine if given {@link Uri} has a 3867 * {@link MediaColumns#OWNER_PACKAGE_NAME} column. 3868 */ hasOwnerPackageName(Uri uri)3869 private static boolean hasOwnerPackageName(Uri uri) { 3870 // It's easier to maintain this as an inverted list 3871 final int table = matchUri(uri, true); 3872 switch (table) { 3873 case IMAGES_THUMBNAILS_ID: 3874 case IMAGES_THUMBNAILS: 3875 case VIDEO_THUMBNAILS_ID: 3876 case VIDEO_THUMBNAILS: 3877 case AUDIO_ALBUMART: 3878 case AUDIO_ALBUMART_ID: 3879 case AUDIO_ALBUMART_FILE_ID: 3880 return false; 3881 default: 3882 return true; 3883 } 3884 } 3885 3886 @Override delete(Uri uri, String userWhere, String[] userWhereArgs)3887 public int delete(Uri uri, String userWhere, String[] userWhereArgs) { 3888 Trace.traceBegin(TRACE_TAG_DATABASE, "insert"); 3889 try { 3890 return deleteInternal(uri, userWhere, userWhereArgs); 3891 } finally { 3892 Trace.traceEnd(TRACE_TAG_DATABASE); 3893 } 3894 } 3895 deleteInternal(Uri uri, String userWhere, String[] userWhereArgs)3896 private int deleteInternal(Uri uri, String userWhere, String[] userWhereArgs) { 3897 uri = safeUncanonicalize(uri); 3898 3899 int count; 3900 3901 final String volumeName = getVolumeName(uri); 3902 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 3903 final boolean allowHidden = isCallingPackageAllowedHidden(); 3904 final int match = matchUri(uri, allowHidden); 3905 3906 // handle MEDIA_SCANNER before calling getDatabaseForUri() 3907 if (match == MEDIA_SCANNER) { 3908 if (mMediaScannerVolume == null) { 3909 return 0; 3910 } 3911 3912 final DatabaseHelper helper; 3913 try { 3914 helper = getDatabaseForUri(MediaStore.Files.getContentUri(mMediaScannerVolume)); 3915 } catch (VolumeNotFoundException e) { 3916 return e.translateForUpdateDelete(targetSdkVersion); 3917 } 3918 3919 helper.mScanStopTime = SystemClock.currentTimeMicro(); 3920 String msg = dump(helper, false); 3921 logToDb(helper.getWritableDatabase(), msg); 3922 3923 if (MediaStore.VOLUME_INTERNAL.equals(mMediaScannerVolume)) { 3924 // persist current build fingerprint as fingerprint for system (internal) sound scan 3925 final SharedPreferences scanSettings = getContext().getSharedPreferences( 3926 android.media.MediaScanner.SCANNED_BUILD_PREFS_NAME, 3927 Context.MODE_PRIVATE); 3928 final SharedPreferences.Editor editor = scanSettings.edit(); 3929 editor.putString(android.media.MediaScanner.LAST_INTERNAL_SCAN_FINGERPRINT, 3930 Build.FINGERPRINT); 3931 editor.apply(); 3932 } 3933 mMediaScannerVolume = null; 3934 return 1; 3935 } 3936 3937 if (match == VOLUMES_ID) { 3938 detachVolume(uri); 3939 count = 1; 3940 } 3941 3942 final DatabaseHelper helper; 3943 final SQLiteDatabase db; 3944 try { 3945 helper = getDatabaseForUri(uri); 3946 db = helper.getWritableDatabase(); 3947 } catch (VolumeNotFoundException e) { 3948 return e.translateForUpdateDelete(targetSdkVersion); 3949 } 3950 3951 { 3952 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_DELETE, uri, match, null); 3953 3954 // Give callers interacting with a specific media item a chance to 3955 // escalate access if they don't already have it 3956 switch (match) { 3957 case AUDIO_MEDIA_ID: 3958 case VIDEO_MEDIA_ID: 3959 case IMAGES_MEDIA_ID: 3960 enforceCallingPermission(uri, true); 3961 } 3962 3963 final String[] projection = new String[] { 3964 FileColumns.MEDIA_TYPE, 3965 FileColumns.DATA, 3966 FileColumns._ID, 3967 FileColumns.IS_DOWNLOAD, 3968 FileColumns.MIME_TYPE, 3969 }; 3970 final LongSparseArray<String> deletedDownloadIds = new LongSparseArray<>(); 3971 if (qb.getTables().equals("files")) { 3972 String deleteparam = uri.getQueryParameter(MediaStore.PARAM_DELETE_DATA); 3973 if (deleteparam == null || ! deleteparam.equals("false")) { 3974 Cursor c = qb.query(db, projection, userWhere, userWhereArgs, 3975 null, null, null, null); 3976 String [] idvalue = new String[] { "" }; 3977 String [] playlistvalues = new String[] { "", "" }; 3978 try { 3979 while (c.moveToNext()) { 3980 final int mediaType = c.getInt(0); 3981 final String data = c.getString(1); 3982 final long id = c.getLong(2); 3983 final int isDownload = c.getInt(3); 3984 final String mimeType = c.getString(4); 3985 3986 // Forget that caller is owner of this item 3987 mCallingIdentity.get().setOwned(id, false); 3988 3989 // Invalidate thumbnails and revoke all outstanding grants 3990 final Uri deletedUri = Files.getContentUri(volumeName, id); 3991 invalidateThumbnails(deletedUri); 3992 acceptWithExpansion((expandedUri) -> { 3993 getContext().revokeUriPermission(expandedUri, 3994 Intent.FLAG_GRANT_READ_URI_PERMISSION 3995 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); 3996 }, deletedUri); 3997 3998 // Only need to inform DownloadProvider about the downloads deleted on 3999 // external volume. 4000 if (isDownload == 1) { 4001 deletedDownloadIds.put(id, mimeType); 4002 } 4003 if (mediaType == FileColumns.MEDIA_TYPE_IMAGE) { 4004 deleteIfAllowed(uri, data); 4005 MediaDocumentsProvider.onMediaStoreDelete(getContext(), 4006 volumeName, FileColumns.MEDIA_TYPE_IMAGE, id); 4007 } else if (mediaType == FileColumns.MEDIA_TYPE_VIDEO) { 4008 deleteIfAllowed(uri, data); 4009 MediaDocumentsProvider.onMediaStoreDelete(getContext(), 4010 volumeName, FileColumns.MEDIA_TYPE_VIDEO, id); 4011 } else if (mediaType == FileColumns.MEDIA_TYPE_AUDIO) { 4012 if (!helper.mInternal) { 4013 deleteIfAllowed(uri, data); 4014 MediaDocumentsProvider.onMediaStoreDelete(getContext(), 4015 volumeName, FileColumns.MEDIA_TYPE_AUDIO, id); 4016 4017 idvalue[0] = String.valueOf(id); 4018 db.delete("audio_genres_map", "audio_id=?", idvalue); 4019 // for each playlist that the item appears in, move 4020 // all the items behind it forward by one 4021 Cursor cc = db.query("audio_playlists_map", 4022 sPlaylistIdPlayOrder, 4023 "audio_id=?", idvalue, null, null, null); 4024 try { 4025 while (cc.moveToNext()) { 4026 long playlistId = cc.getLong(0); 4027 playlistvalues[0] = String.valueOf(playlistId); 4028 playlistvalues[1] = String.valueOf(cc.getInt(1)); 4029 int rowsChanged = db.executeSql("UPDATE audio_playlists_map" + 4030 " SET play_order=play_order-1" + 4031 " WHERE playlist_id=? AND play_order>?", 4032 playlistvalues); 4033 4034 if (rowsChanged > 0) { 4035 updatePlaylistDateModifiedToNow(db, playlistId); 4036 } 4037 } 4038 db.delete("audio_playlists_map", "audio_id=?", idvalue); 4039 } finally { 4040 IoUtils.closeQuietly(cc); 4041 } 4042 } 4043 } else if (isDownload == 1) { 4044 deleteIfAllowed(uri, data); 4045 MediaDocumentsProvider.onMediaStoreDelete(getContext(), 4046 volumeName, mediaType, id); 4047 } else if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) { 4048 // TODO, maybe: remove the audio_playlists_cleanup trigger and 4049 // implement functionality here (clean up the playlist map) 4050 } 4051 } 4052 } finally { 4053 IoUtils.closeQuietly(c); 4054 } 4055 // Do not allow deletion if the file/object is referenced as parent 4056 // by some other entries. It could cause database corruption. 4057 appendWhereStandalone(qb, ID_NOT_PARENT_CLAUSE); 4058 } 4059 } 4060 4061 switch (match) { 4062 case MTP_OBJECTS: 4063 case MTP_OBJECTS_ID: 4064 count = deleteRecursive(qb, db, userWhere, userWhereArgs); 4065 break; 4066 case AUDIO_GENRES_ID_MEMBERS: 4067 count = deleteRecursive(qb, db, userWhere, userWhereArgs); 4068 break; 4069 4070 case IMAGES_THUMBNAILS_ID: 4071 case IMAGES_THUMBNAILS: 4072 case VIDEO_THUMBNAILS_ID: 4073 case VIDEO_THUMBNAILS: 4074 // Delete the referenced files first. 4075 Cursor c = qb.query(db, sDataOnlyColumn, userWhere, userWhereArgs, null, null, 4076 null, null); 4077 if (c != null) { 4078 try { 4079 while (c.moveToNext()) { 4080 deleteIfAllowed(uri, c.getString(0)); 4081 } 4082 } finally { 4083 IoUtils.closeQuietly(c); 4084 } 4085 } 4086 count = deleteRecursive(qb, db, userWhere, userWhereArgs); 4087 break; 4088 4089 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 4090 long playlistId = Long.parseLong(uri.getPathSegments().get(3)); 4091 count = deleteRecursive(qb, db, userWhere, userWhereArgs); 4092 if (count > 0) { 4093 updatePlaylistDateModifiedToNow(db, playlistId); 4094 } 4095 break; 4096 default: 4097 count = deleteRecursive(qb, db, userWhere, userWhereArgs); 4098 break; 4099 } 4100 4101 if (deletedDownloadIds.size() > 0) { 4102 final long token = Binder.clearCallingIdentity(); 4103 try (ContentProviderClient client = getContext().getContentResolver() 4104 .acquireUnstableContentProviderClient( 4105 android.provider.Downloads.Impl.AUTHORITY)) { 4106 final Bundle extras = new Bundle(); 4107 final long[] ids = new long[deletedDownloadIds.size()]; 4108 final String[] mimeTypes = new String[deletedDownloadIds.size()]; 4109 for (int i = deletedDownloadIds.size() - 1; i >= 0; --i) { 4110 ids[i] = deletedDownloadIds.keyAt(i); 4111 mimeTypes[i] = deletedDownloadIds.valueAt(i); 4112 } 4113 extras.putLongArray(android.provider.Downloads.EXTRA_IDS, ids); 4114 extras.putStringArray(android.provider.Downloads.EXTRA_MIME_TYPES, mimeTypes); 4115 client.call(android.provider.Downloads.CALL_MEDIASTORE_DOWNLOADS_DELETED, 4116 null, extras); 4117 } catch (RemoteException e) { 4118 // Should not happen 4119 } finally { 4120 Binder.restoreCallingIdentity(token); 4121 } 4122 } 4123 } 4124 4125 if (count > 0) { 4126 acceptWithExpansion(helper::notifyChange, uri); 4127 } 4128 return count; 4129 } 4130 4131 /** 4132 * Executes identical delete repeatedly within a single transaction until 4133 * stability is reached. Combined with {@link #ID_NOT_PARENT_CLAUSE}, this 4134 * can be used to recursively delete all matching entries, since it only 4135 * deletes parents when no references remaining. 4136 */ deleteRecursive(SQLiteQueryBuilder qb, SQLiteDatabase db, String userWhere, String[] userWhereArgs)4137 private int deleteRecursive(SQLiteQueryBuilder qb, SQLiteDatabase db, String userWhere, 4138 String[] userWhereArgs) { 4139 db.beginTransaction(); 4140 try { 4141 int n = 0; 4142 int total = 0; 4143 do { 4144 n = qb.delete(db, userWhere, userWhereArgs); 4145 total += n; 4146 } while (n > 0); 4147 db.setTransactionSuccessful(); 4148 return total; 4149 } finally { 4150 db.endTransaction(); 4151 } 4152 } 4153 4154 @Override call(String method, String arg, Bundle extras)4155 public Bundle call(String method, String arg, Bundle extras) { 4156 switch (method) { 4157 case MediaStore.SCAN_FILE_CALL: 4158 case MediaStore.SCAN_VOLUME_CALL: { 4159 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4160 final CallingIdentity providerToken = clearCallingIdentity(); 4161 try { 4162 final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM); 4163 final File file = new File(uri.getPath()); 4164 final Bundle res = new Bundle(); 4165 switch (method) { 4166 case MediaStore.SCAN_FILE_CALL: 4167 res.putParcelable(Intent.EXTRA_STREAM, 4168 MediaScanner.instance(getContext()).scanFile(file)); 4169 break; 4170 case MediaStore.SCAN_VOLUME_CALL: 4171 MediaService.onScanVolume(getContext(), Uri.fromFile(file)); 4172 break; 4173 } 4174 return res; 4175 } catch (IOException e) { 4176 throw new RuntimeException(e); 4177 } finally { 4178 restoreCallingIdentity(providerToken); 4179 restoreLocalCallingIdentity(token); 4180 } 4181 } 4182 case MediaStore.UNHIDE_CALL: { 4183 throw new UnsupportedOperationException(); 4184 } 4185 case MediaStore.RETRANSLATE_CALL: { 4186 localizeTitles(); 4187 return null; 4188 } 4189 case MediaStore.GET_VERSION_CALL: { 4190 final String volumeName = extras.getString(Intent.EXTRA_TEXT); 4191 4192 final SQLiteDatabase db; 4193 try { 4194 db = getDatabaseForUri(MediaStore.Files.getContentUri(volumeName)) 4195 .getReadableDatabase(); 4196 } catch (VolumeNotFoundException e) { 4197 throw e.rethrowAsIllegalArgumentException(); 4198 } 4199 4200 final String version = db.getVersion() + ":" + getOrCreateUuid(db); 4201 4202 final Bundle res = new Bundle(); 4203 res.putString(Intent.EXTRA_TEXT, version); 4204 return res; 4205 } 4206 case MediaStore.GET_DOCUMENT_URI_CALL: { 4207 final Uri mediaUri = extras.getParcelable(DocumentsContract.EXTRA_URI); 4208 enforceCallingPermission(mediaUri, false); 4209 4210 final Uri fileUri; 4211 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4212 try { 4213 fileUri = Uri.fromFile(queryForDataFile(mediaUri, null)); 4214 } catch (FileNotFoundException e) { 4215 throw new IllegalArgumentException(e); 4216 } finally { 4217 restoreLocalCallingIdentity(token); 4218 } 4219 4220 try (ContentProviderClient client = getContext().getContentResolver() 4221 .acquireUnstableContentProviderClient( 4222 DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) { 4223 extras.putParcelable(DocumentsContract.EXTRA_URI, fileUri); 4224 return client.call(method, null, extras); 4225 } catch (RemoteException e) { 4226 throw new IllegalStateException(e); 4227 } 4228 } 4229 case MediaStore.GET_MEDIA_URI_CALL: { 4230 final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI); 4231 getContext().enforceCallingUriPermission(documentUri, 4232 Intent.FLAG_GRANT_READ_URI_PERMISSION, TAG); 4233 4234 final Uri fileUri; 4235 try (ContentProviderClient client = getContext().getContentResolver() 4236 .acquireUnstableContentProviderClient( 4237 DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY)) { 4238 final Bundle res = client.call(method, null, extras); 4239 fileUri = res.getParcelable(DocumentsContract.EXTRA_URI); 4240 } catch (RemoteException e) { 4241 throw new IllegalStateException(e); 4242 } 4243 4244 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4245 try { 4246 final Bundle res = new Bundle(); 4247 res.putParcelable(DocumentsContract.EXTRA_URI, 4248 queryForMediaUri(new File(fileUri.getPath()), null)); 4249 return res; 4250 } catch (FileNotFoundException e) { 4251 throw new IllegalArgumentException(e); 4252 } finally { 4253 restoreLocalCallingIdentity(token); 4254 } 4255 } 4256 case MediaStore.GET_CONTRIBUTED_MEDIA_CALL: { 4257 getContext().enforceCallingOrSelfPermission( 4258 android.Manifest.permission.CLEAR_APP_USER_DATA, TAG); 4259 4260 final String packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); 4261 final long totalSize = forEachContributedMedia(packageName, null); 4262 final Bundle res = new Bundle(); 4263 res.putLong(Intent.EXTRA_INDEX, totalSize); 4264 return res; 4265 } 4266 case MediaStore.DELETE_CONTRIBUTED_MEDIA_CALL: { 4267 getContext().enforceCallingOrSelfPermission( 4268 android.Manifest.permission.CLEAR_APP_USER_DATA, TAG); 4269 4270 final String packageName = extras.getString(Intent.EXTRA_PACKAGE_NAME); 4271 forEachContributedMedia(packageName, (uri) -> { 4272 delete(uri, null, null); 4273 }); 4274 return null; 4275 } 4276 default: 4277 throw new UnsupportedOperationException("Unsupported call: " + method); 4278 } 4279 } 4280 4281 /** 4282 * Execute the given operation for each media item contributed by given 4283 * package. The meaning of "contributed" means it won't automatically be 4284 * deleted when the app is uninstalled. 4285 */ forEachContributedMedia(String packageName, Consumer<Uri> consumer)4286 private @BytesLong long forEachContributedMedia(String packageName, Consumer<Uri> consumer) { 4287 final DatabaseHelper helper = mExternalDatabase; 4288 final SQLiteDatabase db = helper.getReadableDatabase(); 4289 4290 final SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 4291 qb.setTables("files"); 4292 qb.appendWhere( 4293 DatabaseUtils.bindSelection(FileColumns.OWNER_PACKAGE_NAME + "=?", packageName) 4294 + " AND NOT " + FileColumns.DATA + " REGEXP '" 4295 + PATTERN_OWNED_PATH.pattern() + "'"); 4296 4297 long totalSize = 0; 4298 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4299 try { 4300 try (Cursor c = qb.query(db, new String[] { 4301 FileColumns.VOLUME_NAME, FileColumns._ID, FileColumns.SIZE, FileColumns.DATA 4302 }, null, null, null, null, null, null)) { 4303 while (c.moveToNext()) { 4304 final String volumeName = c.getString(0); 4305 final long id = c.getLong(1); 4306 final long size = c.getLong(2); 4307 final String data = c.getString(3); 4308 4309 Log.d(TAG, "Found " + data + " from " + packageName + " in " 4310 + helper.mName + " with size " + size); 4311 if (consumer != null) { 4312 consumer.accept(Files.getContentUri(volumeName, id)); 4313 } 4314 totalSize += size; 4315 } 4316 } 4317 } finally { 4318 restoreLocalCallingIdentity(token); 4319 } 4320 return totalSize; 4321 } 4322 pruneThumbnails(@onNull CancellationSignal signal)4323 private void pruneThumbnails(@NonNull CancellationSignal signal) { 4324 final DatabaseHelper helper = mExternalDatabase; 4325 final SQLiteDatabase db = helper.getReadableDatabase(); 4326 4327 // Determine all known media items 4328 final LongArray knownIds = new LongArray(); 4329 try (Cursor c = db.query(true, "files", new String[] { BaseColumns._ID }, 4330 null, null, null, null, null, null, signal)) { 4331 while (c.moveToNext()) { 4332 knownIds.add(c.getLong(0)); 4333 } 4334 } 4335 4336 final long[] knownIdsRaw = knownIds.toArray(); 4337 Arrays.sort(knownIdsRaw); 4338 4339 for (String volumeName : getExternalVolumeNames()) { 4340 final File volumePath; 4341 try { 4342 volumePath = getVolumePath(volumeName); 4343 } catch (FileNotFoundException e) { 4344 Log.w(TAG, "Failed to resolve volume " + volumeName, e); 4345 continue; 4346 } 4347 4348 // Reconcile all thumbnails, deleting stale items 4349 for (File thumbDir : new File[] { 4350 buildPath(volumePath, Environment.DIRECTORY_MUSIC, ".thumbnails"), 4351 buildPath(volumePath, Environment.DIRECTORY_MOVIES, ".thumbnails"), 4352 buildPath(volumePath, Environment.DIRECTORY_PICTURES, ".thumbnails"), 4353 }) { 4354 // Possibly bail before digging into each directory 4355 signal.throwIfCanceled(); 4356 4357 for (File thumbFile : FileUtils.listFilesOrEmpty(thumbDir)) { 4358 final String name = ModernMediaScanner.extractName(thumbFile); 4359 try { 4360 final long id = Long.parseLong(name); 4361 if (Arrays.binarySearch(knownIdsRaw, id) >= 0) { 4362 // Thumbnail belongs to known media, keep it 4363 continue; 4364 } 4365 } catch (NumberFormatException e) { 4366 } 4367 4368 Log.v(TAG, "Deleting stale thumbnail " + thumbFile); 4369 thumbFile.delete(); 4370 } 4371 } 4372 } 4373 4374 // Also delete stale items from legacy tables 4375 db.execSQL("delete from thumbnails " 4376 + "where image_id not in (select _id from images)"); 4377 db.execSQL("delete from videothumbnails " 4378 + "where video_id not in (select _id from video)"); 4379 } 4380 4381 static abstract class Thumbnailer { 4382 final String directoryName; 4383 Thumbnailer(String directoryName)4384 public Thumbnailer(String directoryName) { 4385 this.directoryName = directoryName; 4386 } 4387 getThumbnailFile(Uri uri)4388 private File getThumbnailFile(Uri uri) throws IOException { 4389 final String volumeName = resolveVolumeName(uri); 4390 final File volumePath = getVolumePath(volumeName); 4391 return Environment.buildPath(volumePath, directoryName, 4392 ".thumbnails", ContentUris.parseId(uri) + ".jpg"); 4393 } 4394 getThumbnailBitmap(Uri uri, CancellationSignal signal)4395 public abstract Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) 4396 throws IOException; 4397 ensureThumbnail(Uri uri, CancellationSignal signal)4398 public File ensureThumbnail(Uri uri, CancellationSignal signal) throws IOException { 4399 final File thumbFile = getThumbnailFile(uri); 4400 thumbFile.getParentFile().mkdirs(); 4401 if (!thumbFile.exists()) { 4402 final Bitmap thumbnail = getThumbnailBitmap(uri, signal); 4403 try (OutputStream out = new FileOutputStream(thumbFile)) { 4404 thumbnail.compress(Bitmap.CompressFormat.JPEG, 75, out); 4405 } 4406 } 4407 return thumbFile; 4408 } 4409 invalidateThumbnail(Uri uri)4410 public void invalidateThumbnail(Uri uri) throws IOException { 4411 getThumbnailFile(uri).delete(); 4412 } 4413 } 4414 4415 private Thumbnailer mAudioThumbnailer = new Thumbnailer(Environment.DIRECTORY_MUSIC) { 4416 @Override 4417 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 4418 return ThumbnailUtils.createAudioThumbnail(queryForDataFile(uri, signal), 4419 mThumbSize, signal); 4420 } 4421 }; 4422 4423 private Thumbnailer mVideoThumbnailer = new Thumbnailer(Environment.DIRECTORY_MOVIES) { 4424 @Override 4425 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 4426 return ThumbnailUtils.createVideoThumbnail(queryForDataFile(uri, signal), 4427 mThumbSize, signal); 4428 } 4429 }; 4430 4431 private Thumbnailer mImageThumbnailer = new Thumbnailer(Environment.DIRECTORY_PICTURES) { 4432 @Override 4433 public Bitmap getThumbnailBitmap(Uri uri, CancellationSignal signal) throws IOException { 4434 return ThumbnailUtils.createImageThumbnail(queryForDataFile(uri, signal), 4435 mThumbSize, signal); 4436 } 4437 }; 4438 invalidateThumbnails(Uri uri)4439 private void invalidateThumbnails(Uri uri) { 4440 Trace.traceBegin(TRACE_TAG_DATABASE, "invalidateThumbnails"); 4441 try { 4442 invalidateThumbnailsInternal(uri); 4443 } finally { 4444 Trace.traceEnd(TRACE_TAG_DATABASE); 4445 } 4446 } 4447 invalidateThumbnailsInternal(Uri uri)4448 private void invalidateThumbnailsInternal(Uri uri) { 4449 final long id = ContentUris.parseId(uri); 4450 try { 4451 mAudioThumbnailer.invalidateThumbnail(uri); 4452 mVideoThumbnailer.invalidateThumbnail(uri); 4453 mImageThumbnailer.invalidateThumbnail(uri); 4454 } catch (IOException ignored) { 4455 } 4456 4457 final DatabaseHelper helper; 4458 final SQLiteDatabase db; 4459 try { 4460 helper = getDatabaseForUri(uri); 4461 db = helper.getWritableDatabase(); 4462 } catch (VolumeNotFoundException e) { 4463 Log.w(TAG, e); 4464 return; 4465 } 4466 4467 final String idString = Long.toString(id); 4468 try (Cursor c = db.rawQuery("select _data from thumbnails where image_id=?" 4469 + " union all select _data from videothumbnails where video_id=?", 4470 new String[] { idString, idString })) { 4471 while (c.moveToNext()) { 4472 String path = c.getString(0); 4473 deleteIfAllowed(uri, path); 4474 } 4475 } 4476 4477 db.execSQL("delete from thumbnails where image_id=?", new String[] { idString }); 4478 db.execSQL("delete from videothumbnails where video_id=?", new String[] { idString }); 4479 } 4480 4481 @Override update(Uri uri, ContentValues initialValues, String userWhere, String[] userWhereArgs)4482 public int update(Uri uri, ContentValues initialValues, String userWhere, 4483 String[] userWhereArgs) { 4484 Trace.traceBegin(TRACE_TAG_DATABASE, "update"); 4485 try { 4486 return updateInternal(uri, initialValues, userWhere, userWhereArgs); 4487 } finally { 4488 Trace.traceEnd(TRACE_TAG_DATABASE); 4489 } 4490 } 4491 updateInternal(Uri uri, ContentValues initialValues, String userWhere, String[] userWhereArgs)4492 private int updateInternal(Uri uri, ContentValues initialValues, String userWhere, 4493 String[] userWhereArgs) { 4494 if ("com.google.android.GoogleCamera".equals(getCallingPackageOrSelf())) { 4495 if (matchUri(uri, false) == IMAGES_MEDIA_ID) { 4496 Log.w(TAG, "Working around app bug in b/111966296"); 4497 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri)); 4498 } else if (matchUri(uri, false) == VIDEO_MEDIA_ID) { 4499 Log.w(TAG, "Working around app bug in b/112246630"); 4500 uri = MediaStore.Files.getContentUri("external", ContentUris.parseId(uri)); 4501 } 4502 } 4503 4504 uri = safeUncanonicalize(uri); 4505 4506 int count; 4507 4508 final String volumeName = getVolumeName(uri); 4509 final int targetSdkVersion = getCallingPackageTargetSdkVersion(); 4510 final boolean allowHidden = isCallingPackageAllowedHidden(); 4511 final int match = matchUri(uri, allowHidden); 4512 4513 final DatabaseHelper helper; 4514 final SQLiteDatabase db; 4515 try { 4516 helper = getDatabaseForUri(uri); 4517 db = helper.getWritableDatabase(); 4518 } catch (VolumeNotFoundException e) { 4519 return e.translateForUpdateDelete(targetSdkVersion); 4520 } 4521 4522 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, uri, match, null); 4523 4524 // Give callers interacting with a specific media item a chance to 4525 // escalate access if they don't already have it 4526 switch (match) { 4527 case AUDIO_MEDIA_ID: 4528 case VIDEO_MEDIA_ID: 4529 case IMAGES_MEDIA_ID: 4530 enforceCallingPermission(uri, true); 4531 } 4532 4533 boolean triggerInvalidate = false; 4534 boolean triggerScan = false; 4535 String genre = null; 4536 if (initialValues != null) { 4537 // IDs are forever; nobody should be editing them 4538 initialValues.remove(MediaColumns._ID); 4539 4540 // Ignore or augment incoming raw filesystem paths 4541 for (String column : sDataColumns.keySet()) { 4542 if (!initialValues.containsKey(column)) continue; 4543 4544 if (isCallingPackageSystem() || isCallingPackageLegacy()) { 4545 // Mutation allowed 4546 } else { 4547 Log.w(TAG, "Ignoring mutation of " + column + " from " 4548 + getCallingPackageOrSelf()); 4549 initialValues.remove(column); 4550 } 4551 } 4552 4553 if (!isCallingPackageSystem()) { 4554 Trace.traceBegin(TRACE_TAG_DATABASE, "filter"); 4555 4556 // Remote callers have no direct control over owner column; we 4557 // force it be whoever is creating the content. 4558 initialValues.remove(MediaColumns.OWNER_PACKAGE_NAME); 4559 4560 // We default to filtering mutable columns, except when we know 4561 // the single item being updated is pending; when it's finally 4562 // published we'll overwrite these values. 4563 final Uri finalUri = uri; 4564 final Supplier<Boolean> isPending = new CachedSupplier<>(() -> { 4565 return isPending(finalUri); 4566 }); 4567 4568 // Column values controlled by media scanner aren't writable by 4569 // apps, since any edits here don't reflect the metadata on 4570 // disk, and they'd be overwritten during a rescan. 4571 for (String column : new ArraySet<>(initialValues.keySet())) { 4572 if (sMutableColumns.contains(column)) { 4573 // Mutation normally allowed 4574 } else if (isPending.get()) { 4575 // Mutation relaxed while pending 4576 } else { 4577 Log.w(TAG, "Ignoring mutation of " + column + " from " 4578 + getCallingPackageOrSelf()); 4579 initialValues.remove(column); 4580 4581 switch (match) { 4582 default: 4583 triggerScan = true; 4584 break; 4585 // If entry is a playlist, do not re-scan to match previous behavior 4586 // and allow persistence of database-only edits until real re-scan 4587 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 4588 case AUDIO_PLAYLISTS_ID: 4589 break; 4590 } 4591 } 4592 4593 // If we're publishing this item, perform a blocking scan to 4594 // make sure metadata is updated 4595 if (MediaColumns.IS_PENDING.equals(column)) { 4596 triggerScan = true; 4597 } 4598 } 4599 4600 Trace.traceEnd(TRACE_TAG_DATABASE); 4601 } 4602 4603 genre = initialValues.getAsString(Audio.AudioColumns.GENRE); 4604 initialValues.remove(Audio.AudioColumns.GENRE); 4605 4606 if ("files".equals(qb.getTables())) { 4607 maybeMarkAsDownload(initialValues); 4608 } 4609 4610 // We no longer track location metadata 4611 if (initialValues.containsKey(ImageColumns.LATITUDE)) { 4612 initialValues.putNull(ImageColumns.LATITUDE); 4613 } 4614 if (initialValues.containsKey(ImageColumns.LONGITUDE)) { 4615 initialValues.putNull(ImageColumns.LONGITUDE); 4616 } 4617 } 4618 4619 // If we're not updating anything, then we can skip 4620 if (initialValues.isEmpty()) return 0; 4621 4622 final boolean isThumbnail; 4623 switch (match) { 4624 case IMAGES_THUMBNAILS: 4625 case IMAGES_THUMBNAILS_ID: 4626 case VIDEO_THUMBNAILS: 4627 case VIDEO_THUMBNAILS_ID: 4628 case AUDIO_ALBUMART: 4629 case AUDIO_ALBUMART_ID: 4630 isThumbnail = true; 4631 break; 4632 default: 4633 isThumbnail = false; 4634 break; 4635 } 4636 4637 // If we're touching columns that would change placement of a file, 4638 // blend in current values and recalculate path 4639 if (containsAny(initialValues.keySet(), sPlacementColumns) 4640 && !initialValues.containsKey(MediaColumns.DATA) 4641 && !isCallingPackageSystem() 4642 && !isThumbnail) { 4643 Trace.traceBegin(TRACE_TAG_DATABASE, "movement"); 4644 4645 // We only support movement under well-defined collections 4646 switch (match) { 4647 case AUDIO_MEDIA_ID: 4648 case VIDEO_MEDIA_ID: 4649 case IMAGES_MEDIA_ID: 4650 case DOWNLOADS_ID: 4651 break; 4652 default: 4653 throw new IllegalArgumentException("Movement of " + uri 4654 + " which isn't part of well-defined collection not allowed"); 4655 } 4656 4657 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4658 try (Cursor c = queryForSingleItem(uri, 4659 sPlacementColumns.toArray(EmptyArray.STRING), userWhere, userWhereArgs, null)) { 4660 for (int i = 0; i < c.getColumnCount(); i++) { 4661 final String column = c.getColumnName(i); 4662 if (!initialValues.containsKey(column)) { 4663 initialValues.put(column, c.getString(i)); 4664 } 4665 } 4666 } catch (FileNotFoundException e) { 4667 throw new IllegalStateException(e); 4668 } finally { 4669 restoreLocalCallingIdentity(token); 4670 } 4671 4672 // Regenerate path using blended values; this will throw if caller 4673 // is attempting to place file into invalid location 4674 final String beforePath = initialValues.getAsString(MediaColumns.DATA); 4675 final String beforeVolume = extractVolumeName(beforePath); 4676 final String beforeOwner = extractPathOwnerPackageName(beforePath); 4677 initialValues.remove(MediaColumns.DATA); 4678 try { 4679 ensureNonUniqueFileColumns(match, uri, initialValues, beforePath); 4680 } catch (VolumeArgumentException e) { 4681 return e.translateForUpdateDelete(targetSdkVersion); 4682 } 4683 4684 final String probePath = initialValues.getAsString(MediaColumns.DATA); 4685 final String probeVolume = extractVolumeName(probePath); 4686 final String probeOwner = extractPathOwnerPackageName(probePath); 4687 if (Objects.equals(beforePath, probePath)) { 4688 Log.d(TAG, "Identical paths " + beforePath + "; not moving"); 4689 } else if (!Objects.equals(beforeVolume, probeVolume)) { 4690 throw new IllegalArgumentException("Changing volume from " + beforePath + " to " 4691 + probePath + " not allowed"); 4692 } else if (!Objects.equals(beforeOwner, probeOwner)) { 4693 throw new IllegalArgumentException("Changing ownership from " + beforePath + " to " 4694 + probePath + " not allowed"); 4695 } else { 4696 // Now that we've confirmed an actual movement is taking place, 4697 // ensure we have a unique destination 4698 initialValues.remove(MediaColumns.DATA); 4699 try { 4700 ensureUniqueFileColumns(match, uri, initialValues); 4701 } catch (VolumeArgumentException e) { 4702 return e.translateForUpdateDelete(targetSdkVersion); 4703 } 4704 final String afterPath = initialValues.getAsString(MediaColumns.DATA); 4705 4706 Log.d(TAG, "Moving " + beforePath + " to " + afterPath); 4707 try { 4708 Os.rename(beforePath, afterPath); 4709 } catch (ErrnoException e) { 4710 throw new IllegalStateException(e); 4711 } 4712 initialValues.put(MediaColumns.DATA, afterPath); 4713 } 4714 4715 Trace.traceEnd(TRACE_TAG_DATABASE); 4716 } 4717 4718 // Make sure any updated paths look sane 4719 try { 4720 assertFileColumnsSane(match, uri, initialValues); 4721 } catch (VolumeArgumentException e) { 4722 return e.translateForUpdateDelete(targetSdkVersion); 4723 } 4724 4725 // if the media type is being changed, check if it's being changed from image or video 4726 // to something else 4727 if (initialValues.containsKey(FileColumns.MEDIA_TYPE)) { 4728 final int newMediaType = initialValues.getAsInteger(FileColumns.MEDIA_TYPE); 4729 4730 // If we're changing media types, invalidate any cached "empty" 4731 // answers for the new collection type. 4732 MediaDocumentsProvider.onMediaStoreInsert( 4733 getContext(), volumeName, newMediaType, -1); 4734 4735 // If we're changing media types, invalidate any thumbnails 4736 triggerInvalidate = true; 4737 } 4738 4739 if (initialValues.containsKey(FileColumns.DATA)) { 4740 // If we're changing paths, invalidate any thumbnails 4741 triggerInvalidate = true; 4742 } 4743 4744 // Since the update mutation may prevent us from matching items after 4745 // it's applied, we need to snapshot affected IDs here 4746 final LongArray updatedIds = new LongArray(); 4747 if (triggerInvalidate || triggerScan) { 4748 Trace.traceBegin(TRACE_TAG_DATABASE, "snapshot"); 4749 final LocalCallingIdentity token = clearLocalCallingIdentity(); 4750 try (Cursor c = qb.query(db, new String[] { FileColumns._ID }, 4751 userWhere, userWhereArgs, null, null, null)) { 4752 while (c.moveToNext()) { 4753 updatedIds.add(c.getLong(0)); 4754 } 4755 } finally { 4756 restoreLocalCallingIdentity(token); 4757 Trace.traceEnd(TRACE_TAG_DATABASE); 4758 } 4759 } 4760 4761 // special case renaming directories via MTP. 4762 // in this case we must update all paths in the database with 4763 // the directory name as a prefix 4764 if ((match == MTP_OBJECTS || match == MTP_OBJECTS_ID || match == FILES_DIRECTORY) 4765 && initialValues != null 4766 // Is a rename operation 4767 && ((initialValues.size() == 1 && initialValues.containsKey(FileColumns.DATA)) 4768 // Is a move operation 4769 || (initialValues.size() == 2 && initialValues.containsKey(FileColumns.DATA) 4770 && initialValues.containsKey(FileColumns.PARENT)))) { 4771 String oldPath = null; 4772 String newPath = initialValues.getAsString(MediaStore.MediaColumns.DATA); 4773 synchronized (mDirectoryCache) { 4774 mDirectoryCache.remove(newPath); 4775 } 4776 // MtpDatabase will rename the directory first, so we test the new file name 4777 File f = new File(newPath); 4778 if (newPath != null && f.isDirectory()) { 4779 Cursor cursor = qb.query(db, PATH_PROJECTION, userWhere, userWhereArgs, null, null, 4780 null, null); 4781 try { 4782 if (cursor != null && cursor.moveToNext()) { 4783 oldPath = cursor.getString(1); 4784 } 4785 } finally { 4786 IoUtils.closeQuietly(cursor); 4787 } 4788 final boolean isDownload = isDownload(newPath); 4789 if (oldPath != null) { 4790 synchronized (mDirectoryCache) { 4791 mDirectoryCache.remove(oldPath); 4792 } 4793 final boolean wasDownload = isDownload(oldPath); 4794 // first rename the row for the directory 4795 count = qb.update(db, initialValues, userWhere, userWhereArgs); 4796 if (count > 0) { 4797 // update the paths of any files and folders contained in the directory 4798 Object[] bindArgs = new Object[] { 4799 newPath, 4800 oldPath.length() + 1, 4801 oldPath + "/", 4802 oldPath + "0", 4803 // update bucket_display_name and bucket_id based on new path 4804 f.getName(), 4805 f.toString().toLowerCase().hashCode(), 4806 isDownload 4807 }; 4808 db.execSQL("UPDATE files SET _data=?1||SUBSTR(_data, ?2)" + 4809 // also update bucket_display_name 4810 ",bucket_display_name=?5" + 4811 ",bucket_id=?6" + 4812 ",is_download=?7" + 4813 " WHERE _data >= ?3 AND _data < ?4;", 4814 bindArgs); 4815 } 4816 4817 if (count > 0) { 4818 acceptWithExpansion(helper::notifyChange, uri); 4819 } 4820 if (f.getName().startsWith(".")) { 4821 MediaScanner.instance(getContext()).scanFile(new File(newPath)); 4822 } 4823 return count; 4824 } 4825 } else if (newPath.toLowerCase(Locale.US).endsWith("/.nomedia")) { 4826 MediaScanner.instance(getContext()).scanFile(new File(newPath).getParentFile()); 4827 } 4828 } 4829 4830 switch (match) { 4831 case AUDIO_MEDIA: 4832 case AUDIO_MEDIA_ID: 4833 { 4834 ContentValues values = new ContentValues(initialValues); 4835 String albumartist = values.getAsString(MediaStore.Audio.Media.ALBUM_ARTIST); 4836 String compilation = values.getAsString(MediaStore.Audio.Media.COMPILATION); 4837 values.remove(MediaStore.Audio.Media.COMPILATION); 4838 4839 // Insert the artist into the artist table and remove it from 4840 // the input values 4841 String artist = values.getAsString("artist"); 4842 values.remove("artist"); 4843 if (artist != null) { 4844 long artistRowId; 4845 ArrayMap<String, Long> artistCache = helper.mArtistCache; 4846 synchronized(artistCache) { 4847 Long temp = artistCache.get(artist); 4848 if (temp == null) { 4849 artistRowId = getKeyIdForName(helper, db, 4850 "artists", "artist_key", "artist", 4851 artist, artist, null, 0, null, artistCache, uri); 4852 } else { 4853 artistRowId = temp.longValue(); 4854 } 4855 } 4856 values.put("artist_id", Integer.toString((int)artistRowId)); 4857 } 4858 4859 // Do the same for the album field. 4860 String so = values.getAsString("album"); 4861 values.remove("album"); 4862 if (so != null) { 4863 String path = values.getAsString(MediaStore.MediaColumns.DATA); 4864 int albumHash = 0; 4865 if (albumartist != null) { 4866 albumHash = albumartist.hashCode(); 4867 } else if (compilation != null && compilation.equals("1")) { 4868 // nothing to do, hash already set 4869 } else { 4870 if (path == null) { 4871 if (match == AUDIO_MEDIA) { 4872 Log.w(TAG, "Possible multi row album name update without" 4873 + " path could give wrong album key"); 4874 } else { 4875 //Log.w(TAG, "Specify path to avoid extra query"); 4876 Cursor c = query(uri, 4877 new String[] { MediaStore.Audio.Media.DATA}, 4878 null, null, null); 4879 if (c != null) { 4880 try { 4881 int numrows = c.getCount(); 4882 if (numrows == 1) { 4883 c.moveToFirst(); 4884 path = c.getString(0); 4885 } else { 4886 Log.e(TAG, "" + numrows + " rows for " + uri); 4887 } 4888 } finally { 4889 IoUtils.closeQuietly(c); 4890 } 4891 } 4892 } 4893 } 4894 if (path != null) { 4895 albumHash = path.substring(0, path.lastIndexOf('/')).hashCode(); 4896 } 4897 } 4898 4899 String s = so.toString(); 4900 long albumRowId; 4901 ArrayMap<String, Long> albumCache = helper.mAlbumCache; 4902 synchronized(albumCache) { 4903 String cacheName = s + albumHash; 4904 Long temp = albumCache.get(cacheName); 4905 if (temp == null) { 4906 albumRowId = getKeyIdForName(helper, db, 4907 "albums", "album_key", "album", 4908 s, cacheName, path, albumHash, artist, albumCache, uri); 4909 } else { 4910 albumRowId = temp.longValue(); 4911 } 4912 } 4913 values.put("album_id", Integer.toString((int)albumRowId)); 4914 } 4915 4916 // don't allow the title_key field to be updated directly 4917 values.remove("title_key"); 4918 // If the title field is modified, update the title_key 4919 so = values.getAsString("title"); 4920 if (so != null) { 4921 try { 4922 final String localizedTitle = getLocalizedTitle(so); 4923 if (localizedTitle != null) { 4924 values.put("title_resource_uri", so); 4925 so = localizedTitle; 4926 } else { 4927 values.putNull("title_resource_uri"); 4928 } 4929 } catch (Exception e) { 4930 values.put("title_resource_uri", so); 4931 } 4932 values.put("title_key", MediaStore.Audio.keyFor(so)); 4933 // do a final trim of the title, in case it started with the special 4934 // "sort first" character (ascii \001) 4935 values.put("title", so.trim()); 4936 } 4937 4938 count = qb.update(db, values, userWhere, userWhereArgs); 4939 if (genre != null) { 4940 if (count == 1 && match == AUDIO_MEDIA_ID) { 4941 long rowId = Long.parseLong(uri.getPathSegments().get(3)); 4942 updateGenre(rowId, genre, volumeName); 4943 } else { 4944 // can't handle genres for bulk update or for non-audio files 4945 Log.w(TAG, "ignoring genre in update: count = " 4946 + count + " match = " + match); 4947 } 4948 } 4949 } 4950 break; 4951 case IMAGES_MEDIA: 4952 case IMAGES_MEDIA_ID: 4953 case VIDEO_MEDIA: 4954 case VIDEO_MEDIA_ID: 4955 { 4956 ContentValues values = new ContentValues(initialValues); 4957 // Don't allow bucket id or display name to be updated directly. 4958 // The same names are used for both images and table columns, so 4959 // we use the ImageColumns constants here. 4960 values.remove(ImageColumns.BUCKET_ID); 4961 values.remove(ImageColumns.BUCKET_DISPLAY_NAME); 4962 // If the data is being modified update the bucket values 4963 computeDataValues(values); 4964 count = qb.update(db, values, userWhere, userWhereArgs); 4965 } 4966 break; 4967 4968 case AUDIO_MEDIA_ID_PLAYLISTS_ID: 4969 case AUDIO_PLAYLISTS_ID: 4970 long playlistId = ContentUris.parseId(uri); 4971 count = qb.update(db, initialValues, userWhere, userWhereArgs); 4972 if (count > 0) { 4973 updatePlaylistDateModifiedToNow(db, playlistId); 4974 } 4975 break; 4976 case AUDIO_PLAYLISTS_ID_MEMBERS: 4977 long playlistIdMembers = Long.parseLong(uri.getPathSegments().get(3)); 4978 count = qb.update(db, initialValues, userWhere, userWhereArgs); 4979 if (count > 0) { 4980 updatePlaylistDateModifiedToNow(db, playlistIdMembers); 4981 } 4982 break; 4983 case AUDIO_PLAYLISTS_ID_MEMBERS_ID: 4984 String moveit = uri.getQueryParameter("move"); 4985 if (moveit != null) { 4986 String key = MediaStore.Audio.Playlists.Members.PLAY_ORDER; 4987 if (initialValues.containsKey(key)) { 4988 int newpos = initialValues.getAsInteger(key); 4989 List <String> segments = uri.getPathSegments(); 4990 long playlist = Long.parseLong(segments.get(3)); 4991 int oldpos = Integer.parseInt(segments.get(5)); 4992 int rowsChanged = movePlaylistEntry(volumeName, helper, db, playlist, oldpos, newpos); 4993 if (rowsChanged > 0) { 4994 updatePlaylistDateModifiedToNow(db, playlist); 4995 } 4996 4997 return rowsChanged; 4998 } 4999 throw new IllegalArgumentException("Need to specify " + key + 5000 " when using 'move' parameter"); 5001 } 5002 // fall through 5003 default: 5004 count = qb.update(db, initialValues, userWhere, userWhereArgs); 5005 break; 5006 } 5007 5008 // If the caller tried (and failed) to update metadata, the file on disk 5009 // might have changed, to scan it to collect the latest metadata. 5010 if (triggerInvalidate || triggerScan) { 5011 Trace.traceBegin(TRACE_TAG_DATABASE, "invalidate"); 5012 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5013 try { 5014 for (int i = 0; i < updatedIds.size(); i++) { 5015 final long updatedId = updatedIds.get(i); 5016 final Uri updatedUri = Files.getContentUri(volumeName, updatedId); 5017 BackgroundThread.getExecutor().execute(() -> { 5018 invalidateThumbnails(updatedUri); 5019 }); 5020 5021 if (triggerScan) { 5022 try (Cursor c = queryForSingleItem(updatedUri, 5023 new String[] { FileColumns.DATA }, null, null, null)) { 5024 MediaScanner.instance(getContext()).scanFile(new File(c.getString(0))); 5025 } catch (Exception e) { 5026 Log.w(TAG, "Failed to update metadata for " + updatedUri, e); 5027 } 5028 } 5029 } 5030 } finally { 5031 restoreLocalCallingIdentity(token); 5032 Trace.traceEnd(TRACE_TAG_DATABASE); 5033 } 5034 } 5035 5036 if (count > 0) { 5037 acceptWithExpansion(helper::notifyChange, uri); 5038 } 5039 return count; 5040 } 5041 movePlaylistEntry(String volumeName, DatabaseHelper helper, SQLiteDatabase db, long playlist, int from, int to)5042 private int movePlaylistEntry(String volumeName, DatabaseHelper helper, SQLiteDatabase db, 5043 long playlist, int from, int to) { 5044 if (from == to) { 5045 return 0; 5046 } 5047 db.beginTransaction(); 5048 int numlines = 0; 5049 Cursor c = null; 5050 try { 5051 c = db.query("audio_playlists_map", 5052 new String [] {"play_order" }, 5053 "playlist_id=?", new String[] {"" + playlist}, null, null, "play_order", 5054 from + ",1"); 5055 c.moveToFirst(); 5056 int from_play_order = c.getInt(0); 5057 IoUtils.closeQuietly(c); 5058 c = db.query("audio_playlists_map", 5059 new String [] {"play_order" }, 5060 "playlist_id=?", new String[] {"" + playlist}, null, null, "play_order", 5061 to + ",1"); 5062 c.moveToFirst(); 5063 int to_play_order = c.getInt(0); 5064 db.execSQL("UPDATE audio_playlists_map SET play_order=-1" + 5065 " WHERE play_order=" + from_play_order + 5066 " AND playlist_id=" + playlist); 5067 // We could just run both of the next two statements, but only one of 5068 // of them will actually do anything, so might as well skip the compile 5069 // and execute steps. 5070 if (from < to) { 5071 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order-1" + 5072 " WHERE play_order<=" + to_play_order + 5073 " AND play_order>" + from_play_order + 5074 " AND playlist_id=" + playlist); 5075 numlines = to - from + 1; 5076 } else { 5077 db.execSQL("UPDATE audio_playlists_map SET play_order=play_order+1" + 5078 " WHERE play_order>=" + to_play_order + 5079 " AND play_order<" + from_play_order + 5080 " AND playlist_id=" + playlist); 5081 numlines = from - to + 1; 5082 } 5083 db.execSQL("UPDATE audio_playlists_map SET play_order=" + to_play_order + 5084 " WHERE play_order=-1 AND playlist_id=" + playlist); 5085 db.setTransactionSuccessful(); 5086 } finally { 5087 db.endTransaction(); 5088 IoUtils.closeQuietly(c); 5089 } 5090 5091 Uri uri = ContentUris.withAppendedId( 5092 MediaStore.Audio.Playlists.getContentUri(volumeName), playlist); 5093 // notifyChange() must be called after the database transaction is ended 5094 // or the listeners will read the old data in the callback 5095 getContext().getContentResolver().notifyChange(uri, null); 5096 5097 return numlines; 5098 } 5099 updatePlaylistDateModifiedToNow(SQLiteDatabase database, long playlistId)5100 private void updatePlaylistDateModifiedToNow(SQLiteDatabase database, long playlistId) { 5101 ContentValues values = new ContentValues(); 5102 values.put( 5103 FileColumns.DATE_MODIFIED, 5104 TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) 5105 ); 5106 5107 database.update( 5108 MediaStore.Files.TABLE, 5109 values, 5110 MediaStore.Files.FileColumns._ID + "=?", 5111 new String[]{String.valueOf(playlistId)} 5112 ); 5113 } 5114 5115 @Override openFile(Uri uri, String mode)5116 public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { 5117 return openFileCommon(uri, mode, null); 5118 } 5119 5120 @Override openFile(Uri uri, String mode, CancellationSignal signal)5121 public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) 5122 throws FileNotFoundException { 5123 return openFileCommon(uri, mode, signal); 5124 } 5125 openFileCommon(Uri uri, String mode, CancellationSignal signal)5126 private ParcelFileDescriptor openFileCommon(Uri uri, String mode, CancellationSignal signal) 5127 throws FileNotFoundException { 5128 uri = safeUncanonicalize(uri); 5129 5130 final boolean allowHidden = isCallingPackageAllowedHidden(); 5131 final int match = matchUri(uri, allowHidden); 5132 final String volumeName = getVolumeName(uri); 5133 5134 // Handle some legacy cases where we need to redirect thumbnails 5135 switch (match) { 5136 case AUDIO_ALBUMART_ID: { 5137 final long albumId = Long.parseLong(uri.getPathSegments().get(3)); 5138 final Uri targetUri = ContentUris 5139 .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId); 5140 return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal), 5141 ParcelFileDescriptor.MODE_READ_ONLY); 5142 5143 } 5144 case AUDIO_ALBUMART_FILE_ID: { 5145 final long audioId = Long.parseLong(uri.getPathSegments().get(3)); 5146 final Uri targetUri = ContentUris 5147 .withAppendedId(Audio.Media.getContentUri(volumeName), audioId); 5148 return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal), 5149 ParcelFileDescriptor.MODE_READ_ONLY); 5150 } 5151 case VIDEO_MEDIA_ID_THUMBNAIL: { 5152 final long videoId = Long.parseLong(uri.getPathSegments().get(3)); 5153 final Uri targetUri = ContentUris 5154 .withAppendedId(Video.Media.getContentUri(volumeName), videoId); 5155 return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal), 5156 ParcelFileDescriptor.MODE_READ_ONLY); 5157 } 5158 case IMAGES_MEDIA_ID_THUMBNAIL: { 5159 final long imageId = Long.parseLong(uri.getPathSegments().get(3)); 5160 final Uri targetUri = ContentUris 5161 .withAppendedId(Images.Media.getContentUri(volumeName), imageId); 5162 return ParcelFileDescriptor.open(ensureThumbnail(targetUri, signal), 5163 ParcelFileDescriptor.MODE_READ_ONLY); 5164 } 5165 } 5166 5167 return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal); 5168 } 5169 5170 @Override openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts)5171 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts) 5172 throws FileNotFoundException { 5173 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, null); 5174 } 5175 5176 @Override openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)5177 public AssetFileDescriptor openTypedAssetFile(Uri uri, String mimeTypeFilter, Bundle opts, 5178 CancellationSignal signal) throws FileNotFoundException { 5179 return openTypedAssetFileCommon(uri, mimeTypeFilter, opts, signal); 5180 } 5181 openTypedAssetFileCommon(Uri uri, String mimeTypeFilter, Bundle opts, CancellationSignal signal)5182 private AssetFileDescriptor openTypedAssetFileCommon(Uri uri, String mimeTypeFilter, 5183 Bundle opts, CancellationSignal signal) throws FileNotFoundException { 5184 uri = safeUncanonicalize(uri); 5185 5186 // TODO: enforce that caller has access to this uri 5187 5188 // Offer thumbnail of media, when requested 5189 final boolean wantsThumb = (opts != null) && opts.containsKey(ContentResolver.EXTRA_SIZE) 5190 && (mimeTypeFilter != null) && mimeTypeFilter.startsWith("image/"); 5191 if (wantsThumb) { 5192 final File thumbFile = ensureThumbnail(uri, signal); 5193 return new AssetFileDescriptor( 5194 ParcelFileDescriptor.open(thumbFile, ParcelFileDescriptor.MODE_READ_ONLY), 5195 0, AssetFileDescriptor.UNKNOWN_LENGTH); 5196 } 5197 5198 // Worst case, return the underlying file 5199 return new AssetFileDescriptor(openFileCommon(uri, "r", signal), 0, 5200 AssetFileDescriptor.UNKNOWN_LENGTH); 5201 } 5202 ensureThumbnail(Uri uri, CancellationSignal signal)5203 private File ensureThumbnail(Uri uri, CancellationSignal signal) throws FileNotFoundException { 5204 final boolean allowHidden = isCallingPackageAllowedHidden(); 5205 final int match = matchUri(uri, allowHidden); 5206 5207 Trace.traceBegin(TRACE_TAG_DATABASE, "ensureThumbnail"); 5208 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5209 try { 5210 final File thumbFile; 5211 switch (match) { 5212 case AUDIO_ALBUMS_ID: { 5213 final String volumeName = MediaStore.getVolumeName(uri); 5214 final Uri baseUri = MediaStore.Audio.Media.getContentUri(volumeName); 5215 final long albumId = ContentUris.parseId(uri); 5216 try (Cursor c = query(baseUri, new String[] { MediaStore.Audio.Media._ID }, 5217 MediaStore.Audio.Media.ALBUM_ID + "=" + albumId, null, null, signal)) { 5218 if (c.moveToFirst()) { 5219 final long audioId = c.getLong(0); 5220 final Uri targetUri = ContentUris.withAppendedId(baseUri, audioId); 5221 return mAudioThumbnailer.ensureThumbnail(targetUri, signal); 5222 } else { 5223 throw new FileNotFoundException("No media for album " + uri); 5224 } 5225 } 5226 } 5227 case AUDIO_MEDIA_ID: 5228 return mAudioThumbnailer.ensureThumbnail(uri, signal); 5229 case VIDEO_MEDIA_ID: 5230 return mVideoThumbnailer.ensureThumbnail(uri, signal); 5231 case IMAGES_MEDIA_ID: 5232 return mImageThumbnailer.ensureThumbnail(uri, signal); 5233 default: 5234 throw new FileNotFoundException(); 5235 } 5236 } catch (IOException e) { 5237 Log.w(TAG, e); 5238 throw new FileNotFoundException(e.getMessage()); 5239 } finally { 5240 restoreLocalCallingIdentity(token); 5241 Trace.traceEnd(TRACE_TAG_DATABASE); 5242 } 5243 } 5244 5245 /** 5246 * Update the metadata columns for the image residing at given {@link Uri} 5247 * by reading data from the underlying image. 5248 */ updateImageMetadata(ContentValues values, File file)5249 private void updateImageMetadata(ContentValues values, File file) { 5250 final BitmapFactory.Options bitmapOpts = new BitmapFactory.Options(); 5251 bitmapOpts.inJustDecodeBounds = true; 5252 BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOpts); 5253 5254 values.put(MediaColumns.WIDTH, bitmapOpts.outWidth); 5255 values.put(MediaColumns.HEIGHT, bitmapOpts.outHeight); 5256 } 5257 5258 /** 5259 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 5260 */ queryForDataFile(Uri uri, CancellationSignal signal)5261 File queryForDataFile(Uri uri, CancellationSignal signal) 5262 throws FileNotFoundException { 5263 return queryForDataFile(uri, null, null, signal); 5264 } 5265 5266 /** 5267 * Return the {@link MediaColumns#DATA} field for the given {@code Uri}. 5268 */ queryForDataFile(Uri uri, String selection, String[] selectionArgs, CancellationSignal signal)5269 File queryForDataFile(Uri uri, String selection, String[] selectionArgs, 5270 CancellationSignal signal) throws FileNotFoundException { 5271 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns.DATA }, 5272 selection, selectionArgs, signal)) { 5273 final String data = cursor.getString(0); 5274 if (TextUtils.isEmpty(data)) { 5275 throw new FileNotFoundException("Missing path for " + uri); 5276 } else { 5277 return new File(data); 5278 } 5279 } 5280 } 5281 5282 /** 5283 * Return the {@link Uri} for the given {@code File}. 5284 */ queryForMediaUri(File file, CancellationSignal signal)5285 Uri queryForMediaUri(File file, CancellationSignal signal) throws FileNotFoundException { 5286 final String volumeName = MediaStore.getVolumeName(file); 5287 final Uri uri = Files.getContentUri(volumeName); 5288 try (Cursor cursor = queryForSingleItem(uri, new String[] { MediaColumns._ID }, 5289 MediaColumns.DATA + "=?", new String[] { file.getAbsolutePath() }, signal)) { 5290 return ContentUris.withAppendedId(uri, cursor.getLong(0)); 5291 } 5292 } 5293 5294 /** 5295 * Query the given {@link Uri}, expecting only a single item to be found. 5296 * 5297 * @throws FileNotFoundException if no items were found, or multiple items 5298 * were found, or there was trouble reading the data. 5299 */ queryForSingleItem(Uri uri, String[] projection, String selection, String[] selectionArgs, CancellationSignal signal)5300 Cursor queryForSingleItem(Uri uri, String[] projection, String selection, 5301 String[] selectionArgs, CancellationSignal signal) throws FileNotFoundException { 5302 final Cursor c = query(uri, projection, 5303 ContentResolver.createSqlQueryBundle(selection, selectionArgs, null), signal); 5304 if (c == null) { 5305 throw new FileNotFoundException("Missing cursor for " + uri); 5306 } else if (c.getCount() < 1) { 5307 IoUtils.closeQuietly(c); 5308 throw new FileNotFoundException("No item at " + uri); 5309 } else if (c.getCount() > 1) { 5310 IoUtils.closeQuietly(c); 5311 throw new FileNotFoundException("Multiple items at " + uri); 5312 } 5313 5314 if (c.moveToFirst()) { 5315 return c; 5316 } else { 5317 IoUtils.closeQuietly(c); 5318 throw new FileNotFoundException("Failed to read row from " + uri); 5319 } 5320 } 5321 5322 /** 5323 * Replacement for {@link #openFileHelper(Uri, String)} which enforces any 5324 * permissions applicable to the path before returning. 5325 */ openFileAndEnforcePathPermissionsHelper(Uri uri, int match, String mode, CancellationSignal signal)5326 private ParcelFileDescriptor openFileAndEnforcePathPermissionsHelper(Uri uri, int match, 5327 String mode, CancellationSignal signal) throws FileNotFoundException { 5328 final int modeBits = ParcelFileDescriptor.parseMode(mode); 5329 final boolean forWrite = (modeBits != ParcelFileDescriptor.MODE_READ_ONLY); 5330 5331 final boolean hasOwnerPackageName = hasOwnerPackageName(uri); 5332 final String[] projection = new String[] { 5333 MediaColumns.DATA, 5334 hasOwnerPackageName ? MediaColumns.OWNER_PACKAGE_NAME : "NULL", 5335 hasOwnerPackageName ? MediaColumns.IS_PENDING : "0", 5336 }; 5337 5338 final File file; 5339 final String ownerPackageName; 5340 final boolean isPending; 5341 final LocalCallingIdentity token = clearLocalCallingIdentity(); 5342 try (Cursor c = queryForSingleItem(uri, projection, null, null, signal)) { 5343 final String data = c.getString(0); 5344 if (TextUtils.isEmpty(data)) { 5345 throw new FileNotFoundException("Missing path for " + uri); 5346 } else { 5347 file = new File(data).getCanonicalFile(); 5348 } 5349 ownerPackageName = c.getString(1); 5350 isPending = c.getInt(2) != 0; 5351 } catch (IOException e) { 5352 throw new FileNotFoundException(e.toString()); 5353 } finally { 5354 restoreLocalCallingIdentity(token); 5355 } 5356 5357 checkAccess(uri, file, forWrite); 5358 5359 // Require ownership if item is still pending 5360 final boolean hasOwner = (ownerPackageName != null); 5361 final boolean callerIsOwner = Objects.equals(getCallingPackageOrSelf(), ownerPackageName); 5362 if (isPending && hasOwner && !callerIsOwner) { 5363 throw new IllegalStateException( 5364 "Only owner is able to interact with pending media " + uri); 5365 } 5366 5367 // Figure out if we need to redact contents 5368 final boolean redactionNeeded = callerIsOwner ? false : isRedactionNeeded(uri); 5369 final RedactionInfo redactionInfo = redactionNeeded ? getRedactionRanges(file) 5370 : new RedactionInfo(EmptyArray.LONG, EmptyArray.LONG); 5371 5372 // Yell if caller requires original, since we can't give it to them 5373 // unless they have access granted above 5374 if (redactionNeeded 5375 && parseBoolean(uri.getQueryParameter(MediaStore.PARAM_REQUIRE_ORIGINAL))) { 5376 throw new UnsupportedOperationException( 5377 "Caller must hold ACCESS_MEDIA_LOCATION permission to access original"); 5378 } 5379 5380 // Kick off metadata update when writing is finished 5381 final OnCloseListener listener = (e) -> { 5382 // We always update metadata to reflect the state on disk, even when 5383 // the remote writer tried claiming an exception 5384 invalidateThumbnails(uri); 5385 5386 try { 5387 switch (match) { 5388 case IMAGES_THUMBNAILS_ID: 5389 case VIDEO_THUMBNAILS_ID: 5390 final ContentValues values = new ContentValues(); 5391 updateImageMetadata(values, file); 5392 update(uri, values, null, null); 5393 break; 5394 default: 5395 MediaScanner.instance(getContext()).scanFile(file); 5396 break; 5397 } 5398 } catch (Exception e2) { 5399 Log.w(TAG, "Failed to update metadata for " + uri, e2); 5400 } 5401 }; 5402 5403 try { 5404 // First, handle any redaction that is needed for caller 5405 final ParcelFileDescriptor pfd; 5406 if (redactionInfo.redactionRanges.length > 0) { 5407 pfd = RedactingFileDescriptor.open( 5408 getContext(), 5409 file, 5410 modeBits, 5411 redactionInfo.redactionRanges, 5412 redactionInfo.freeOffsets); 5413 } else { 5414 pfd = ParcelFileDescriptor.open(file, modeBits); 5415 } 5416 5417 // Second, wrap in any listener that we've requested 5418 if (!isPending && forWrite && listener != null) { 5419 return ParcelFileDescriptor.fromPfd(pfd, BackgroundThread.getHandler(), listener); 5420 } else { 5421 return pfd; 5422 } 5423 } catch (IOException e) { 5424 if (e instanceof FileNotFoundException) { 5425 throw (FileNotFoundException) e; 5426 } else { 5427 throw new IllegalStateException(e); 5428 } 5429 } 5430 } 5431 deleteIfAllowed(Uri uri, String path)5432 private void deleteIfAllowed(Uri uri, String path) { 5433 try { 5434 final File file = new File(path); 5435 checkAccess(uri, file, true); 5436 file.delete(); 5437 } catch (Exception e) { 5438 Log.e(TAG, "Couldn't delete " + path, e); 5439 } 5440 } 5441 5442 @Deprecated isPending(Uri uri)5443 private boolean isPending(Uri uri) { 5444 final int match = matchUri(uri, true); 5445 switch (match) { 5446 case AUDIO_MEDIA_ID: 5447 case VIDEO_MEDIA_ID: 5448 case IMAGES_MEDIA_ID: 5449 try (Cursor c = queryForSingleItem(uri, 5450 new String[] { MediaColumns.IS_PENDING }, null, null, null)) { 5451 return (c.getInt(0) != 0); 5452 } catch (FileNotFoundException e) { 5453 throw new IllegalStateException(e); 5454 } 5455 default: 5456 return false; 5457 } 5458 } 5459 5460 @Deprecated isRedactionNeeded(Uri uri)5461 private boolean isRedactionNeeded(Uri uri) { 5462 return mCallingIdentity.get().hasPermission(PERMISSION_IS_REDACTION_NEEDED); 5463 } 5464 5465 /** 5466 * Set of Exif tags that should be considered for redaction. 5467 */ 5468 private static final String[] REDACTED_EXIF_TAGS = new String[] { 5469 ExifInterface.TAG_GPS_ALTITUDE, 5470 ExifInterface.TAG_GPS_ALTITUDE_REF, 5471 ExifInterface.TAG_GPS_AREA_INFORMATION, 5472 ExifInterface.TAG_GPS_DOP, 5473 ExifInterface.TAG_GPS_DATESTAMP, 5474 ExifInterface.TAG_GPS_DEST_BEARING, 5475 ExifInterface.TAG_GPS_DEST_BEARING_REF, 5476 ExifInterface.TAG_GPS_DEST_DISTANCE, 5477 ExifInterface.TAG_GPS_DEST_DISTANCE_REF, 5478 ExifInterface.TAG_GPS_DEST_LATITUDE, 5479 ExifInterface.TAG_GPS_DEST_LATITUDE_REF, 5480 ExifInterface.TAG_GPS_DEST_LONGITUDE, 5481 ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, 5482 ExifInterface.TAG_GPS_DIFFERENTIAL, 5483 ExifInterface.TAG_GPS_IMG_DIRECTION, 5484 ExifInterface.TAG_GPS_IMG_DIRECTION_REF, 5485 ExifInterface.TAG_GPS_LATITUDE, 5486 ExifInterface.TAG_GPS_LATITUDE_REF, 5487 ExifInterface.TAG_GPS_LONGITUDE, 5488 ExifInterface.TAG_GPS_LONGITUDE_REF, 5489 ExifInterface.TAG_GPS_MAP_DATUM, 5490 ExifInterface.TAG_GPS_MEASURE_MODE, 5491 ExifInterface.TAG_GPS_PROCESSING_METHOD, 5492 ExifInterface.TAG_GPS_SATELLITES, 5493 ExifInterface.TAG_GPS_SPEED, 5494 ExifInterface.TAG_GPS_SPEED_REF, 5495 ExifInterface.TAG_GPS_STATUS, 5496 ExifInterface.TAG_GPS_TIMESTAMP, 5497 ExifInterface.TAG_GPS_TRACK, 5498 ExifInterface.TAG_GPS_TRACK_REF, 5499 ExifInterface.TAG_GPS_VERSION_ID, 5500 }; 5501 5502 /** 5503 * Set of ISO boxes that should be considered for redaction. 5504 */ 5505 private static final int[] REDACTED_ISO_BOXES = new int[] { 5506 IsoInterface.BOX_LOCI, 5507 IsoInterface.BOX_XYZ, 5508 IsoInterface.BOX_GPS, 5509 IsoInterface.BOX_GPS0, 5510 }; 5511 5512 private static final class RedactionInfo { 5513 public final long[] redactionRanges; 5514 public final long[] freeOffsets; RedactionInfo(long[] redactionRanges, long[] freeOffsets)5515 public RedactionInfo(long[] redactionRanges, long[] freeOffsets) { 5516 this.redactionRanges = redactionRanges; 5517 this.freeOffsets = freeOffsets; 5518 } 5519 } 5520 getRedactionRanges(File file)5521 private RedactionInfo getRedactionRanges(File file) { 5522 Trace.traceBegin(TRACE_TAG_DATABASE, "getRedactionRanges"); 5523 final LongArray res = new LongArray(); 5524 final LongArray freeOffsets = new LongArray(); 5525 try (FileInputStream is = new FileInputStream(file)) { 5526 final ExifInterface exif = new ExifInterface(is.getFD()); 5527 for (String tag : REDACTED_EXIF_TAGS) { 5528 final long[] range = exif.getAttributeRange(tag); 5529 if (range != null) { 5530 res.add(range[0]); 5531 res.add(range[0] + range[1]); 5532 } 5533 } 5534 5535 final IsoInterface iso = IsoInterface.fromFileDescriptor(is.getFD()); 5536 for (int box : REDACTED_ISO_BOXES) { 5537 final long[] ranges = iso.getBoxRanges(box); 5538 for (int i = 0; i < ranges.length; i += 2) { 5539 long boxTypeOffset = ranges[i] - 4; 5540 freeOffsets.add(boxTypeOffset); 5541 res.add(boxTypeOffset); 5542 res.add(ranges[i + 1]); 5543 } 5544 } 5545 5546 // Redact xmp where present 5547 final Set<String> redactedXmpTags = new ArraySet<>(Arrays.asList(REDACTED_EXIF_TAGS)); 5548 final XmpInterface exifXmp = XmpInterface.fromContainer(exif, redactedXmpTags); 5549 res.addAll(exifXmp.getRedactionRanges()); 5550 final XmpInterface isoXmp = XmpInterface.fromContainer(iso, redactedXmpTags); 5551 res.addAll(isoXmp.getRedactionRanges()); 5552 } catch (IOException e) { 5553 Log.w(TAG, "Failed to redact " + file + ": " + e); 5554 } 5555 Trace.traceEnd(TRACE_TAG_DATABASE); 5556 return new RedactionInfo(res.toArray(), freeOffsets.toArray()); 5557 } 5558 checkCallingPermissionGlobal(Uri uri, boolean forWrite)5559 private boolean checkCallingPermissionGlobal(Uri uri, boolean forWrite) { 5560 // System internals can work with all media 5561 if (isCallingPackageSystem()) { 5562 return true; 5563 } 5564 5565 // Check if caller is known to be owner of this item, to speed up 5566 // performance of our permission checks 5567 final int table = matchUri(uri, true); 5568 switch (table) { 5569 case AUDIO_MEDIA_ID: 5570 case VIDEO_MEDIA_ID: 5571 case IMAGES_MEDIA_ID: 5572 case FILES_ID: 5573 case DOWNLOADS_ID: 5574 final long id = ContentUris.parseId(uri); 5575 if (mCallingIdentity.get().isOwned(id)) { 5576 return true; 5577 } 5578 } 5579 5580 // Outstanding grant means they get access 5581 if (getContext().checkUriPermission(uri, mCallingIdentity.get().pid, 5582 mCallingIdentity.get().uid, forWrite 5583 ? Intent.FLAG_GRANT_WRITE_URI_PERMISSION 5584 : Intent.FLAG_GRANT_READ_URI_PERMISSION) == PERMISSION_GRANTED) { 5585 return true; 5586 } 5587 5588 return false; 5589 } 5590 checkCallingPermissionLegacy(Uri uri, boolean forWrite, String callingPackage)5591 private boolean checkCallingPermissionLegacy(Uri uri, boolean forWrite, String callingPackage) { 5592 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY); 5593 } 5594 5595 @Deprecated checkCallingPermissionAudio(boolean forWrite, String callingPackage)5596 private boolean checkCallingPermissionAudio(boolean forWrite, String callingPackage) { 5597 if (forWrite) { 5598 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_AUDIO); 5599 } else { 5600 return mCallingIdentity.get().hasPermission(PERMISSION_READ_AUDIO); 5601 } 5602 } 5603 5604 @Deprecated checkCallingPermissionVideo(boolean forWrite, String callingPackage)5605 private boolean checkCallingPermissionVideo(boolean forWrite, String callingPackage) { 5606 if (forWrite) { 5607 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_VIDEO); 5608 } else { 5609 return mCallingIdentity.get().hasPermission(PERMISSION_READ_VIDEO); 5610 } 5611 } 5612 5613 @Deprecated checkCallingPermissionImages(boolean forWrite, String callingPackage)5614 private boolean checkCallingPermissionImages(boolean forWrite, String callingPackage) { 5615 if (forWrite) { 5616 return mCallingIdentity.get().hasPermission(PERMISSION_WRITE_IMAGES); 5617 } else { 5618 return mCallingIdentity.get().hasPermission(PERMISSION_READ_IMAGES); 5619 } 5620 } 5621 5622 /** 5623 * Enforce that caller has access to the given {@link Uri}. 5624 * 5625 * @throws SecurityException if access isn't allowed. 5626 */ enforceCallingPermission(Uri uri, boolean forWrite)5627 private void enforceCallingPermission(Uri uri, boolean forWrite) { 5628 Trace.traceBegin(TRACE_TAG_DATABASE, "enforceCallingPermission"); 5629 try { 5630 enforceCallingPermissionInternal(uri, forWrite); 5631 } finally { 5632 Trace.traceEnd(TRACE_TAG_DATABASE); 5633 } 5634 } 5635 enforceCallingPermissionInternal(Uri uri, boolean forWrite)5636 private void enforceCallingPermissionInternal(Uri uri, boolean forWrite) { 5637 // Try a simple global check first before falling back to performing a 5638 // simple query to probe for access. 5639 if (checkCallingPermissionGlobal(uri, forWrite)) { 5640 // Access allowed, yay! 5641 return; 5642 } 5643 5644 final DatabaseHelper helper; 5645 final SQLiteDatabase db; 5646 try { 5647 helper = getDatabaseForUri(uri); 5648 db = helper.getReadableDatabase(); 5649 } catch (VolumeNotFoundException e) { 5650 throw e.rethrowAsIllegalArgumentException(); 5651 } 5652 5653 final boolean allowHidden = isCallingPackageAllowedHidden(); 5654 final int table = matchUri(uri, allowHidden); 5655 5656 // First, check to see if caller has direct write access 5657 if (forWrite) { 5658 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_UPDATE, uri, table, null); 5659 try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) { 5660 if (c.moveToFirst()) { 5661 // Direct write access granted, yay! 5662 return; 5663 } 5664 } 5665 } 5666 5667 // We only allow the user to grant access to specific media items in 5668 // strongly typed collections; never to broad collections 5669 boolean allowUserGrant = false; 5670 final int matchUri = matchUri(uri, true); 5671 switch (matchUri) { 5672 case IMAGES_MEDIA_ID: 5673 case AUDIO_MEDIA_ID: 5674 case VIDEO_MEDIA_ID: 5675 allowUserGrant = true; 5676 break; 5677 } 5678 5679 // Second, check to see if caller has direct read access 5680 final SQLiteQueryBuilder qb = getQueryBuilder(TYPE_QUERY, uri, table, null); 5681 try (Cursor c = qb.query(db, new String[0], null, null, null, null, null)) { 5682 if (c.moveToFirst()) { 5683 if (!forWrite) { 5684 // Direct read access granted, yay! 5685 return; 5686 } else if (allowUserGrant) { 5687 // Caller has read access, but they wanted to write, and 5688 // they'll need to get the user to grant that access 5689 final Context context = getContext(); 5690 final PendingIntent intent = PendingIntent.getActivity(context, 42, 5691 new Intent(null, uri, context, PermissionActivity.class), 5692 FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE); 5693 5694 final Icon icon = getCollectionIcon(uri); 5695 final RemoteAction action = new RemoteAction(icon, 5696 context.getText(R.string.permission_required_action), 5697 context.getText(R.string.permission_required_action), 5698 intent); 5699 5700 throw new RecoverableSecurityException(new SecurityException( 5701 getCallingPackageOrSelf() + " has no access to " + uri), 5702 context.getText(R.string.permission_required), action); 5703 } 5704 } 5705 } 5706 5707 throw new SecurityException(getCallingPackageOrSelf() + " has no access to " + uri); 5708 } 5709 getCollectionIcon(Uri uri)5710 private Icon getCollectionIcon(Uri uri) { 5711 final PackageManager pm = getContext().getPackageManager(); 5712 final String type = uri.getPathSegments().get(1); 5713 final String groupName; 5714 switch (type) { 5715 default: groupName = android.Manifest.permission_group.STORAGE; break; 5716 } 5717 try { 5718 final PermissionGroupInfo perm = pm.getPermissionGroupInfo(groupName, 0); 5719 return Icon.createWithResource(perm.packageName, perm.icon); 5720 } catch (NameNotFoundException e) { 5721 throw new RuntimeException(e); 5722 } 5723 } 5724 checkAccess(Uri uri, File file, boolean isWrite)5725 private void checkAccess(Uri uri, File file, boolean isWrite) throws FileNotFoundException { 5726 // First, does caller have the needed row-level access? 5727 enforceCallingPermission(uri, isWrite); 5728 5729 // Second, does the path look sane? 5730 if (!FileUtils.contains(Environment.getStorageDirectory(), file)) { 5731 checkWorldReadAccess(file.getAbsolutePath()); 5732 } 5733 } 5734 5735 /** 5736 * Check whether the path is a world-readable file 5737 */ checkWorldReadAccess(String path)5738 private static void checkWorldReadAccess(String path) throws FileNotFoundException { 5739 // Path has already been canonicalized, and we relax the check to look 5740 // at groups to support runtime storage permissions. 5741 final int accessBits = path.startsWith("/storage/") ? OsConstants.S_IRGRP 5742 : OsConstants.S_IROTH; 5743 try { 5744 StructStat stat = Os.stat(path); 5745 if (OsConstants.S_ISREG(stat.st_mode) && 5746 ((stat.st_mode & accessBits) == accessBits)) { 5747 checkLeadingPathComponentsWorldExecutable(path); 5748 return; 5749 } 5750 } catch (ErrnoException e) { 5751 // couldn't stat the file, either it doesn't exist or isn't 5752 // accessible to us 5753 } 5754 5755 throw new FileNotFoundException("Can't access " + path); 5756 } 5757 checkLeadingPathComponentsWorldExecutable(String filePath)5758 private static void checkLeadingPathComponentsWorldExecutable(String filePath) 5759 throws FileNotFoundException { 5760 File parent = new File(filePath).getParentFile(); 5761 5762 // Path has already been canonicalized, and we relax the check to look 5763 // at groups to support runtime storage permissions. 5764 final int accessBits = filePath.startsWith("/storage/") ? OsConstants.S_IXGRP 5765 : OsConstants.S_IXOTH; 5766 5767 while (parent != null) { 5768 if (! parent.exists()) { 5769 // parent dir doesn't exist, give up 5770 throw new FileNotFoundException("access denied"); 5771 } 5772 try { 5773 StructStat stat = Os.stat(parent.getPath()); 5774 if ((stat.st_mode & accessBits) != accessBits) { 5775 // the parent dir doesn't have the appropriate access 5776 throw new FileNotFoundException("Can't access " + filePath); 5777 } 5778 } catch (ErrnoException e1) { 5779 // couldn't stat() parent 5780 throw new FileNotFoundException("Can't access " + filePath); 5781 } 5782 parent = parent.getParentFile(); 5783 } 5784 } 5785 5786 /** 5787 * Look up the artist or album entry for the given name, creating that entry 5788 * if it does not already exists. 5789 * @param db The database 5790 * @param table The table to store the key/name pair in. 5791 * @param keyField The name of the key-column 5792 * @param nameField The name of the name-column 5793 * @param rawName The name that the calling app was trying to insert into the database 5794 * @param cacheName The string that will be inserted in to the cache 5795 * @param path The full path to the file being inserted in to the audio table 5796 * @param albumHash A hash to distinguish between different albums of the same name 5797 * @param artist The name of the artist, if known 5798 * @param cache The cache to add this entry to 5799 * @param srcuri The Uri that prompted the call to this method, used for determining whether this is 5800 * the internal or external database 5801 * @return The row ID for this artist/album, or -1 if the provided name was invalid 5802 */ getKeyIdForName(DatabaseHelper helper, SQLiteDatabase db, String table, String keyField, String nameField, String rawName, String cacheName, String path, int albumHash, String artist, ArrayMap<String, Long> cache, Uri srcuri)5803 private long getKeyIdForName(DatabaseHelper helper, SQLiteDatabase db, 5804 String table, String keyField, String nameField, 5805 String rawName, String cacheName, String path, int albumHash, 5806 String artist, ArrayMap<String, Long> cache, Uri srcuri) { 5807 long rowId; 5808 5809 if (rawName == null || rawName.length() == 0) { 5810 rawName = MediaStore.UNKNOWN_STRING; 5811 } 5812 String k = MediaStore.Audio.keyFor(rawName); 5813 5814 if (k == null) { 5815 // shouldn't happen, since we only get null keys for null inputs 5816 Log.e(TAG, "null key", new Exception()); 5817 return -1; 5818 } 5819 5820 boolean isAlbum = table.equals("albums"); 5821 boolean isUnknown = MediaStore.UNKNOWN_STRING.equals(rawName); 5822 5823 // To distinguish same-named albums, we append a hash. The hash is based 5824 // on the "album artist" tag if present, otherwise on the "compilation" tag 5825 // if present, otherwise on the path. 5826 // Ideally we would also take things like CDDB ID in to account, so 5827 // we can group files from the same album that aren't in the same 5828 // folder, but this is a quick and easy start that works immediately 5829 // without requiring support from the mp3, mp4 and Ogg meta data 5830 // readers, as long as the albums are in different folders. 5831 if (isAlbum) { 5832 k = k + albumHash; 5833 if (isUnknown) { 5834 k = k + artist; 5835 } 5836 } 5837 5838 String [] selargs = { k }; 5839 Cursor c = db.query(table, null, keyField + "=?", selargs, null, null, null); 5840 5841 try { 5842 switch (c.getCount()) { 5843 case 0: { 5844 // insert new entry into table 5845 ContentValues otherValues = new ContentValues(); 5846 otherValues.put(keyField, k); 5847 otherValues.put(nameField, rawName); 5848 rowId = db.insert(table, "duration", otherValues); 5849 if (rowId > 0) { 5850 String volume = srcuri.toString().substring(16, 24); // extract internal/external 5851 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 5852 getContext().getContentResolver().notifyChange(uri, null); 5853 } 5854 } 5855 break; 5856 case 1: { 5857 // Use the existing entry 5858 c.moveToFirst(); 5859 rowId = c.getLong(0); 5860 5861 // Determine whether the current rawName is better than what's 5862 // currently stored in the table, and update the table if it is. 5863 String currentFancyName = c.getString(2); 5864 String bestName = makeBestName(rawName, currentFancyName); 5865 if (!bestName.equals(currentFancyName)) { 5866 // update the table with the new name 5867 ContentValues newValues = new ContentValues(); 5868 newValues.put(nameField, bestName); 5869 db.update(table, newValues, "rowid="+Integer.toString((int)rowId), null); 5870 String volume = srcuri.toString().substring(16, 24); // extract internal/external 5871 Uri uri = Uri.parse("content://media/" + volume + "/audio/" + table + "/" + rowId); 5872 getContext().getContentResolver().notifyChange(uri, null); 5873 // We have to remove the previous key from the cache otherwise we will 5874 // not be able to change between upper and lower case letters. 5875 if (isAlbum) { 5876 cache.remove(currentFancyName + albumHash); 5877 } else { 5878 cache.remove(currentFancyName); 5879 } 5880 } 5881 } 5882 break; 5883 default: 5884 // corrupt database 5885 Log.e(TAG, "Multiple entries in table " + table + " for key " + k); 5886 rowId = -1; 5887 break; 5888 } 5889 } finally { 5890 IoUtils.closeQuietly(c); 5891 } 5892 5893 if (cache != null && ! isUnknown) { 5894 cache.put(cacheName, rowId); 5895 } 5896 return rowId; 5897 } 5898 5899 /** 5900 * Returns the best string to use for display, given two names. 5901 * Note that this function does not necessarily return either one 5902 * of the provided names; it may decide to return a better alternative 5903 * (for example, specifying the inputs "Police" and "Police, The" will 5904 * return "The Police") 5905 * 5906 * The basic assumptions are: 5907 * - longer is better ("The police" is better than "Police") 5908 * - prefix is better ("The Police" is better than "Police, The") 5909 * - accents are better ("Motörhead" is better than "Motorhead") 5910 * 5911 * @param one The first of the two names to consider 5912 * @param two The last of the two names to consider 5913 * @return The actual name to use 5914 */ makeBestName(String one, String two)5915 String makeBestName(String one, String two) { 5916 String name; 5917 5918 // Longer names are usually better. 5919 if (one.length() > two.length()) { 5920 name = one; 5921 } else { 5922 // Names with accents are usually better, and conveniently sort later 5923 if (one.toLowerCase().compareTo(two.toLowerCase()) >= 0) { 5924 name = one; 5925 } else { 5926 name = two; 5927 } 5928 } 5929 5930 // Prefixes are better than postfixes. 5931 if (name.endsWith(", the") || name.endsWith(",the") || 5932 name.endsWith(", an") || name.endsWith(",an") || 5933 name.endsWith(", a") || name.endsWith(",a")) { 5934 String fix = name.substring(1 + name.lastIndexOf(',')); 5935 name = fix.trim() + " " + name.substring(0, name.lastIndexOf(',')); 5936 } 5937 5938 // TODO: word-capitalize the resulting name 5939 return name; 5940 } 5941 5942 private static class FallbackException extends Exception { FallbackException(String message)5943 public FallbackException(String message) { 5944 super(message); 5945 } 5946 rethrowAsIllegalArgumentException()5947 public IllegalArgumentException rethrowAsIllegalArgumentException() { 5948 throw new IllegalArgumentException(getMessage()); 5949 } 5950 translateForQuery(int targetSdkVersion)5951 public Cursor translateForQuery(int targetSdkVersion) { 5952 if (targetSdkVersion >= Build.VERSION_CODES.Q) { 5953 throw new IllegalArgumentException(getMessage()); 5954 } else { 5955 Log.w(TAG, getMessage()); 5956 return null; 5957 } 5958 } 5959 translateForInsert(int targetSdkVersion)5960 public Uri translateForInsert(int targetSdkVersion) { 5961 if (targetSdkVersion >= Build.VERSION_CODES.Q) { 5962 throw new IllegalArgumentException(getMessage()); 5963 } else { 5964 Log.w(TAG, getMessage()); 5965 return null; 5966 } 5967 } 5968 translateForUpdateDelete(int targetSdkVersion)5969 public int translateForUpdateDelete(int targetSdkVersion) { 5970 if (targetSdkVersion >= Build.VERSION_CODES.Q) { 5971 throw new IllegalArgumentException(getMessage()); 5972 } else { 5973 Log.w(TAG, getMessage()); 5974 return 0; 5975 } 5976 } 5977 } 5978 5979 static class VolumeNotFoundException extends FallbackException { VolumeNotFoundException(String volumeName)5980 public VolumeNotFoundException(String volumeName) { 5981 super("Volume " + volumeName + " not found"); 5982 } 5983 } 5984 5985 static class VolumeArgumentException extends FallbackException { VolumeArgumentException(File actual, Collection<File> allowed)5986 public VolumeArgumentException(File actual, Collection<File> allowed) { 5987 super("Requested path " + actual + " doesn't appear under " + allowed); 5988 } 5989 } 5990 getDatabaseForUri(Uri uri)5991 private @NonNull DatabaseHelper getDatabaseForUri(Uri uri) throws VolumeNotFoundException { 5992 final String volumeName = resolveVolumeName(uri); 5993 synchronized (mAttachedVolumeNames) { 5994 if (!mAttachedVolumeNames.contains(volumeName)) { 5995 throw new VolumeNotFoundException(volumeName); 5996 } 5997 } 5998 if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) { 5999 return mInternalDatabase; 6000 } else { 6001 return mExternalDatabase; 6002 } 6003 } 6004 isMediaDatabaseName(String name)6005 static boolean isMediaDatabaseName(String name) { 6006 if (INTERNAL_DATABASE_NAME.equals(name)) { 6007 return true; 6008 } 6009 if (EXTERNAL_DATABASE_NAME.equals(name)) { 6010 return true; 6011 } 6012 if (name.startsWith("external-") && name.endsWith(".db")) { 6013 return true; 6014 } 6015 return false; 6016 } 6017 isInternalMediaDatabaseName(String name)6018 static boolean isInternalMediaDatabaseName(String name) { 6019 if (INTERNAL_DATABASE_NAME.equals(name)) { 6020 return true; 6021 } 6022 return false; 6023 } 6024 attachVolume(Uri uri)6025 private void attachVolume(Uri uri) { 6026 attachVolume(MediaStore.getVolumeName(uri)); 6027 } 6028 attachVolume(String volume)6029 public Uri attachVolume(String volume) { 6030 if (mCallingIdentity.get().pid != android.os.Process.myPid()) { 6031 throw new SecurityException( 6032 "Opening and closing databases not allowed."); 6033 } 6034 6035 // Quick sanity check for shady volume names 6036 MediaStore.checkArgumentVolumeName(volume); 6037 6038 // Quick sanity check that volume actually exists 6039 if (!MediaStore.VOLUME_INTERNAL.equals(volume)) { 6040 try { 6041 getVolumePath(volume); 6042 } catch (IOException e) { 6043 throw new IllegalArgumentException( 6044 "Volume " + volume + " currently unavailable", e); 6045 } 6046 } 6047 6048 synchronized (mAttachedVolumeNames) { 6049 mAttachedVolumeNames.add(volume); 6050 } 6051 6052 final Uri uri = MediaStore.AUTHORITY_URI.buildUpon().appendPath(volume).build(); 6053 getContext().getContentResolver().notifyChange(uri, null); 6054 if (LOCAL_LOGV) Log.v(TAG, "Attached volume: " + volume); 6055 if (!MediaStore.VOLUME_INTERNAL.equals(volume)) { 6056 final DatabaseHelper helper = mInternalDatabase; 6057 ensureDefaultFolders(volume, helper, helper.getWritableDatabase()); 6058 } 6059 return uri; 6060 } 6061 detachVolume(Uri uri)6062 private void detachVolume(Uri uri) { 6063 detachVolume(MediaStore.getVolumeName(uri)); 6064 } 6065 detachVolume(String volume)6066 public void detachVolume(String volume) { 6067 if (mCallingIdentity.get().pid != android.os.Process.myPid()) { 6068 throw new SecurityException( 6069 "Opening and closing databases not allowed."); 6070 } 6071 6072 // Quick sanity check for shady volume names 6073 MediaStore.checkArgumentVolumeName(volume); 6074 6075 if (MediaStore.VOLUME_INTERNAL.equals(volume)) { 6076 throw new UnsupportedOperationException( 6077 "Deleting the internal volume is not allowed"); 6078 } 6079 6080 // Signal any scanning to shut down 6081 MediaScanner.instance(getContext()).onDetachVolume(volume); 6082 6083 synchronized (mAttachedVolumeNames) { 6084 mAttachedVolumeNames.remove(volume); 6085 } 6086 6087 final Uri uri = MediaStore.AUTHORITY_URI.buildUpon().appendPath(volume).build(); 6088 getContext().getContentResolver().notifyChange(uri, null); 6089 if (LOCAL_LOGV) Log.v(TAG, "Detached volume: " + volume); 6090 } 6091 6092 /* 6093 * Useful commands to enable debugging: 6094 * $ adb shell setprop log.tag.MediaProvider VERBOSE 6095 * $ adb shell setprop db.log.slow_query_threshold.`adb shell cat \ 6096 * /data/system/packages.list |grep "com.android.providers.media " |cut -b 29-33` 0 6097 * $ adb shell setprop db.log.bindargs 1 6098 */ 6099 6100 static final String TAG = "MediaProvider"; 6101 static final boolean LOCAL_LOGV = Log.isLoggable(TAG, Log.VERBOSE); 6102 6103 private static final String INTERNAL_DATABASE_NAME = "internal.db"; 6104 private static final String EXTERNAL_DATABASE_NAME = "external.db"; 6105 6106 // maximum number of cached external databases to keep 6107 private static final int MAX_EXTERNAL_DATABASES = 3; 6108 6109 // Delete databases that have not been used in two months 6110 // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60) 6111 private static final long OBSOLETE_DATABASE_DB = 5184000000L; 6112 6113 // Memory optimization - close idle connections after 30s of inactivity 6114 private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000; 6115 6116 @GuardedBy("mAttachedVolumeNames") 6117 private final ArraySet<String> mAttachedVolumeNames = new ArraySet<>(); 6118 6119 private DatabaseHelper mInternalDatabase; 6120 private DatabaseHelper mExternalDatabase; 6121 6122 // name of the volume currently being scanned by the media scanner (or null) 6123 private String mMediaScannerVolume; 6124 6125 // current FAT volume ID 6126 private int mVolumeId = -1; 6127 6128 // WARNING: the values of IMAGES_MEDIA, AUDIO_MEDIA, and VIDEO_MEDIA and AUDIO_PLAYLISTS 6129 // are stored in the "files" table, so do not renumber them unless you also add 6130 // a corresponding database upgrade step for it. 6131 private static final int IMAGES_MEDIA = 1; 6132 private static final int IMAGES_MEDIA_ID = 2; 6133 private static final int IMAGES_MEDIA_ID_THUMBNAIL = 3; 6134 private static final int IMAGES_THUMBNAILS = 4; 6135 private static final int IMAGES_THUMBNAILS_ID = 5; 6136 6137 private static final int AUDIO_MEDIA = 100; 6138 private static final int AUDIO_MEDIA_ID = 101; 6139 private static final int AUDIO_MEDIA_ID_GENRES = 102; 6140 private static final int AUDIO_MEDIA_ID_GENRES_ID = 103; 6141 private static final int AUDIO_MEDIA_ID_PLAYLISTS = 104; 6142 private static final int AUDIO_MEDIA_ID_PLAYLISTS_ID = 105; 6143 private static final int AUDIO_GENRES = 106; 6144 private static final int AUDIO_GENRES_ID = 107; 6145 private static final int AUDIO_GENRES_ID_MEMBERS = 108; 6146 private static final int AUDIO_GENRES_ALL_MEMBERS = 109; 6147 private static final int AUDIO_PLAYLISTS = 110; 6148 private static final int AUDIO_PLAYLISTS_ID = 111; 6149 private static final int AUDIO_PLAYLISTS_ID_MEMBERS = 112; 6150 private static final int AUDIO_PLAYLISTS_ID_MEMBERS_ID = 113; 6151 private static final int AUDIO_ARTISTS = 114; 6152 private static final int AUDIO_ARTISTS_ID = 115; 6153 private static final int AUDIO_ALBUMS = 116; 6154 private static final int AUDIO_ALBUMS_ID = 117; 6155 private static final int AUDIO_ARTISTS_ID_ALBUMS = 118; 6156 private static final int AUDIO_ALBUMART = 119; 6157 private static final int AUDIO_ALBUMART_ID = 120; 6158 private static final int AUDIO_ALBUMART_FILE_ID = 121; 6159 6160 private static final int VIDEO_MEDIA = 200; 6161 private static final int VIDEO_MEDIA_ID = 201; 6162 private static final int VIDEO_MEDIA_ID_THUMBNAIL = 202; 6163 private static final int VIDEO_THUMBNAILS = 203; 6164 private static final int VIDEO_THUMBNAILS_ID = 204; 6165 6166 private static final int VOLUMES = 300; 6167 private static final int VOLUMES_ID = 301; 6168 6169 private static final int MEDIA_SCANNER = 500; 6170 6171 private static final int FS_ID = 600; 6172 private static final int VERSION = 601; 6173 6174 private static final int FILES = 700; 6175 private static final int FILES_ID = 701; 6176 6177 // Used only by the MTP implementation 6178 private static final int MTP_OBJECTS = 702; 6179 private static final int MTP_OBJECTS_ID = 703; 6180 private static final int MTP_OBJECT_REFERENCES = 704; 6181 6182 // Used only to invoke special logic for directories 6183 private static final int FILES_DIRECTORY = 706; 6184 6185 private static final int DOWNLOADS = 800; 6186 private static final int DOWNLOADS_ID = 801; 6187 6188 private static final UriMatcher HIDDEN_URI_MATCHER = 6189 new UriMatcher(UriMatcher.NO_MATCH); 6190 6191 private static final UriMatcher PUBLIC_URI_MATCHER = 6192 new UriMatcher(UriMatcher.NO_MATCH); 6193 6194 private static final String[] PATH_PROJECTION = new String[] { 6195 MediaStore.MediaColumns._ID, 6196 MediaStore.MediaColumns.DATA, 6197 }; 6198 6199 private static final String OBJECT_REFERENCES_QUERY = 6200 "SELECT " + Audio.Playlists.Members.AUDIO_ID + " FROM audio_playlists_map" 6201 + " WHERE " + Audio.Playlists.Members.PLAYLIST_ID + "=?" 6202 + " ORDER BY " + Audio.Playlists.Members.PLAY_ORDER; 6203 matchUri(Uri uri, boolean allowHidden)6204 private static int matchUri(Uri uri, boolean allowHidden) { 6205 final int publicMatch = PUBLIC_URI_MATCHER.match(uri); 6206 if (publicMatch != UriMatcher.NO_MATCH) { 6207 return publicMatch; 6208 } 6209 6210 final int hiddenMatch = HIDDEN_URI_MATCHER.match(uri); 6211 if (hiddenMatch != UriMatcher.NO_MATCH) { 6212 // Detect callers asking about hidden behavior by looking closer when 6213 // the matchers diverge; we only care about apps that are explicitly 6214 // targeting a specific public API level. 6215 if (!allowHidden) { 6216 throw new IllegalStateException("Unknown URL: " + uri + " is hidden API"); 6217 } 6218 return hiddenMatch; 6219 } 6220 6221 return UriMatcher.NO_MATCH; 6222 } 6223 6224 static { 6225 final UriMatcher publicMatcher = PUBLIC_URI_MATCHER; 6226 final UriMatcher hiddenMatcher = HIDDEN_URI_MATCHER; 6227 publicMatcher.addURI(AUTHORITY, "*/images/media", IMAGES_MEDIA)6228 publicMatcher.addURI(AUTHORITY, "*/images/media", IMAGES_MEDIA); publicMatcher.addURI(AUTHORITY, "*/images/media/#", IMAGES_MEDIA_ID)6229 publicMatcher.addURI(AUTHORITY, "*/images/media/#", IMAGES_MEDIA_ID); publicMatcher.addURI(AUTHORITY, "*/images/media/#/thumbnail", IMAGES_MEDIA_ID_THUMBNAIL)6230 publicMatcher.addURI(AUTHORITY, "*/images/media/#/thumbnail", IMAGES_MEDIA_ID_THUMBNAIL); publicMatcher.addURI(AUTHORITY, "*/images/thumbnails", IMAGES_THUMBNAILS)6231 publicMatcher.addURI(AUTHORITY, "*/images/thumbnails", IMAGES_THUMBNAILS); publicMatcher.addURI(AUTHORITY, "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID)6232 publicMatcher.addURI(AUTHORITY, "*/images/thumbnails/#", IMAGES_THUMBNAILS_ID); 6233 publicMatcher.addURI(AUTHORITY, "*/audio/media", AUDIO_MEDIA)6234 publicMatcher.addURI(AUTHORITY, "*/audio/media", AUDIO_MEDIA); publicMatcher.addURI(AUTHORITY, "*/audio/media/#", AUDIO_MEDIA_ID)6235 publicMatcher.addURI(AUTHORITY, "*/audio/media/#", AUDIO_MEDIA_ID); publicMatcher.addURI(AUTHORITY, "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES)6236 publicMatcher.addURI(AUTHORITY, "*/audio/media/#/genres", AUDIO_MEDIA_ID_GENRES); publicMatcher.addURI(AUTHORITY, "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID)6237 publicMatcher.addURI(AUTHORITY, "*/audio/media/#/genres/#", AUDIO_MEDIA_ID_GENRES_ID); hiddenMatcher.addURI(AUTHORITY, "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS)6238 hiddenMatcher.addURI(AUTHORITY, "*/audio/media/#/playlists", AUDIO_MEDIA_ID_PLAYLISTS); hiddenMatcher.addURI(AUTHORITY, "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID)6239 hiddenMatcher.addURI(AUTHORITY, "*/audio/media/#/playlists/#", AUDIO_MEDIA_ID_PLAYLISTS_ID); publicMatcher.addURI(AUTHORITY, "*/audio/genres", AUDIO_GENRES)6240 publicMatcher.addURI(AUTHORITY, "*/audio/genres", AUDIO_GENRES); publicMatcher.addURI(AUTHORITY, "*/audio/genres/#", AUDIO_GENRES_ID)6241 publicMatcher.addURI(AUTHORITY, "*/audio/genres/#", AUDIO_GENRES_ID); publicMatcher.addURI(AUTHORITY, "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS)6242 publicMatcher.addURI(AUTHORITY, "*/audio/genres/#/members", AUDIO_GENRES_ID_MEMBERS); 6243 // TODO: not actually defined in API, but CTS tested publicMatcher.addURI(AUTHORITY, "*/audio/genres/all/members", AUDIO_GENRES_ALL_MEMBERS)6244 publicMatcher.addURI(AUTHORITY, "*/audio/genres/all/members", AUDIO_GENRES_ALL_MEMBERS); publicMatcher.addURI(AUTHORITY, "*/audio/playlists", AUDIO_PLAYLISTS)6245 publicMatcher.addURI(AUTHORITY, "*/audio/playlists", AUDIO_PLAYLISTS); publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#", AUDIO_PLAYLISTS_ID)6246 publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#", AUDIO_PLAYLISTS_ID); publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS)6247 publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#/members", AUDIO_PLAYLISTS_ID_MEMBERS); publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID)6248 publicMatcher.addURI(AUTHORITY, "*/audio/playlists/#/members/#", AUDIO_PLAYLISTS_ID_MEMBERS_ID); publicMatcher.addURI(AUTHORITY, "*/audio/artists", AUDIO_ARTISTS)6249 publicMatcher.addURI(AUTHORITY, "*/audio/artists", AUDIO_ARTISTS); publicMatcher.addURI(AUTHORITY, "*/audio/artists/#", AUDIO_ARTISTS_ID)6250 publicMatcher.addURI(AUTHORITY, "*/audio/artists/#", AUDIO_ARTISTS_ID); publicMatcher.addURI(AUTHORITY, "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS)6251 publicMatcher.addURI(AUTHORITY, "*/audio/artists/#/albums", AUDIO_ARTISTS_ID_ALBUMS); publicMatcher.addURI(AUTHORITY, "*/audio/albums", AUDIO_ALBUMS)6252 publicMatcher.addURI(AUTHORITY, "*/audio/albums", AUDIO_ALBUMS); publicMatcher.addURI(AUTHORITY, "*/audio/albums/#", AUDIO_ALBUMS_ID)6253 publicMatcher.addURI(AUTHORITY, "*/audio/albums/#", AUDIO_ALBUMS_ID); 6254 // TODO: not actually defined in API, but CTS tested publicMatcher.addURI(AUTHORITY, "*/audio/albumart", AUDIO_ALBUMART)6255 publicMatcher.addURI(AUTHORITY, "*/audio/albumart", AUDIO_ALBUMART); 6256 // TODO: not actually defined in API, but CTS tested publicMatcher.addURI(AUTHORITY, "*/audio/albumart/#", AUDIO_ALBUMART_ID)6257 publicMatcher.addURI(AUTHORITY, "*/audio/albumart/#", AUDIO_ALBUMART_ID); 6258 // TODO: not actually defined in API, but CTS tested publicMatcher.addURI(AUTHORITY, "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID)6259 publicMatcher.addURI(AUTHORITY, "*/audio/media/#/albumart", AUDIO_ALBUMART_FILE_ID); 6260 publicMatcher.addURI(AUTHORITY, "*/video/media", VIDEO_MEDIA)6261 publicMatcher.addURI(AUTHORITY, "*/video/media", VIDEO_MEDIA); publicMatcher.addURI(AUTHORITY, "*/video/media/#", VIDEO_MEDIA_ID)6262 publicMatcher.addURI(AUTHORITY, "*/video/media/#", VIDEO_MEDIA_ID); publicMatcher.addURI(AUTHORITY, "*/video/media/#/thumbnail", VIDEO_MEDIA_ID_THUMBNAIL)6263 publicMatcher.addURI(AUTHORITY, "*/video/media/#/thumbnail", VIDEO_MEDIA_ID_THUMBNAIL); publicMatcher.addURI(AUTHORITY, "*/video/thumbnails", VIDEO_THUMBNAILS)6264 publicMatcher.addURI(AUTHORITY, "*/video/thumbnails", VIDEO_THUMBNAILS); publicMatcher.addURI(AUTHORITY, "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID)6265 publicMatcher.addURI(AUTHORITY, "*/video/thumbnails/#", VIDEO_THUMBNAILS_ID); 6266 publicMatcher.addURI(AUTHORITY, "*/media_scanner", MEDIA_SCANNER)6267 publicMatcher.addURI(AUTHORITY, "*/media_scanner", MEDIA_SCANNER); 6268 6269 // NOTE: technically hidden, since Uri is never exposed publicMatcher.addURI(AUTHORITY, "*/fs_id", FS_ID)6270 publicMatcher.addURI(AUTHORITY, "*/fs_id", FS_ID); 6271 // NOTE: technically hidden, since Uri is never exposed publicMatcher.addURI(AUTHORITY, "*/version", VERSION)6272 publicMatcher.addURI(AUTHORITY, "*/version", VERSION); 6273 hiddenMatcher.addURI(AUTHORITY, "*", VOLUMES_ID)6274 hiddenMatcher.addURI(AUTHORITY, "*", VOLUMES_ID); hiddenMatcher.addURI(AUTHORITY, null, VOLUMES)6275 hiddenMatcher.addURI(AUTHORITY, null, VOLUMES); 6276 6277 // Used by MTP implementation publicMatcher.addURI(AUTHORITY, "*/file", FILES)6278 publicMatcher.addURI(AUTHORITY, "*/file", FILES); publicMatcher.addURI(AUTHORITY, "*/file/#", FILES_ID)6279 publicMatcher.addURI(AUTHORITY, "*/file/#", FILES_ID); hiddenMatcher.addURI(AUTHORITY, "*/object", MTP_OBJECTS)6280 hiddenMatcher.addURI(AUTHORITY, "*/object", MTP_OBJECTS); hiddenMatcher.addURI(AUTHORITY, "*/object/#", MTP_OBJECTS_ID)6281 hiddenMatcher.addURI(AUTHORITY, "*/object/#", MTP_OBJECTS_ID); hiddenMatcher.addURI(AUTHORITY, "*/object/#/references", MTP_OBJECT_REFERENCES)6282 hiddenMatcher.addURI(AUTHORITY, "*/object/#/references", MTP_OBJECT_REFERENCES); 6283 6284 // Used only to trigger special logic for directories hiddenMatcher.addURI(AUTHORITY, "*/dir", FILES_DIRECTORY)6285 hiddenMatcher.addURI(AUTHORITY, "*/dir", FILES_DIRECTORY); 6286 publicMatcher.addURI(AUTHORITY, "*/downloads", DOWNLOADS)6287 publicMatcher.addURI(AUTHORITY, "*/downloads", DOWNLOADS); publicMatcher.addURI(AUTHORITY, "*/downloads/#", DOWNLOADS_ID)6288 publicMatcher.addURI(AUTHORITY, "*/downloads/#", DOWNLOADS_ID); 6289 } 6290 6291 /** 6292 * Set of columns that can be safely mutated by external callers; all other 6293 * columns are treated as read-only, since they reflect what the media 6294 * scanner found on disk, and any mutations would be overwritten the next 6295 * time the media was scanned. 6296 */ 6297 private static final ArraySet<String> sMutableColumns = new ArraySet<>(); 6298 6299 { 6300 sMutableColumns.add(MediaStore.MediaColumns.DATA); 6301 sMutableColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); 6302 sMutableColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); 6303 sMutableColumns.add(MediaStore.MediaColumns.IS_PENDING); 6304 sMutableColumns.add(MediaStore.MediaColumns.IS_TRASHED); 6305 sMutableColumns.add(MediaStore.MediaColumns.DATE_EXPIRES); 6306 sMutableColumns.add(MediaStore.MediaColumns.PRIMARY_DIRECTORY); 6307 sMutableColumns.add(MediaStore.MediaColumns.SECONDARY_DIRECTORY); 6308 6309 sMutableColumns.add(MediaStore.Audio.AudioColumns.BOOKMARK); 6310 6311 sMutableColumns.add(MediaStore.Video.VideoColumns.TAGS); 6312 sMutableColumns.add(MediaStore.Video.VideoColumns.CATEGORY); 6313 sMutableColumns.add(MediaStore.Video.VideoColumns.BOOKMARK); 6314 6315 sMutableColumns.add(MediaStore.Audio.Playlists.NAME); 6316 sMutableColumns.add(MediaStore.Audio.Playlists.Members.AUDIO_ID); 6317 sMutableColumns.add(MediaStore.Audio.Playlists.Members.PLAY_ORDER); 6318 6319 sMutableColumns.add(MediaStore.Files.FileColumns.MIME_TYPE); 6320 sMutableColumns.add(MediaStore.Files.FileColumns.MEDIA_TYPE); 6321 } 6322 6323 /** 6324 * Set of columns that affect placement of files on disk. 6325 */ 6326 private static final ArraySet<String> sPlacementColumns = new ArraySet<>(); 6327 6328 { 6329 sPlacementColumns.add(MediaStore.MediaColumns.DATA); 6330 sPlacementColumns.add(MediaStore.MediaColumns.RELATIVE_PATH); 6331 sPlacementColumns.add(MediaStore.MediaColumns.DISPLAY_NAME); 6332 sPlacementColumns.add(MediaStore.MediaColumns.MIME_TYPE); 6333 sPlacementColumns.add(MediaStore.MediaColumns.PRIMARY_DIRECTORY); 6334 sPlacementColumns.add(MediaStore.MediaColumns.SECONDARY_DIRECTORY); 6335 } 6336 6337 /** 6338 * List of abusive custom columns that we're willing to allow via 6339 * {@link SQLiteQueryBuilder#setProjectionGreylist(List)}. 6340 */ 6341 static final ArrayList<Pattern> sGreylist = new ArrayList<>(); 6342 addGreylistPattern(String pattern)6343 private static void addGreylistPattern(String pattern) { 6344 sGreylist.add(Pattern.compile(" *" + pattern + " *")); 6345 } 6346 6347 static { 6348 final String maybeAs = "( (as )?[_a-z0-9]+)?"; 6349 addGreylistPattern("(?i)[_a-z0-9]+" + maybeAs); 6350 addGreylistPattern("audio\\._id AS _id"); 6351 addGreylistPattern("(?i)(min|max|sum|avg|total|count|cast)\\(([_a-z0-9]+" + maybeAs + "|\\*)\\)" + maybeAs); 6352 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"); 6353 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\\)"); 6354 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\\)"); 6355 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\\)"); 6356 addGreylistPattern("\"content://media/[a-z]+/audio/media\""); 6357 addGreylistPattern("substr\\(_data, length\\(_data\\)-length\\(_display_name\\), 1\\) as filename_prevchar"); 6358 addGreylistPattern("\\*" + maybeAs); 6359 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"); 6360 } 6361 6362 @GuardedBy("sProjectionMapCache") 6363 private static final ArrayMap<Class<?>, ArrayMap<String, String>> 6364 sProjectionMapCache = new ArrayMap<>(); 6365 6366 /** 6367 * Return a projection map that represents the valid columns that can be 6368 * queried the given contract class. The mapping is built automatically 6369 * using the {@link Column} annotation, and is designed to ensure that we 6370 * always support public API commitments. 6371 */ getProjectionMap(Class<?> clazz)6372 static ArrayMap<String, String> getProjectionMap(Class<?> clazz) { 6373 synchronized (sProjectionMapCache) { 6374 ArrayMap<String, String> map = sProjectionMapCache.get(clazz); 6375 if (map == null) { 6376 map = new ArrayMap<>(); 6377 sProjectionMapCache.put(clazz, map); 6378 try { 6379 for (Field field : clazz.getFields()) { 6380 if (field.isAnnotationPresent(Column.class)) { 6381 final String column = (String) field.get(null); 6382 map.put(column, column); 6383 } 6384 } 6385 } catch (ReflectiveOperationException e) { 6386 throw new RuntimeException(e); 6387 } 6388 } 6389 return map; 6390 } 6391 } 6392 6393 /** 6394 * Simple attempt to balance the given SQL expression by adding parenthesis 6395 * when needed. 6396 * <p> 6397 * Since this is only used for recovering from abusive apps, we're not 6398 * interested in trying to build a fully valid SQL parser up in Java. It'll 6399 * give up when it encounters complex SQL, such as string literals. 6400 */ 6401 @VisibleForTesting maybeBalance(@ullable String sql)6402 static @Nullable String maybeBalance(@Nullable String sql) { 6403 if (sql == null) return null; 6404 6405 int count = 0; 6406 char literal = '\0'; 6407 for (int i = 0; i < sql.length(); i++) { 6408 final char c = sql.charAt(i); 6409 6410 if (c == '\'' || c == '"') { 6411 if (literal == '\0') { 6412 // Start literal 6413 literal = c; 6414 } else if (literal == c) { 6415 // End literal 6416 literal = '\0'; 6417 } 6418 } 6419 6420 if (literal == '\0') { 6421 if (c == '(') { 6422 count++; 6423 } else if (c == ')') { 6424 count--; 6425 } 6426 } 6427 } 6428 while (count > 0) { 6429 sql = sql + ")"; 6430 count--; 6431 } 6432 while (count < 0) { 6433 sql = "(" + sql; 6434 count++; 6435 } 6436 return sql; 6437 } 6438 containsAny(Set<T> a, Set<T> b)6439 static <T> boolean containsAny(Set<T> a, Set<T> b) { 6440 for (T i : b) { 6441 if (a.contains(i)) { 6442 return true; 6443 } 6444 } 6445 return false; 6446 } 6447 6448 /** 6449 * Gracefully recover from abusive callers that are smashing invalid 6450 * {@code GROUP BY} clauses into {@code WHERE} clauses. 6451 */ 6452 @VisibleForTesting recoverAbusiveGroupBy(Pair<String, String> selectionAndGroupBy)6453 static Pair<String, String> recoverAbusiveGroupBy(Pair<String, String> selectionAndGroupBy) { 6454 final String origSelection = selectionAndGroupBy.first; 6455 final String origGroupBy = selectionAndGroupBy.second; 6456 6457 final int index = (origSelection != null) 6458 ? origSelection.toUpperCase().indexOf(" GROUP BY ") : -1; 6459 if (index != -1) { 6460 String selection = origSelection.substring(0, index); 6461 String groupBy = origSelection.substring(index + " GROUP BY ".length()); 6462 6463 // Try balancing things out 6464 selection = maybeBalance(selection); 6465 groupBy = maybeBalance(groupBy); 6466 6467 // Yell if we already had a group by requested 6468 if (!TextUtils.isEmpty(origGroupBy)) { 6469 throw new IllegalArgumentException( 6470 "Abusive '" + groupBy + "' conflicts with requested '" + origGroupBy + "'"); 6471 } 6472 6473 Log.w(TAG, "Recovered abusive '" + selection + "' and '" + groupBy + "' from '" 6474 + origSelection + "'"); 6475 return Pair.create(selection, groupBy); 6476 } else { 6477 return selectionAndGroupBy; 6478 } 6479 } 6480 6481 @VisibleForTesting computeCommonPrefix(@onNull List<Uri> uris)6482 static @Nullable Uri computeCommonPrefix(@NonNull List<Uri> uris) { 6483 if (uris.isEmpty()) return null; 6484 6485 final Uri base = uris.get(0); 6486 final List<String> basePath = new ArrayList<>(base.getPathSegments()); 6487 for (int i = 1; i < uris.size(); i++) { 6488 final List<String> probePath = uris.get(i).getPathSegments(); 6489 for (int j = 0; j < basePath.size() && j < probePath.size(); j++) { 6490 if (!Objects.equals(basePath.get(j), probePath.get(j))) { 6491 // Trim away all remaining common elements 6492 while (basePath.size() > j) { 6493 basePath.remove(j); 6494 } 6495 } 6496 } 6497 6498 final int probeSize = probePath.size(); 6499 while (basePath.size() > probeSize) { 6500 basePath.remove(probeSize); 6501 } 6502 } 6503 6504 final Uri.Builder builder = base.buildUpon().path(null); 6505 for (int i = 0; i < basePath.size(); i++) { 6506 builder.appendPath(basePath.get(i)); 6507 } 6508 return builder.build(); 6509 } 6510 6511 @Deprecated getCallingPackageOrSelf()6512 private String getCallingPackageOrSelf() { 6513 return mCallingIdentity.get().getPackageName(); 6514 } 6515 6516 @Deprecated getCallingPackageTargetSdkVersion()6517 private int getCallingPackageTargetSdkVersion() { 6518 return mCallingIdentity.get().getTargetSdkVersion(); 6519 } 6520 6521 @Deprecated isCallingPackageAllowedHidden()6522 private boolean isCallingPackageAllowedHidden() { 6523 return isCallingPackageSystem(); 6524 } 6525 6526 @Deprecated isCallingPackageSystem()6527 private boolean isCallingPackageSystem() { 6528 return mCallingIdentity.get().hasPermission(PERMISSION_IS_SYSTEM); 6529 } 6530 6531 @Deprecated isCallingPackageLegacy()6532 private boolean isCallingPackageLegacy() { 6533 return mCallingIdentity.get().hasPermission(PERMISSION_IS_LEGACY); 6534 } 6535 6536 @Override dump(FileDescriptor fd, PrintWriter writer, String[] args)6537 public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { 6538 final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); 6539 pw.printPair("mThumbSize", mThumbSize); 6540 pw.println(); 6541 pw.printPair("mAttachedVolumeNames", mAttachedVolumeNames); 6542 pw.println(); 6543 6544 pw.println(dump(mInternalDatabase, true)); 6545 pw.println(dump(mExternalDatabase, true)); 6546 } 6547 dump(DatabaseHelper dbh, boolean dumpDbLog)6548 private String dump(DatabaseHelper dbh, boolean dumpDbLog) { 6549 StringBuilder s = new StringBuilder(); 6550 s.append(dbh.mName); 6551 s.append(": "); 6552 SQLiteDatabase db = dbh.getReadableDatabase(); 6553 if (db == null) { 6554 s.append("null"); 6555 } else { 6556 s.append("version " + db.getVersion() + ", "); 6557 Cursor c = db.query("files", new String[] {"count(*)"}, null, null, null, null, null); 6558 try { 6559 if (c != null && c.moveToFirst()) { 6560 int num = c.getInt(0); 6561 s.append(num + " rows, "); 6562 } else { 6563 s.append("couldn't get row count, "); 6564 } 6565 } finally { 6566 IoUtils.closeQuietly(c); 6567 } 6568 if (dbh.mScanStartTime != 0) { 6569 s.append("scan started " + DateUtils.formatDateTime(getContext(), 6570 dbh.mScanStartTime / 1000, 6571 DateUtils.FORMAT_SHOW_DATE 6572 | DateUtils.FORMAT_SHOW_TIME 6573 | DateUtils.FORMAT_ABBREV_ALL)); 6574 long now = dbh.mScanStopTime; 6575 if (now < dbh.mScanStartTime) { 6576 now = SystemClock.currentTimeMicro(); 6577 } 6578 s.append(" (" + DateUtils.formatElapsedTime( 6579 (now - dbh.mScanStartTime) / 1000000) + ")"); 6580 if (dbh.mScanStopTime < dbh.mScanStartTime) { 6581 if (mMediaScannerVolume != null && 6582 dbh.mName.startsWith(mMediaScannerVolume)) { 6583 s.append(" (ongoing)"); 6584 } else { 6585 s.append(" (scanning " + mMediaScannerVolume + ")"); 6586 } 6587 } 6588 } 6589 if (dumpDbLog) { 6590 c = db.query("log", new String[] {"time", "message"}, 6591 null, null, null, null, "rowid"); 6592 try { 6593 if (c != null) { 6594 while (c.moveToNext()) { 6595 String when = c.getString(0); 6596 String msg = c.getString(1); 6597 s.append("\n" + when + " : " + msg); 6598 } 6599 } 6600 } finally { 6601 IoUtils.closeQuietly(c); 6602 } 6603 } else { 6604 s.append(": pid=" + android.os.Process.myPid()); 6605 s.append(", fingerprint=" + Build.FINGERPRINT); 6606 } 6607 } 6608 return s.toString(); 6609 } 6610 } 6611