• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media;
18 
19 import static android.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&ouml;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