Merge "Sanitize paths before query"
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 9e281f7..41eb152 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -1198,6 +1198,23 @@
         public static final String IS_DOWNLOAD = "is_download";
 
         /**
+         * Generation number at which metadata for this media item was first
+         * inserted. This is useful for apps that are attempting to quickly
+         * identify exactly which media items have been added since a previous
+         * point in time. Generation numbers are monotonically increasing over
+         * time, and can be safely arithmetically compared.
+         * <p>
+         * Detecting media additions using generation numbers is more robust
+         * than using {@link #DATE_ADDED}, since those values may change in
+         * unexpected ways when apps use {@link File#setLastModified(long)} or
+         * when the system clock is set incorrectly.
+         *
+         * @see MediaStore#getGeneration(Context, String)
+         */
+        @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
+        public static final String GENERATION_ADDED = "generation_added";
+
+        /**
          * Generation number at which metadata for this media item was last
          * changed. This is useful for apps that are attempting to quickly
          * identify exactly which media items have changed since a previous
@@ -1212,7 +1229,7 @@
          * @see MediaStore#getGeneration(Context, String)
          */
         @Column(value = Cursor.FIELD_TYPE_INTEGER, readOnly = true)
-        public static final String GENERATION = "generation";
+        public static final String GENERATION_MODIFIED = "generation_modified";
 
         // =======================================
         // ==== MediaMetadataRetriever values ====
@@ -3655,30 +3672,33 @@
      * Return the latest generation value for the given volume.
      * <p>
      * Generation numbers are useful for apps that are attempting to quickly
-     * identify exactly which media items have changed since a previous point in
-     * time. Generation numbers are monotonically increasing over time, and can
-     * be safely arithmetically compared.
+     * identify exactly which media items have been added or changed since a
+     * previous point in time. Generation numbers are monotonically increasing
+     * over time, and can be safely arithmetically compared.
      * <p>
      * Detecting media changes using generation numbers is more robust than
