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