-     * using {@link MediaColumns#DATE_MODIFIED}, since those values may change
-     * in unexpected ways when apps use {@link File#setLastModified(long)} or
-     * when the system clock is set incorrectly.
+     * using {@link MediaColumns#DATE_ADDED} or
+     * {@link MediaColumns#DATE_MODIFIED}, since those values may change in
+     * unexpected ways when apps use {@link File#setLastModified(long)} or when
+     * the system clock is set incorrectly.
      *
-     * @param volumeName specific volume to obtain an opaque version string for.
-     *            Must be one of the values returned from
+     * @param volumeName specific volume to obtain an generation value for. Must
+     *            be one of the values returned from
      *            {@link #getExternalVolumeNames(Context)}.
-     * @see MediaColumns#GENERATION
+     * @see MediaColumns#GENERATION_ADDED
+     * @see MediaColumns#GENERATION_MODIFIED
      */
     public static long getGeneration(@NonNull Context context, @NonNull String volumeName) {
-        final ContentResolver resolver = context.getContentResolver();
-        try (ContentProviderClient client = resolver.acquireContentProviderClient(AUTHORITY)) {
-            final Bundle in = new Bundle();
-            in.putString(Intent.EXTRA_TEXT, volumeName);
-            final Bundle out = client.call(GET_GENERATION_CALL, null, in);
-            return out.getLong(Intent.EXTRA_INDEX);
-        } catch (RemoteException e) {
-            throw e.rethrowAsRuntimeException();
-        }
+        return getGeneration(context.getContentResolver(), volumeName);
+    }
+
+    /** {@hide} */
+    public static long getGeneration(@NonNull ContentResolver resolver,
+            @NonNull String volumeName) {
+        final Bundle in = new Bundle();
+        in.putString(Intent.EXTRA_TEXT, volumeName);
+        final Bundle out = resolver.call(AUTHORITY, GET_GENERATION_CALL, null, in);
+        return out.getLong(Intent.EXTRA_INDEX);
     }
 
     /**
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index 7b29b92..812e6b6 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -14,6 +14,7 @@
 
 #define ATRACE_TAG ATRACE_TAG_APP
 #define LOG_TAG "FuseDaemon"
+#define LIBFUSE_LOG_TAG "libfuse"
 
 #include "FuseDaemon.h"
 
@@ -74,7 +75,9 @@
 
 // logging macros to avoid duplication.
 #define TRACE LOG(DEBUG)
+#define TRACE_VERBOSE LOG(VERBOSE)
 #define TRACE_FUSE(__fuse) TRACE << "[" << __fuse->path << "] "
+#define TRACE_FUSE_VERBOSE(__fuse) TRACE_VERBOSE << "[" << __fuse->path << "] "
 
 #define ATRACE_NAME(name) ScopedTrace ___tracer(name)
 #define ATRACE_CALL() ATRACE_NAME(__FUNCTION__)
@@ -319,7 +322,7 @@
  */
 static int set_file_lock(int fd, bool for_read, const std::string& path) {
     std::string lock_str = (for_read ? "read" : "write");
-    TRACE << "Setting " << lock_str << " lock for path " << path;
+    TRACE_VERBOSE << "Setting " << lock_str << " lock for path " << path;
 
     struct flock fl{};
     fl.l_type = for_read ? F_RDLCK : F_WRLCK;
@@ -330,7 +333,7 @@
         PLOG(ERROR) << "Failed to set " << lock_str << " lock on path " << path;
         return res;
     }
-    TRACE << "Successfully set " << lock_str << " lock on path " << path;
+    TRACE_VERBOSE << "Successfully set " << lock_str << " lock on path " << path;
     return res;
 }
 
@@ -344,7 +347,7 @@
  * Returns true if fd may have a lock, false otherwise
  */
 static bool is_file_locked(int fd, const std::string& path) {
-    TRACE << "Checking if file is locked " << path;
+    TRACE_VERBOSE << "Checking if file is locked " << path;
 
     struct flock fl{};
     fl.l_type = F_WRLCK;
@@ -357,7 +360,7 @@
         return true;
     }
     bool locked = fl.l_type != F_UNLCK;
-    TRACE << "File " << path << " is " << (locked ? "locked" : "unlocked");
+    TRACE_VERBOSE << "File " << path << " is " << (locked ? "locked" : "unlocked");
     return locked;
 }
 
@@ -453,8 +456,8 @@
     node* parent_node = fuse->FromInode(parent);
     string parent_path = parent_node->BuildPath();
 
-    TRACE_FUSE(fuse) << "LOOKUP " << name << " @ " << parent << " (" << safe_name(parent_node)
-                     << ")";
+    TRACE_FUSE_VERBOSE(fuse) << "LOOKUP " << name << " @ " << parent << " ("
+                             << safe_name(parent_node) << ")";
 
     string child_path = parent_path + "/" + name;
 
@@ -995,7 +998,6 @@
     ATRACE_CALL();
     handle* h = reinterpret_cast<handle*>(fi->fh);
     struct fuse* fuse = get_fuse(req);
-    TRACE_FUSE(fuse) << "READ";
 
     fuse->fadviser.Record(h->fd, size);
 
@@ -1316,7 +1318,7 @@
 
     node* node = fuse->FromInode(ino);
     const string path = node->BuildPath();
-    TRACE_FUSE(fuse) << "ACCESS " << path;
+    TRACE_FUSE_VERBOSE(fuse) << "ACCESS " << path;
 
     int res = access(path.c_str(), F_OK);
     fuse_reply_err(req, res ? errno : 0);
@@ -1481,11 +1483,11 @@
     });
 
 static void fuse_logger(enum fuse_log_level level, const char* fmt, va_list ap) {
-    __android_log_vprint(fuse_to_android_loglevel.at(level), LOG_TAG, fmt, ap);
+    __android_log_vprint(fuse_to_android_loglevel.at(level), LIBFUSE_LOG_TAG, fmt, ap);
 }
 
 bool FuseDaemon::ShouldOpenWithFuse(int fd, bool for_read, const std::string& path) {
-    TRACE << "Checking if file should be opened with FUSE " << path;
+    TRACE_VERBOSE << "Checking if file should be opened with FUSE " << path;
     bool use_fuse = false;
 
     if (active.load(std::memory_order_acquire)) {
diff --git a/logging.sh b/logging.sh
index 3a11df2..43f0d86 100755
--- a/logging.sh
+++ b/logging.sh
@@ -6,16 +6,24 @@
 then
     adb shell setprop log.tag.MediaProvider VERBOSE
     adb shell setprop log.tag.ModernMediaScanner VERBOSE
+    adb shell setprop log.tag.FuseDaemon DEBUG
+    adb shell setprop log.tag.libfuse DEBUG
 else
     adb shell setprop log.tag.MediaProvider INFO
     adb shell setprop log.tag.ModernMediaScanner INFO
+    adb shell setprop log.tag.FuseDaemon INFO
+    adb shell setprop log.tag.libfuse INFO
 fi
 
 if [ $level == "extreme" ]
 then
     adb shell setprop log.tag.SQLiteQueryBuilder VERBOSE
+    adb shell setprop log.tag.FuseDaemon VERBOSE
+    adb shell setprop log.tag.libfuse VERBOSE
 else
     adb shell setprop log.tag.SQLiteQueryBuilder INFO
+    adb shell setprop log.tag.FuseDaemon INFO
+    adb shell setprop log.tag.libfuse INFO
 fi
 
 # Kill process to kick new settings into place
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 53d7dc0..d146716 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -79,19 +79,13 @@
  * on demand, create and upgrade the schema, etc.
  */
 public class DatabaseHelper extends SQLiteOpenHelper implements AutoCloseable {
-    // maximum number of cached external databases to keep
-    private static final int MAX_EXTERNAL_DATABASES = 3;
-
-    // Delete databases that have not been used in two months
-    // 60 days in milliseconds (1000 * 60 * 60 * 24 * 60)
-    private static final long OBSOLETE_DATABASE_DB = 5184000000L;
-
     static final String INTERNAL_DATABASE_NAME = "internal.db";
     static final String EXTERNAL_DATABASE_NAME = "external.db";
 
     /**
      * Raw SQL clause that can be used to obtain the current generation, which
-     * is designed to be populated into {@link MediaColumns#GENERATION}.
+     * is designed to be populated into {@link MediaColumns#GENERATION_ADDED} or
+     * {@link MediaColumns#GENERATION_MODIFIED}.
      */
     public static final String CURRENT_GENERATION_CLAUSE = "SELECT generation FROM local_metadata";
 
@@ -208,89 +202,6 @@
         downgradeDatabase(db, oldV, newV);
     }
 
-    /**
-     * For devices that have removable storage, we support keeping multiple databases
-     * to allow users to switch between a number of cards.
-     * On such devices, touch this particular database and garbage collect old databases.
-     * An LRU cache system is used to clean up databases for old external
-     * storage volumes.
-     */
-    @Override
-    public void onOpen(SQLiteDatabase db) {
-        if (mEarlyUpgrade) return; // Doing early upgrade.
-        if (mInternal) return;  // The internal database is kept separately.
-
-        // the code below is only needed on devices with removable storage
-        if (!Environment.isExternalStorageRemovable()) return;
-
-        // touch the database file to show it is most recently used
-        File file = new File(db.getPath());
-        long now = System.currentTimeMillis();
-        file.setLastModified(now);
-
-        // delete least recently used databases if we are over the limit
-        String[] databases = mContext.databaseList();
-        // Don't delete wal auxiliary files(db-shm and db-wal) directly because db file may
-        // not be deleted, and it will cause Disk I/O error when accessing this database.
-        List<String> dbList = new ArrayList<String>();
-        for (String database : databases) {
-            if (database != null && database.endsWith(".db")) {
-                dbList.add(database);
-            }
-        }
-        databases = dbList.toArray(new String[0]);
-        int count = databases.length;
-        int limit = MAX_EXTERNAL_DATABASES;
-
-        // delete external databases that have not been used in the past two months
-        long twoMonthsAgo = now - OBSOLETE_DATABASE_DB;
-        for (int i = 0; i < databases.length; i++) {
-            File other = mContext.getDatabasePath(databases[i]);
-            if (INTERNAL_DATABASE_NAME.equals(databases[i]) || file.equals(other)) {
-                databases[i] = null;
-                count--;
-                if (file.equals(other)) {
-                    // reduce limit to account for the existence of the database we
-                    // are about to open, which we removed from the list.
-                    limit--;
-                }
-            } else {
-                long time = other.lastModified();
-                if (time < twoMonthsAgo) {
-                    if (LOGV) Log.v(TAG, "Deleting old database " + databases[i]);
-                    mContext.deleteDatabase(databases[i]);
-                    databases[i] = null;
-                    count--;
-                }
-            }
-        }
-
-        // delete least recently used databases until
-        // we are no longer over the limit
-        while (count > limit) {
-            int lruIndex = -1;
-            long lruTime = 0;
-
-            for (int i = 0; i < databases.length; i++) {
-                if (databases[i] != null) {
-                    long time = mContext.getDatabasePath(databases[i]).lastModified();
-                    if (lruTime == 0 || time < lruTime) {
-                        lruIndex = i;
-                        lruTime = time;
-                    }
-                }
-            }
-
-            // delete least recently used database
-            if (lruIndex != -1) {
-                if (LOGV) Log.v(TAG, "Deleting old database " + databases[lruIndex]);
-                mContext.deleteDatabase(databases[lruIndex]);
-                databases[lruIndex] = null;
-                count--;
-            }
-        }
-    }
-
     @GuardedBy("mProjectionMapCache")
     private final ArrayMap<Class<?>[], ArrayMap<String, String>>
             mProjectionMapCache = new ArrayMap<>();
@@ -531,7 +442,8 @@
                 + "is_favorite INTEGER DEFAULT 0, num_tracks INTEGER DEFAULT NULL,"
                 + "writer TEXT DEFAULT NULL, exposure_time TEXT DEFAULT NULL,"
                 + "f_number TEXT DEFAULT NULL, iso INTEGER DEFAULT NULL,"
-                + "scene_capture_type INTEGER DEFAULT NULL, generation INTEGER DEFAULT 0)");
+                + "scene_capture_type INTEGER DEFAULT NULL, generation_added INTEGER DEFAULT 0,"
+                + "generation_modified INTEGER DEFAULT 0)");
 
         db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
         if (!mInternal) {
@@ -714,7 +626,7 @@
 
         db.execSQL("CREATE VIEW audio_artists AS SELECT "
                 + "  artist_id AS " + Audio.Artists._ID
-                + ", artist AS " + Audio.Artists.ARTIST
+                + ", MIN(artist) AS " + Audio.Artists.ARTIST
                 + ", artist_key AS " + Audio.Artists.ARTIST_KEY
                 + ", COUNT(DISTINCT album_id) AS " + Audio.Artists.NUMBER_OF_ALBUMS
                 + ", COUNT(DISTINCT _id) AS " + Audio.Artists.NUMBER_OF_TRACKS
@@ -725,7 +637,7 @@
         db.execSQL("CREATE VIEW audio_albums AS SELECT "
                 + "  album_id AS " + Audio.Albums._ID
                 + ", album_id AS " + Audio.Albums.ALBUM_ID
-                + ", album AS " + Audio.Albums.ALBUM
+                + ", MIN(album) AS " + Audio.Albums.ALBUM
                 + ", album_key AS " + Audio.Albums.ALBUM_KEY
                 + ", artist_id AS " + Audio.Albums.ARTIST_ID
                 + ", artist AS " + Audio.Albums.ARTIST
@@ -741,7 +653,7 @@
 
         db.execSQL("CREATE VIEW audio_genres AS SELECT "
                 + "  genre_id AS " + Audio.Genres._ID
-                + ", genre AS " + Audio.Genres.NAME
+                + ", MIN(genre) AS " + Audio.Genres.NAME
                 + " FROM audio"
                 + " WHERE volume_name IN " + filterVolumeNames
                 + " GROUP BY genre_id");
@@ -938,11 +850,14 @@
         db.execSQL("DELETE FROM log;");
     }
 
-    private static void updateAddGeneration(SQLiteDatabase db, boolean internal) {
+    private static void updateAddLocalMetadata(SQLiteDatabase db, boolean internal) {
         db.execSQL("CREATE TABLE local_metadata (generation INTEGER DEFAULT 0)");
         db.execSQL("INSERT INTO local_metadata VALUES (0)");
+    }
 
-        db.execSQL("ALTER TABLE files ADD COLUMN generation INTEGER DEFAULT 0;");
+    private static void updateAddGeneration(SQLiteDatabase db, boolean internal) {
+        db.execSQL("ALTER TABLE files ADD COLUMN generation_added INTEGER DEFAULT 0;");
+        db.execSQL("ALTER TABLE files ADD COLUMN generation_modified INTEGER DEFAULT 0;");
     }
 
     private static void recomputeDataValues(SQLiteDatabase db, boolean internal) {
@@ -973,7 +888,7 @@
     static final int VERSION_O = 800;
     static final int VERSION_P = 900;
     static final int VERSION_Q = 1023;
-    static final int VERSION_R = 1108;
+    static final int VERSION_R = 1109;
     static final int VERSION_LATEST = VERSION_R;
 
     /**
@@ -1096,6 +1011,9 @@
                 updateAddSceneCaptureType(db, internal);
             }
             if (fromVersion < 1108) {
+                updateAddLocalMetadata(db, internal);
+            }
+            if (fromVersion < 1109) {
                 updateAddGeneration(db, internal);
             }
 
@@ -1158,7 +1076,8 @@
 
     /**
      * Return the current generation that will be populated into
-     * {@link MediaColumns#GENERATION}.
+     * {@link MediaColumns#GENERATION_ADDED} or
+     * {@link MediaColumns#GENERATION_MODIFIED}.
      */
     public long getGeneration() {
         return android.database.DatabaseUtils.longForQuery(getReadableDatabase(),
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index d4f32f3..3942057 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -23,6 +23,7 @@
 
 import static com.android.providers.media.util.PermissionUtils.checkIsLegacyStorageGranted;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionBackup;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionManageExternalStorage;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
 import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
@@ -186,6 +187,7 @@
     public static final int PERMISSION_IS_LEGACY_READ = 1 << 9;
     public static final int PERMISSION_IS_LEGACY_GRANTED = 1 << 10;
     public static final int PERMISSION_IS_BACKUP = 1 << 11;
+    public static final int PERMISSION_MANAGE_EXTERNAL_STORAGE = 1 << 12;
 
     private int hasPermission;
     private int hasPermissionResolved;
@@ -233,6 +235,8 @@
                 return checkPermissionWriteVideo(context, pid, uid, getPackageName());
             case PERMISSION_WRITE_IMAGES:
                 return checkPermissionWriteImages(context, pid, uid, getPackageName());
+            case PERMISSION_MANAGE_EXTERNAL_STORAGE:
+                return checkPermissionManageExternalStorage(context, pid, uid, packageName);
             default:
                 return false;
         }
diff --git a/src/com/android/providers/media/MediaDocumentsProvider.java b/src/com/android/providers/media/MediaDocumentsProvider.java
index 65150c8..8576f2e 100644
--- a/src/com/android/providers/media/MediaDocumentsProvider.java
+++ b/src/com/android/providers/media/MediaDocumentsProvider.java
@@ -86,7 +86,7 @@
 public class MediaDocumentsProvider extends DocumentsProvider {
     private static final String TAG = "MediaDocumentsProvider";
 
-    private static final String AUTHORITY = "com.android.providers.media.documents";
+    public static final String AUTHORITY = "com.android.providers.media.documents";
 
     private static final String SUPPORTED_QUERY_ARGS = joinNewline(
             DocumentsContract.QUERY_ARG_DISPLAY_NAME,
@@ -112,18 +112,18 @@
     private static final String AUDIO_MIME_TYPES = joinNewline(
             "audio/*", "application/ogg", "application/x-flac");
 
-    private static final String TYPE_IMAGES_ROOT = "images_root";
-    private static final String TYPE_IMAGES_BUCKET = "images_bucket";
-    private static final String TYPE_IMAGE = "image";
+    static final String TYPE_IMAGES_ROOT = "images_root";
+    static final String TYPE_IMAGES_BUCKET = "images_bucket";
+    static final String TYPE_IMAGE = "image";
 
-    private static final String TYPE_VIDEOS_ROOT = "videos_root";
-    private static final String TYPE_VIDEOS_BUCKET = "videos_bucket";
-    private static final String TYPE_VIDEO = "video";
+    static final String TYPE_VIDEOS_ROOT = "videos_root";
+    static final String TYPE_VIDEOS_BUCKET = "videos_bucket";
+    static final String TYPE_VIDEO = "video";
 
-    private static final String TYPE_AUDIO_ROOT = "audio_root";
-    private static final String TYPE_AUDIO = "audio";
-    private static final String TYPE_ARTIST = "artist";
-    private static final String TYPE_ALBUM = "album";
+    static final String TYPE_AUDIO_ROOT = "audio_root";
+    static final String TYPE_AUDIO = "audio";
+    static final String TYPE_ARTIST = "artist";
+    static final String TYPE_ALBUM = "album";
 
     private static boolean sReturnedImagesEmpty = false;
     private static boolean sReturnedVideosEmpty = false;
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 47d0bbb..31da5f6 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -44,6 +44,7 @@
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_LEGACY_WRITE;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_REDACTION_NEEDED;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_IS_SYSTEM;
+import static com.android.providers.media.LocalCallingIdentity.PERMISSION_MANAGE_EXTERNAL_STORAGE;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_AUDIO;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_IMAGES;
 import static com.android.providers.media.LocalCallingIdentity.PERMISSION_READ_VIDEO;
@@ -1071,8 +1072,8 @@
     /**
      * Updates database entry for given {@code path} with {@code values}
      */
-    private boolean updateDatabaseForFuseRename(DatabaseHelper helper, String oldPath, String newPath,
-            ContentValues values) {
+    private boolean updateDatabaseForFuseRename(@NonNull DatabaseHelper helper,
+            @NonNull String oldPath, @NonNull String newPath, @NonNull ContentValues values) {
         final Uri uriOldPath = Files.getContentUriForPath(oldPath);
         boolean allowHidden = isCallingPackageAllowedHidden();
         final SQLiteQueryBuilder qbForUpdate = getQueryBuilder(TYPE_UPDATE,
@@ -4984,10 +4985,15 @@
     private boolean shouldBypassFuseRestrictions(boolean forWrite) {
         boolean isRequestingLegacyStorage = forWrite ? isCallingPackageLegacyWrite()
                 : isCallingPackageLegacyRead();
+        if (isRequestingLegacyStorage) {
+            return true;
+        }
 
-        // TODO(b/137755945): We should let file managers bypass FUSE restrictions as well.
-        //  Remember to change the documentation above when this is addressed.
-        return isRequestingLegacyStorage;
+        if (mCallingIdentity.get().hasPermission(PERMISSION_MANAGE_EXTERNAL_STORAGE)) {
+            return true;
+        }
+
+        return false;
     }
 
     /**
@@ -5970,11 +5976,12 @@
         }
 
         final Uri uri = MediaStore.AUTHORITY_URI.buildUpon().appendPath(volume).build();
-        getContext().getContentResolver().notifyChange(uri, null);
+        final DatabaseHelper helper = MediaStore.VOLUME_INTERNAL.equals(volume)
+                ? mInternalDatabase : mExternalDatabase;
+        acceptWithExpansion(helper::notifyChange, uri);
         if (LOGV) Log.v(TAG, "Attached volume: " + volume);
         if (!MediaStore.VOLUME_INTERNAL.equals(volume)) {
             BackgroundThread.getExecutor().execute(() -> {
-                final DatabaseHelper helper = mExternalDatabase;
                 ensureDefaultFolders(volume, helper);
             });
         }
@@ -6007,7 +6014,9 @@
         }
 
         final Uri uri = MediaStore.AUTHORITY_URI.buildUpon().appendPath(volume).build();
-        getContext().getContentResolver().notifyChange(uri, null);
+        final DatabaseHelper helper = MediaStore.VOLUME_INTERNAL.equals(volume)
+                ? mInternalDatabase : mExternalDatabase;
+        acceptWithExpansion(helper::notifyChange, uri);
         if (LOGV) Log.v(TAG, "Detached volume: " + volume);
     }
 
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 58c453a..fc199da 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -263,7 +263,7 @@
         private final Uri mFilesUri;
         private final CancellationSignal mSignal;
 
-        private final long mStartCurrentTime;
+        private final long mStartGeneration;
         private final boolean mSingleFile;
         private final Set<Path> mAcquiredDirectoryLocks = new ArraySet<>();
         private final ArrayList<ContentProviderOperation> mPending = new ArrayList<>();
@@ -291,7 +291,7 @@
             mFilesUri = MediaStore.Files.getContentUri(mVolumeName);
             mSignal = getOrCreateSignal(mVolumeName);
 
-            mStartCurrentTime = System.currentTimeMillis();
+            mStartGeneration = MediaStore.getGeneration(mResolver, mVolumeName);
             mSingleFile = mRoot.isFile();
 
             Trace.endSection();
@@ -362,12 +362,12 @@
                     + MtpConstants.FORMAT_UNDEFINED + ") != "
                     + MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST;
             final String dataClause = FileColumns.DATA + " LIKE ? ESCAPE '\\'";
-            final String addedClause = "ifnull(" + FileColumns.DATE_ADDED + ",0) < "
-                    + mStartCurrentTime;
+            final String generationClause = FileColumns.GENERATION_ADDED + " <= "
+                    + mStartGeneration;
 
             final Bundle queryArgs = new Bundle();
             queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SELECTION,
-                    formatClause + " AND " + dataClause + " AND " + addedClause);
+                    formatClause + " AND " + dataClause + " AND " + generationClause);
             queryArgs.putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS,
                     new String[] { escapeForLike(mRoot.getAbsolutePath(), mSingleFile) });
             queryArgs.putString(ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
diff --git a/src/com/android/providers/media/util/DatabaseUtils.java b/src/com/android/providers/media/util/DatabaseUtils.java
index de65ebe..3c18dcf 100644
--- a/src/com/android/providers/media/util/DatabaseUtils.java
+++ b/src/com/android/providers/media/util/DatabaseUtils.java
@@ -466,7 +466,7 @@
 
         for (int i = 0; i < bindArgs.length; i++) {
             final Object bindArg = bindArgs[i];
-            switch (DatabaseUtils.getTypeOfObject(bindArg)) {
+            switch (getTypeOfObject(bindArg)) {
                 case Cursor.FIELD_TYPE_NULL:
                     st.bindNull(i + 1);
                     break;
diff --git a/src/com/android/providers/media/util/PermissionUtils.java b/src/com/android/providers/media/util/PermissionUtils.java
index 732b0ce..0786c88 100644
--- a/src/com/android/providers/media/util/PermissionUtils.java
+++ b/src/com/android/providers/media/util/PermissionUtils.java
@@ -22,6 +22,7 @@
 import static android.Manifest.permission.WRITE_MEDIA_STORAGE;
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.app.AppOpsManager.OPSTR_LEGACY_STORAGE;
+import static android.app.AppOpsManager.OPSTR_MANAGE_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.OPSTR_READ_EXTERNAL_STORAGE;
 import static android.app.AppOpsManager.OPSTR_READ_MEDIA_AUDIO;
 import static android.app.AppOpsManager.OPSTR_READ_MEDIA_IMAGES;
@@ -32,6 +33,7 @@
 import static android.app.AppOpsManager.OPSTR_WRITE_MEDIA_VIDEO;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 
+import android.annotation.NonNull;
 import android.app.AppOpsManager;
 import android.content.Context;
 
@@ -68,6 +70,11 @@
         return context.checkPermission(BACKUP, pid, uid) == PERMISSION_GRANTED;
     }
 
+    public static boolean checkPermissionManageExternalStorage(Context context, int pid, int uid,
+            String packageName) {
+        return hasAppOpPermission(context, pid, uid, packageName, OPSTR_MANAGE_EXTERNAL_STORAGE);
+    }
+
     public static boolean checkPermissionWriteStorage(Context context,
             int pid, int uid, String packageName) {
         return checkPermissionAndAppOp(context, pid,
@@ -160,6 +167,23 @@
         }
     }
 
+    /**
+     * Checks if calling app is allowed the app-op. If its app-op mode is
+     * {@link AppOpsManager#MODE_DEFAULT} then it falls back checking the appropriate permission for
+     * the app-op. The permissions is retrieved from {@link AppOpsManager#opToPermission(String)}.
+     */
+    private static boolean hasAppOpPermission(@NonNull Context context, int pid, int uid,
+            @NonNull String packageName, @NonNull String op) {
+        final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
+        final int mode = appOps.noteOpNoThrow(op, uid, packageName, null, null);
+        if (mode == AppOpsManager.MODE_DEFAULT) {
+            final String permission = AppOpsManager.opToPermission(op);
+            return permission != null
+                    && context.checkPermission(permission, pid, uid) == PERMISSION_GRANTED;
+        }
+        return mode == AppOpsManager.MODE_ALLOWED;
+    }
+
     private static boolean noteAppOpAllowingLegacy(Context context,
             int pid, int uid, String packageName, String op) {
         final AppOpsManager appOps = context.getSystemService(AppOpsManager.class);
diff --git a/src/com/android/providers/media/util/SQLiteQueryBuilder.java b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
index 9c64ef6..fac5112 100644
--- a/src/com/android/providers/media/util/SQLiteQueryBuilder.java
+++ b/src/com/android/providers/media/util/SQLiteQueryBuilder.java
@@ -799,7 +799,8 @@
 
         final boolean hasGeneration = Objects.equals(mTables, "files");
         if (hasGeneration) {
-            values.remove(MediaColumns.GENERATION);
+            values.remove(MediaColumns.GENERATION_ADDED);
+            values.remove(MediaColumns.GENERATION_MODIFIED);
         }
 
         final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
@@ -812,7 +813,9 @@
         }
         if (hasGeneration) {
             sql.append(',');
-            sql.append(MediaColumns.GENERATION);
+            sql.append(MediaColumns.GENERATION_ADDED);
+            sql.append(',');
+            sql.append(MediaColumns.GENERATION_MODIFIED);
         }
         sql.append(") VALUES (");
         for (int i = 0; i < rawValues.size(); i++) {
@@ -826,6 +829,10 @@
             sql.append('(');
             sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
             sql.append(')');
+            sql.append(',');
+            sql.append('(');
+            sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
+            sql.append(')');
         }
         sql.append(")");
         return sql.toString();
@@ -844,7 +851,8 @@
 
         final boolean hasGeneration = Objects.equals(mTables, "files");
         if (hasGeneration) {
-            values.remove(MediaColumns.GENERATION);
+            values.remove(MediaColumns.GENERATION_ADDED);
+            values.remove(MediaColumns.GENERATION_MODIFIED);
         }
 
         final ArrayMap<String, Object> rawValues = com.android.providers.media.util.DatabaseUtils
@@ -858,7 +866,7 @@
         }
         if (hasGeneration) {
             sql.append(',');
-            sql.append(MediaColumns.GENERATION);
+            sql.append(MediaColumns.GENERATION_MODIFIED);
             sql.append('=');
             sql.append('(');
             sql.append(DatabaseHelper.CURRENT_GENERATION_CLAUSE);
diff --git a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
index 391ef1c..46203dc 100644
--- a/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
+++ b/tests/jni/FuseDaemonTest/host/src/com/android/tests/fused/host/FuseDaemonHostTest.java
@@ -183,4 +183,9 @@
     public void testRenameEmptyDirectory() throws Exception {
         runDeviceTest("testRenameEmptyDirectory");
     }
+
+    @Test
+    public void testManageExternalStorageBypassesMediaProviderRestrictions() throws Exception {
+        runDeviceTest("testManageExternalStorageBypassesMediaProviderRestrictions");
+    }
 }
diff --git a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
index b4202e5..f831dce 100644
--- a/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
+++ b/tests/jni/FuseDaemonTest/libs/FuseDaemonTestLib/src/com/android/tests/fused/lib/TestUtils.java
@@ -96,6 +96,14 @@
         }
     }
 
+    public static void adoptShellPermissionIdentity(String... permissions) {
+        sUiAutomation.adoptShellPermissionIdentity(permissions);
+    }
+
+    public static void dropShellPermissionIdentity() {
+        sUiAutomation.dropShellPermissionIdentity();
+    }
+
     public static String executeShellCommand(String cmd) throws Exception {
         try (FileInputStream output = new FileInputStream (sUiAutomation.executeShellCommand(cmd)
                 .getFileDescriptor())) {
diff --git a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
index 793aaf7..7cc1b20 100644
--- a/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
+++ b/tests/jni/FuseDaemonTest/src/com/android/tests/fused/FilePathAccessTest.java
@@ -25,9 +25,11 @@
 import static com.android.tests.fused.lib.RedactionTestHelper.assertExifMetadataMismatch;
 import static com.android.tests.fused.lib.RedactionTestHelper.getExifMetadata;
 import static com.android.tests.fused.lib.RedactionTestHelper.getExifMetadataFromRawResource;
+import static com.android.tests.fused.lib.TestUtils.adoptShellPermissionIdentity;
 import static com.android.tests.fused.lib.TestUtils.assertThrows;
 import static com.android.tests.fused.lib.TestUtils.createFileAs;
 import static com.android.tests.fused.lib.TestUtils.deleteFileAs;
+import static com.android.tests.fused.lib.TestUtils.dropShellPermissionIdentity;
 import static com.android.tests.fused.lib.TestUtils.executeShellCommand;
 import static com.android.tests.fused.lib.TestUtils.getFileMimeTypeFromDatabase;
 import static com.android.tests.fused.lib.TestUtils.getFileRowIdFromDatabase;
@@ -40,6 +42,7 @@
 
 import static org.junit.Assume.assumeTrue;
 
+import android.Manifest;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -1006,6 +1009,16 @@
         }
     }
 
+    @Test
+    public void testManageExternalStorageBypassesMediaProviderRestrictions() throws Exception {
+        adoptShellPermissionIdentity(Manifest.permission.MANAGE_EXTERNAL_STORAGE);
+        try {
+            assertCanCreateFile(new File(EXTERNAL_STORAGE_DIR, NONMEDIA_FILE_NAME));
+        } finally {
+            dropShellPermissionIdentity();
+        }
+    }
+
     private void deleteWithMediaProvider(String relativePath, String displayName) throws Exception {
         String selection = MediaColumns.RELATIVE_PATH + " = ? AND "
                 + MediaColumns.DISPLAY_NAME + " = ?";
diff --git a/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java b/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
new file mode 100644
index 0000000..dc1767f
--- /dev/null
+++ b/tests/src/com/android/providers/media/MediaDocumentsProviderTest.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+public class MediaDocumentsProviderTest {
+    @Test
+    public void testSimple() {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final Context isolatedContext = new IsolatedContext(context, "modern");
+        final ContentResolver resolver = isolatedContext.getContentResolver();
+
+        assertProbe(resolver, "root");
+
+        for (String root : new String[] {
+                MediaDocumentsProvider.TYPE_AUDIO_ROOT,
+                MediaDocumentsProvider.TYPE_VIDEOS_ROOT,
+                MediaDocumentsProvider.TYPE_IMAGES_ROOT,
+        }) {
+            assertProbe(resolver, "root", root, "search");
+
+            assertProbe(resolver, "document", root);
+            assertProbe(resolver, "document", root, "children");
+        }
+
+        for (String recent : new String[] {
+                MediaDocumentsProvider.TYPE_VIDEOS_ROOT,
+                MediaDocumentsProvider.TYPE_IMAGES_ROOT,
+        }) {
+            assertProbe(resolver, "root", recent, "recent");
+        }
+
+        for (String dir : new String[] {
+                MediaDocumentsProvider.TYPE_VIDEOS_BUCKET,
+                MediaDocumentsProvider.TYPE_IMAGES_BUCKET,
+        }) {
+            assertProbe(resolver, "document", dir, "children");
+        }
+
+        for (String item : new String[] {
+                MediaDocumentsProvider.TYPE_ARTIST,
+                MediaDocumentsProvider.TYPE_ALBUM,
+                MediaDocumentsProvider.TYPE_VIDEOS_BUCKET,
+                MediaDocumentsProvider.TYPE_IMAGES_BUCKET,
+
+                MediaDocumentsProvider.TYPE_AUDIO,
+                MediaDocumentsProvider.TYPE_VIDEO,
+                MediaDocumentsProvider.TYPE_IMAGE,
+        }) {
+                assertProbe(resolver, "document", item);
+        }
+    }
+
+    private static void assertProbe(ContentResolver resolver, String... paths) {
+        final Uri.Builder probe = Uri.parse("content://" + MediaDocumentsProvider.AUTHORITY)
+                .buildUpon();
+        for (String path : paths) {
+            probe.appendPath(path);
+        }
+        try (Cursor c = resolver.query(probe.build(), null, Bundle.EMPTY, null)) {
+            assertNotNull(Arrays.toString(paths), c);
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/media/scan/MediaScannerTest.java b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
index 4c72ca6..44d5cfb 100644
--- a/tests/src/com/android/providers/media/scan/MediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
@@ -43,6 +43,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.providers.media.MediaDocumentsProvider;
 import com.android.providers.media.MediaProvider;
 import com.android.providers.media.R;
 import com.android.providers.media.util.FileUtils;
@@ -67,6 +68,7 @@
         private final File mDir;
         private final MockContentResolver mResolver;
         private final MediaProvider mProvider;
+        private final MediaDocumentsProvider mDocumentsProvider;
 
         public IsolatedContext(Context base, String tag) {
             super(base);
@@ -80,8 +82,14 @@
                     .resolveContentProvider(MediaStore.AUTHORITY, 0);
             mProvider = new MediaProvider();
             mProvider.attachInfo(this, info);
-
             mResolver.addProvider(MediaStore.AUTHORITY, mProvider);
+
+            final ProviderInfo documentsInfo = base.getPackageManager()
+                    .resolveContentProvider(MediaDocumentsProvider.AUTHORITY, 0);
+            mDocumentsProvider = new MediaDocumentsProvider();
+            mDocumentsProvider.attachInfo(this, documentsInfo);
+            mResolver.addProvider(MediaDocumentsProvider.AUTHORITY, mDocumentsProvider);
+
             mResolver.addProvider(Settings.AUTHORITY, new MockContentProvider() {
                 @Override
                 public Bundle call(String method, String request, Bundle args) {
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index f1ece3d..f7f1fd3 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -35,7 +35,6 @@
 import android.graphics.Bitmap;
 import android.media.ExifInterface;
 import android.net.Uri;
-import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Files.FileColumns;
@@ -44,8 +43,8 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
 import com.android.providers.media.R;
+import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
 import com.android.providers.media.util.FileUtils;
 
 import org.junit.After;
diff --git a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
index ed84dcc..635b042 100644
--- a/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/DatabaseUtilsTest.java
@@ -28,6 +28,7 @@
 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION;
 import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER;
 import static android.content.ContentResolver.QUERY_SORT_DIRECTION_ASCENDING;
+import static android.database.DatabaseUtils.bindSelection;
 
 import static com.android.providers.media.util.DatabaseUtils.maybeBalance;
 import static com.android.providers.media.util.DatabaseUtils.recoverAbusiveLimit;
@@ -58,6 +59,8 @@
     private final Bundle args = new Bundle();
     private final ArraySet<String> honored = new ArraySet<>();
 
+    private static final Object[] ARGS = { "baz", 4, null };
+
     @Before
     public void setUp() {
         args.clear();
@@ -65,6 +68,41 @@
     }
 
     @Test
+    public void testBindSelection_none() throws Exception {
+        assertEquals(null,
+                bindSelection(null, ARGS));
+        assertEquals("",
+                bindSelection("", ARGS));
+        assertEquals("foo=bar",
+                bindSelection("foo=bar", ARGS));
+    }
+
+    @Test
+    public void testBindSelection_normal() throws Exception {
+        assertEquals("foo='baz'",
+                bindSelection("foo=?", ARGS));
+        assertEquals("foo='baz' AND bar=4",
+                bindSelection("foo=? AND bar=?", ARGS));
+        assertEquals("foo='baz' AND bar=4 AND meow=NULL",
+                bindSelection("foo=? AND bar=? AND meow=?", ARGS));
+    }
+
+    @Test
+    public void testBindSelection_whitespace() throws Exception {
+        assertEquals("BETWEEN 5 AND 10",
+                bindSelection("BETWEEN? AND ?", 5, 10));
+        assertEquals("IN 'foo'",
+                bindSelection("IN?", "foo"));
+    }
+
+    @Test
+    public void testBindSelection_indexed() throws Exception {
+        assertEquals("foo=10 AND bar=11 AND meow=1",
+                bindSelection("foo=?10 AND bar=? AND meow=?1",
+                        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
+    }
+
+    @Test
     public void testResolveQueryArgs_GroupBy() throws Exception {
         args.putStringArray(QUERY_ARG_GROUP_COLUMNS, new String[] { "foo", "bar" });
         args.putString(QUERY_ARG_SQL_GROUP_BY, "raw");
diff --git a/tests/src/com/android/providers/media/util/LongArrayTest.java b/tests/src/com/android/providers/media/util/LongArrayTest.java
new file mode 100644
index 0000000..f2e82f3
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/LongArrayTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class LongArrayTest {
+
+    @Test
+    public void testLongArray() {
+        LongArray a = new LongArray();
+        a.add(1);
+        a.add(2);
+        a.add(3);
+        verify(new long[]{1, 2, 3}, a);
+
+        LongArray b = LongArray.fromArray(new long[]{4, 5, 6, 7, 8}, 3);
+        a.addAll(b);
+        verify(new long[]{1, 2, 3, 4, 5, 6}, a);
+
+        a.resize(2);
+        verify(new long[]{1, 2}, a);
+
+        a.resize(8);
+        verify(new long[]{1, 2, 0, 0, 0, 0, 0, 0}, a);
+
+        a.set(5, 10);
+        verify(new long[]{1, 2, 0, 0, 0, 10, 0, 0}, a);
+
+        a.add(5, 20);
+        assertEquals(20, a.get(5));
+        assertEquals(5, a.indexOf(20));
+        verify(new long[]{1, 2, 0, 0, 0, 20, 10, 0, 0}, a);
+
+        assertEquals(-1, a.indexOf(99));
+
+        a.resize(15);
+        a.set(14, 30);
+        verify(new long[]{1, 2, 0, 0, 0, 20, 10, 0, 0, 0, 0, 0, 0, 0, 30}, a);
+
+        long[] backingArray = new long[]{1, 2, 3, 4};
+        a = LongArray.wrap(backingArray);
+        a.set(0, 10);
+        assertEquals(10, backingArray[0]);
+        backingArray[1] = 20;
+        backingArray[2] = 30;
+        verify(backingArray, a);
+        assertEquals(2, a.indexOf(30));
+
+        a.resize(2);
+        assertEquals(0, backingArray[2]);
+        assertEquals(0, backingArray[3]);
+
+        a.add(50);
+        verify(new long[]{10, 20, 50}, a);
+    }
+
+    public void verify(long[] expected, LongArray longArrays) {
+        assertEquals(expected.length, longArrays.size());
+        assertArrayEquals(expected, longArrays.toArray());
+    }
+}
diff --git a/tests/src/com/android/providers/media/util/MemoryTest.java b/tests/src/com/android/providers/media/util/MemoryTest.java
new file mode 100644
index 0000000..aa9511b
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/MemoryTest.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.util;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.ByteOrder;
+
+@RunWith(AndroidJUnit4.class)
+public class MemoryTest {
+    private final byte[] buf = new byte[4];
+
+    @Test
+    public void testBigEndian() {
+        final int expected = 42;
+        Memory.pokeInt(buf, 0, expected, ByteOrder.BIG_ENDIAN);
+        final int actual = Memory.peekInt(buf, 0, ByteOrder.BIG_ENDIAN);
+        assertEquals(expected, actual);
+    }
+
+    @Test
+    public void testLittleEndian() {
+        final int expected = 42;
+        Memory.pokeInt(buf, 0, expected, ByteOrder.LITTLE_ENDIAN);
+        final int actual = Memory.peekInt(buf, 0, ByteOrder.LITTLE_ENDIAN);
+        assertEquals(expected, actual);
+    }
+}
diff --git a/tests/src/com/android/providers/media/util/MetricsTest.java b/tests/src/com/android/providers/media/util/MetricsTest.java
new file mode 100644
index 0000000..6041734
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/MetricsTest.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.util;
+
+import android.provider.MediaStore;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.providers.media.scan.MediaScanner;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class MetricsTest {
+
+    /**
+     * The best we can do for coverage is make sure we don't explode?
+     */
+    @Test
+    public void testSimple() throws Exception {
+        final String volumeName = MediaStore.VOLUME_EXTERNAL_PRIMARY;
+        final String packageName = "com.example";
+
+        Metrics.logScan(volumeName, MediaScanner.REASON_UNKNOWN, 42, 42, 42, 42, 42);
+        Metrics.logDeletion(volumeName, 42, packageName, 42);
+        Metrics.logPermissionGranted(volumeName, 42, packageName, 42);
+        Metrics.logPermissionDenied(volumeName, 42, packageName, 42);
+        Metrics.logSchemaChange(volumeName, 42, 42, 42, 42);
+        Metrics.logIdleMaintenance(volumeName, 42, 42, 42, 42);
+    }
+}
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
new file mode 100644
index 0000000..af7535f
--- /dev/null
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.util;
+
+import static com.android.providers.media.util.PermissionUtils.checkPermissionBackup;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadAudio;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadImages;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadStorage;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionReadVideo;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionSystem;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteAudio;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteImages;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteStorage;
+import static com.android.providers.media.util.PermissionUtils.checkPermissionWriteVideo;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class PermissionUtilsTest {
+    /**
+     * The best we can do here is assert that we're granted the permissions that
+     * we expect to be holding.
+     */
+    @Test
+    public void testSelfPermissions() throws Exception {
+        final Context context = InstrumentationRegistry.getContext();
+        final int pid = android.os.Process.myPid();
+        final int uid = android.os.Process.myUid();
+        final String packageName = context.getPackageName();
+
+        assertTrue(checkPermissionSystem(context, pid, uid, packageName));
+        assertFalse(checkPermissionBackup(context, pid, uid));
+
+        assertTrue(checkPermissionReadStorage(context, pid, uid, packageName));
+        assertTrue(checkPermissionWriteStorage(context, pid, uid, packageName));
+
+        assertTrue(checkPermissionReadAudio(context, pid, uid, packageName));
+        assertFalse(checkPermissionWriteAudio(context, pid, uid, packageName));
+        assertTrue(checkPermissionReadVideo(context, pid, uid, packageName));
+        assertFalse(checkPermissionWriteVideo(context, pid, uid, packageName));
+        assertTrue(checkPermissionReadImages(context, pid, uid, packageName));
+        assertFalse(checkPermissionWriteImages(context, pid, uid, packageName));
+    }
+}