Merge "Fix DATA column docs" into sc-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 336e8eb..d57a013 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -89,10 +89,6 @@
                 <data android:scheme="package" />
             </intent-filter>
             <intent-filter>
-                <action android:name="android.intent.action.MEDIA_MOUNTED" />
-                <data android:scheme="file" />
-            </intent-filter>
-            <intent-filter>
                 <action android:name="android.intent.action.MEDIA_SCANNER_SCAN_FILE" />
                 <data android:scheme="file" />
             </intent-filter>
diff --git a/apex/Android.bp b/apex/Android.bp
index a5e9942..2668806 100644
--- a/apex/Android.bp
+++ b/apex/Android.bp
@@ -17,7 +17,8 @@
 
 apex_defaults {
     name: "com.android.mediaprovider-defaults",
-    java_libs: ["framework-mediaprovider"],
+    bootclasspath_fragments: ["com.android.mediaprovider-bootclasspath-fragment"],
+    prebuilts: ["current_sdkinfo"],
     key: "com.android.mediaprovider.key",
     certificate: ":com.android.mediaprovider.certificate",
     file_contexts: ":com.android.mediaprovider-file_contexts",
@@ -45,3 +46,10 @@
         "framework-mediaprovider",
     ],
 }
+
+// Encapsulate the contributions made by the com.android.mediaprovider to the bootclasspath.
+bootclasspath_fragment {
+    name: "com.android.mediaprovider-bootclasspath-fragment",
+    contents: ["framework-mediaprovider"],
+    apex_available: ["com.android.mediaprovider"],
+}
diff --git a/apex/apex_manifest.json b/apex/apex_manifest.json
index 6d8da53..d87ce8d 100644
--- a/apex/apex_manifest.json
+++ b/apex/apex_manifest.json
@@ -1,4 +1,4 @@
 {
   "name": "com.android.mediaprovider",
-  "version": 309999900
+  "version": 309999910
 }
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index a482e01..833e9c6 100755
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -749,13 +749,8 @@
     ATRACE_CALL();
     struct fuse* fuse = get_fuse(req);
 
-    if (mode) {
-        fuse_reply_err(req, EOPNOTSUPP);
-        return;
-    }
-
     handle* h = reinterpret_cast<handle*>(fi->fh);
-    auto err = posix_fallocate(h->fd, offset, length);
+    auto err = fallocate(h->fd, mode, offset, length);
     fuse_reply_err(req, err);
 }
 
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index f853150..7bcdf62 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -26,7 +26,6 @@
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ProviderInfo;
@@ -62,7 +61,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.playlist.Playlist;
 import com.android.providers.media.util.BackgroundThread;
 import com.android.providers.media.util.DatabaseUtils;
@@ -106,8 +104,6 @@
      */
     public static final String CURRENT_GENERATION_CLAUSE = "SELECT generation FROM local_metadata";
 
-    static final String PREF_DB_FROM_VERSION = "db_from_version";
-
     private static final int NOTIFY_BATCH_SIZE = 256;
 
     final Context mContext;
@@ -359,8 +355,6 @@
             updateDatabase(db, oldV, newV);
         } finally {
             mSchemaLock.writeLock().unlock();
-            SharedPreferences prefs = mContext.getSharedPreferences(TAG, Context.MODE_PRIVATE);
-            prefs.edit().putInt(PREF_DB_FROM_VERSION, oldV).commit();
         }
     }
 
@@ -1412,7 +1406,8 @@
 
     private static void updateAddRecording(SQLiteDatabase db, boolean internal) {
         db.execSQL("ALTER TABLE files ADD COLUMN is_recording INTEGER DEFAULT 0;");
-        updateRecordingForSUpdate(db, internal);
+        // We add the column is_recording, rescan all music files
+        db.execSQL("UPDATE files SET date_modified=0 WHERE is_music=1;");
     }
 
     private static void updateAddRedactedUriId(SQLiteDatabase db) {
@@ -1567,7 +1562,6 @@
                 UserHandle.myUserId()));
     }
 
-
     private static void recomputeDataValues(SQLiteDatabase db, boolean internal) {
         try (Cursor c = db.query("files", new String[] { FileColumns._ID, FileColumns.DATA },
                 null, null, null, null, null, null)) {
@@ -1619,13 +1613,6 @@
         }
     }
 
-    static void updateRecordingForSUpdate(SQLiteDatabase db, boolean internal) {
-        if (SdkLevel.isAtLeastS()) {
-            // Rescan all music files to update is_recording type
-            db.execSQL("UPDATE files SET date_modified=0 WHERE is_music=1;");
-        }
-    }
-
     static final int VERSION_J = 509;
     static final int VERSION_K = 700;
     static final int VERSION_L = 700;
diff --git a/src/com/android/providers/media/LocalCallingIdentity.java b/src/com/android/providers/media/LocalCallingIdentity.java
index d19627c..60b4e0e 100644
--- a/src/com/android/providers/media/LocalCallingIdentity.java
+++ b/src/com/android/providers/media/LocalCallingIdentity.java
@@ -63,6 +63,7 @@
 
 import com.android.modules.utils.build.SdkLevel;
 import com.android.providers.media.util.LongArray;
+import com.android.providers.media.util.UserCache;
 
 import java.util.Locale;
 
@@ -104,7 +105,8 @@
 
     private static final long UNKNOWN_ROW_ID = -1;
 
-    public static LocalCallingIdentity fromBinder(Context context, ContentProvider provider) {
+    public static LocalCallingIdentity fromBinder(Context context, ContentProvider provider,
+            UserCache userCache) {
         String callingPackage = provider.getCallingPackageUnchecked();
         int binderUid = Binder.getCallingUid();
         if (callingPackage == null) {
@@ -128,6 +130,12 @@
         } else {
             user = UserHandle.getUserHandleForUid(binderUid);
         }
+        if (!userCache.userSharesMediaWithParent(user)) {
+            // It's possible that we got a cross-profile intent from a regular work profile; in
+            // that case, the request was explicitly targeted at the media database of the owner
+            // user; reflect that here.
+            user = Process.myUserHandle();
+        }
         return new LocalCallingIdentity(context, Binder.getCallingPid(), binderUid,
                 user, callingPackage, callingAttributionTag);
     }
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index 69efcae..e76bde9 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -194,7 +194,7 @@
 import com.android.providers.media.DatabaseHelper.OnLegacyMigrationListener;
 import com.android.providers.media.fuse.ExternalStorageServiceImpl;
 import com.android.providers.media.fuse.FuseDaemon;
-import com.android.providers.media.metrics.StatsdPuller;
+import com.android.providers.media.metrics.PulledMetrics;
 import com.android.providers.media.playlist.Playlist;
 import com.android.providers.media.scan.MediaScanner;
 import com.android.providers.media.scan.ModernMediaScanner;
@@ -210,6 +210,7 @@
 import com.android.providers.media.util.MimeUtils;
 import com.android.providers.media.util.PermissionUtils;
 import com.android.providers.media.util.SQLiteQueryBuilder;
+import com.android.providers.media.util.UserCache;
 import com.android.providers.media.util.XmpInterface;
 
 import com.google.common.hash.Hashing;
@@ -416,9 +417,21 @@
         return mVolumeCache.getVolumeId(file);
     }
 
-    public @NonNull Collection<File> getVolumeScanPaths(String volumeName)
+    private @NonNull Collection<File> getAllowedVolumePaths(String volumeName)
             throws FileNotFoundException {
-        return mVolumeCache.getVolumeScanPaths(volumeName, mCallingIdentity.get().getUser());
+        // This method is used to verify whether a path belongs to a certain volume name;
+        // we can't always use the calling user's identity here to determine exactly which
+        // volume is meant, because the MediaScanner may scan paths belonging to another user,
+        // eg a clone user.
+        // So, for volumes like external_primary, just return allowed paths for all users.
+        List<UserHandle> users = mUserCache.getUsersCached();
+        ArrayList<File> allowedPaths = new ArrayList<>();
+        for (UserHandle user : users) {
+            Collection<File> volumeScanPaths = mVolumeCache.getVolumeScanPaths(volumeName, user);
+            allowedPaths.addAll(volumeScanPaths);
+        }
+
+        return allowedPaths;
     }
 
     /**
@@ -442,6 +455,7 @@
     private DevicePolicyManager mDevicePolicyManager;
     private UserManager mUserManager;
 
+    private UserCache mUserCache;
     private VolumeCache mVolumeCache;
 
     private int mExternalStorageAuthorityAppId;
@@ -519,7 +533,7 @@
                     final LocalCallingIdentity cached = mCachedCallingIdentity
                             .get(Binder.getCallingUid());
                     return (cached != null) ? cached
-                            : LocalCallingIdentity.fromBinder(getContext(), this);
+                            : LocalCallingIdentity.fromBinder(getContext(), this, mUserCache);
                 }
             });
 
@@ -896,6 +910,8 @@
     public boolean onCreate() {
         final Context context = getContext();
 
+        mUserCache = new UserCache(context);
+
         // Shift call statistics back to the original caller
         Binder.setProxyTransactListener(mTransactListener);
 
@@ -904,7 +920,7 @@
         mPackageManager = context.getPackageManager();
         mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
         mUserManager = context.getSystemService(UserManager.class);
-        mVolumeCache = new VolumeCache(context);
+        mVolumeCache = new VolumeCache(context, mUserCache);
 
         // Reasonable thumbnail size is half of the smallest screen edge width
         final DisplayMetrics metrics = context.getResources().getDisplayMetrics();
@@ -994,7 +1010,7 @@
             mExternalStorageAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
         }
 
-        StatsdPuller.initialize(context);
+        PulledMetrics.initialize(context);
         return true;
     }
 
@@ -1296,28 +1312,14 @@
             UserHandle user1 = UserHandle.of(userId1);
             UserHandle user2 = UserHandle.of(userId2);
 
-            // Need to create system package context here as the clone profile user doesn't run
-            // a MediaProvider instance of its own, and hence we can't use
-            // createContextAsUser which uses the current MediaProvider module package.
-            final Context userContext1 = getContext().createPackageContextAsUser("system", 0,
-                    user1);
-            boolean isMediaSharedWithParent1 = userContext1.getSystemService(
-                    UserManager.class).isMediaSharedWithParent();
-            final Context userContext2 = getContext().createPackageContextAsUser("system", 0,
-                    user2);
-            boolean isMediaSharedWithParent2 = userContext2.getSystemService(
-                    UserManager.class).isMediaSharedWithParent();
-
-            // Clone profiles share media with the parent user
-            if (SdkLevel.isAtLeastS() && (isMediaSharedWithParent1
-                    || isMediaSharedWithParent2)) {
-                return mUserManager.isSameProfileGroup(user1, user2);
+            if (SdkLevel.isAtLeastS() && (mUserCache.userSharesMediaWithParent(user1)
+                    || mUserCache.userSharesMediaWithParent(user2))) {
+                return true;
             }
             Method isAppCloneUserPair = StorageManager.class.getMethod("isAppCloneUserPair",
                     int.class, int.class);
             return (Boolean) isAppCloneUserPair.invoke(mStorageManager, userId1, userId2);
-        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException |
-                NameNotFoundException e) {
+        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
             Log.w(TAG, "isAppCloneUserPair failed. Users: " + userId1 + " and " + userId2);
             return false;
         }
@@ -1924,6 +1926,7 @@
     public String[] getFilesInDirectoryForFuse(String path, int uid) {
         final LocalCallingIdentity token =
                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+        PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
 
         try {
             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
@@ -2516,6 +2519,7 @@
         final String errorMessage = "Rename " + oldPath + " to " + newPath + " failed. ";
         final LocalCallingIdentity token =
                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+        PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), oldPath);
 
         try {
             if (isPrivatePackagePathNotAccessibleByCaller(oldPath)
@@ -2699,6 +2703,8 @@
 
     private Cursor queryInternal(Uri uri, String[] projection, Bundle queryArgs,
             CancellationSignal signal, boolean forSelf) throws FallbackException {
+        final String volumeName = getVolumeName(uri);
+        PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
         queryArgs = (queryArgs != null) ? queryArgs : new Bundle();
 
         // INCLUDED_DEFAULT_DIRECTORIES extra should only be set inside MediaProvider.
@@ -2718,7 +2724,6 @@
 
         uri = safeUncanonicalize(uri);
 
-        final String volumeName = getVolumeName(uri);
         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
         final boolean allowHidden = isCallingPackageAllowedHidden();
         final int table = matchUri(uri, allowHidden);
@@ -3054,6 +3059,7 @@
                             Environment.DIRECTORY_MUSIC,
                             Environment.DIRECTORY_NOTIFICATIONS,
                             Environment.DIRECTORY_PODCASTS,
+                            FileUtils.DIRECTORY_RECORDINGS,
                             Environment.DIRECTORY_RINGTONES);
                 }
                 break;
@@ -3202,6 +3208,7 @@
         mimeType = values.getAsString(MediaColumns.MIME_TYPE);
         // Quick check MIME type against table
         if (mimeType != null) {
+            PulledMetrics.logMimeTypeAccess(getCallingUidOrSelf(), mimeType);
             final int actualMediaType = MimeUtils.resolveMediaType(mimeType);
             if (defaultMediaType == FileColumns.MEDIA_TYPE_NONE) {
                 // Give callers an opportunity to work with playlists and
@@ -3485,7 +3492,7 @@
         final String volumeName = resolveVolumeName(uri);
         try {
             // Quick check that the requested path actually lives on volume
-            final Collection<File> allowed = getVolumeScanPaths(volumeName);
+            final Collection<File> allowed = getAllowedVolumePaths(volumeName);
             final File actual = new File(values.getAsString(MediaColumns.DATA))
                     .getCanonicalFile();
             if (!FileUtils.contains(allowed, actual)) {
@@ -4061,6 +4068,9 @@
 
     private @Nullable Uri insertInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
             @Nullable Bundle extras) throws FallbackException {
+        final String originalVolumeName = getVolumeName(uri);
+        PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), originalVolumeName);
+
         extras = (extras != null) ? extras : new Bundle();
         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
         extras.remove(QUERY_ARG_REDACTED_URI);
@@ -4072,7 +4082,6 @@
         final int match = matchUri(uri, allowHidden);
 
         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
-        final String originalVolumeName = getVolumeName(uri);
         final String resolvedVolumeName = resolveVolumeName(uri);
 
         // handle MEDIA_SCANNER before calling getDatabaseForUri()
@@ -5155,6 +5164,9 @@
 
     private int deleteInternal(@NonNull Uri uri, @Nullable Bundle extras)
             throws FallbackException {
+        final String volumeName = getVolumeName(uri);
+        PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
+
         extras = (extras != null) ? extras : new Bundle();
         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
         extras.remove(QUERY_ARG_REDACTED_URI);
@@ -5199,7 +5211,6 @@
 
         int count = 0;
 
-        final String volumeName = getVolumeName(uri);
         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
 
         // handle MEDIA_SCANNER before calling getDatabaseForUri()
@@ -5280,6 +5291,8 @@
                             final int isDownload = c.getInt(3);
                             final String mimeType = c.getString(4);
 
+                            // TODO(b/188782594) Consider logging mime type access on delete too.
+
                             // Forget that caller is owner of this item
                             mCallingIdentity.get().setOwned(id, false);
 
@@ -6150,6 +6163,9 @@
 
     private int updateInternal(@NonNull Uri uri, @Nullable ContentValues initialValues,
             @Nullable Bundle extras) throws FallbackException {
+        final String volumeName = getVolumeName(uri);
+        PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
+
         extras = (extras != null) ? extras : new Bundle();
         // REDACTED_URI_BUNDLE_KEY extra should only be set inside MediaProvider.
         extras.remove(QUERY_ARG_REDACTED_URI);
@@ -6185,7 +6201,6 @@
 
         int count;
 
-        final String volumeName = getVolumeName(uri);
         final int targetSdkVersion = getCallingPackageTargetSdkVersion();
         final boolean allowHidden = isCallingPackageAllowedHidden();
         final int match = matchUri(uri, allowHidden);
@@ -7071,31 +7086,37 @@
         final String volumeName = getVolumeName(uri);
 
         // Handle some legacy cases where we need to redirect thumbnails
-        switch (match) {
-            case AUDIO_ALBUMART_ID: {
-                final long albumId = Long.parseLong(uri.getPathSegments().get(3));
-                final Uri targetUri = ContentUris
-                        .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId);
-                return ensureThumbnail(targetUri, signal);
+        try {
+            switch (match) {
+                case AUDIO_ALBUMART_ID: {
+                    final long albumId = Long.parseLong(uri.getPathSegments().get(3));
+                    final Uri targetUri = ContentUris
+                            .withAppendedId(Audio.Albums.getContentUri(volumeName), albumId);
+                    return ensureThumbnail(targetUri, signal);
+                }
+                case AUDIO_ALBUMART_FILE_ID: {
+                    final long audioId = Long.parseLong(uri.getPathSegments().get(3));
+                    final Uri targetUri = ContentUris
+                            .withAppendedId(Audio.Media.getContentUri(volumeName), audioId);
+                    return ensureThumbnail(targetUri, signal);
+                }
+                case VIDEO_MEDIA_ID_THUMBNAIL: {
+                    final long videoId = Long.parseLong(uri.getPathSegments().get(3));
+                    final Uri targetUri = ContentUris
+                            .withAppendedId(Video.Media.getContentUri(volumeName), videoId);
+                    return ensureThumbnail(targetUri, signal);
+                }
+                case IMAGES_MEDIA_ID_THUMBNAIL: {
+                    final long imageId = Long.parseLong(uri.getPathSegments().get(3));
+                    final Uri targetUri = ContentUris
+                            .withAppendedId(Images.Media.getContentUri(volumeName), imageId);
+                    return ensureThumbnail(targetUri, signal);
+                }
             }
-            case AUDIO_ALBUMART_FILE_ID: {
-                final long audioId = Long.parseLong(uri.getPathSegments().get(3));
-                final Uri targetUri = ContentUris
-                        .withAppendedId(Audio.Media.getContentUri(volumeName), audioId);
-                return ensureThumbnail(targetUri, signal);
-            }
-            case VIDEO_MEDIA_ID_THUMBNAIL: {
-                final long videoId = Long.parseLong(uri.getPathSegments().get(3));
-                final Uri targetUri = ContentUris
-                        .withAppendedId(Video.Media.getContentUri(volumeName), videoId);
-                return ensureThumbnail(targetUri, signal);
-            }
-            case IMAGES_MEDIA_ID_THUMBNAIL: {
-                final long imageId = Long.parseLong(uri.getPathSegments().get(3));
-                final Uri targetUri = ContentUris
-                        .withAppendedId(Images.Media.getContentUri(volumeName), imageId);
-                return ensureThumbnail(targetUri, signal);
-            }
+        } finally {
+            // We have to log separately here because openFileAndEnforcePathPermissionsHelper calls
+            // a public MediaProvider API and so logs the access there.
+            PulledMetrics.logVolumeAccessViaMediaProvider(getCallingUidOrSelf(), volumeName);
         }
 
         return openFileAndEnforcePathPermissionsHelper(uri, match, mode, signal, opts);
@@ -8015,6 +8036,9 @@
             int transformsReason, boolean forWrite, boolean redact, boolean logTransformsMetrics) {
         final LocalCallingIdentity token =
                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+
+        PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
+
         boolean isSuccess = false;
 
         final int originalUid = getBinderUidForFuse(uid, tid);
@@ -8351,6 +8375,7 @@
     public int insertFileIfNecessaryForFuse(@NonNull String path, int uid) {
         final LocalCallingIdentity token =
                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+        PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
 
         try {
             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
@@ -8476,6 +8501,8 @@
     public int deleteFileForFuse(@NonNull String path, int uid) throws IOException {
         final LocalCallingIdentity token =
                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+        PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
+
         try {
             if (isPrivatePackagePathNotAccessibleByCaller(path)) {
                 Log.e(TAG, "Can't delete a file in another app's external directory!");
@@ -8538,6 +8565,7 @@
             @NonNull String path, int uid, boolean forCreate) {
         final LocalCallingIdentity token =
                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+        PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
 
         try {
             // App dirs are not indexed, so we don't create an entry for the file.
@@ -8591,6 +8619,7 @@
     public int isOpendirAllowedForFuse(@NonNull String path, int uid, boolean forWrite) {
         final LocalCallingIdentity token =
                 clearLocalCallingIdentity(getCachedCallingIdentityForFuse(uid));
+        PulledMetrics.logFileAccessViaFuse(getCallingUidOrSelf(), path);
         try {
             if ("/storage/emulated".equals(path)) {
                 return OsConstants.EPERM;
@@ -9213,8 +9242,8 @@
         final String volumeName = resolveVolumeName(uri);
         synchronized (mAttachedVolumes) {
             boolean volumeAttached = false;
+            UserHandle user = mCallingIdentity.get().getUser();
             for (MediaVolume vol : mAttachedVolumes) {
-                UserHandle user = mCallingIdentity.get().getUser();
                 if (vol.getName().equals(volumeName) && vol.isVisibleToUser(user)) {
                     volumeAttached = true;
                     break;
@@ -9632,6 +9661,10 @@
         return mCallingIdentity.get().hasPermission(PERMISSION_IS_SYSTEM_GALLERY);
     }
 
+    private int getCallingUidOrSelf() {
+        return mCallingIdentity.get().uid;
+    }
+
     @Deprecated
     private String getCallingPackageOrSelf() {
         return mCallingIdentity.get().getPackageName();
diff --git a/src/com/android/providers/media/MediaService.java b/src/com/android/providers/media/MediaService.java
index 5e3e10f..bc7330a 100644
--- a/src/com/android/providers/media/MediaService.java
+++ b/src/com/android/providers/media/MediaService.java
@@ -29,6 +29,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Trace;
+import android.os.UserHandle;
 import android.os.storage.StorageVolume;
 import android.provider.MediaStore;
 import android.util.Log;
@@ -43,6 +44,21 @@
 public class MediaService extends JobIntentService {
     private static final int JOB_ID = -300;
 
+    private static final String ACTION_SCAN_VOLUME
+            = "com.android.providers.media.action.SCAN_VOLUME";
+
+    private static final String EXTRA_MEDIAVOLUME = "MediaVolume";
+
+    private static final String EXTRA_SCAN_REASON = "scan_reason";
+
+
+    public static void queueVolumeScan(Context context, MediaVolume volume, int reason) {
+        Intent intent = new Intent(ACTION_SCAN_VOLUME);
+        intent.putExtra(EXTRA_MEDIAVOLUME, volume) ;
+        intent.putExtra(EXTRA_SCAN_REASON, reason);
+        enqueueWork(context, intent);
+    }
+
     public static void enqueueWork(Context context, Intent work) {
         enqueueWork(context, MediaService.class, JOB_ID, work);
     }
@@ -69,8 +85,10 @@
                     onScanFile(this, intent.getData());
                     break;
                 }
-                case Intent.ACTION_MEDIA_MOUNTED: {
-                    onScanVolume(this, intent, REASON_MOUNTED);
+                case ACTION_SCAN_VOLUME: {
+                    final MediaVolume volume = intent.getParcelableExtra(EXTRA_MEDIAVOLUME);
+                    int reason = intent.getIntExtra(EXTRA_SCAN_REASON, REASON_DEMAND);
+                    onScanVolume(this, volume, reason);
                     break;
                 }
                 default: {
@@ -116,6 +134,11 @@
     public static void onScanVolume(Context context, MediaVolume volume, int reason)
             throws IOException {
         final String volumeName = volume.getName();
+        UserHandle owner = volume.getUser();
+        if (owner == null) {
+            // Can happen for the internal volume
+            owner = context.getUser();
+        }
         // If we're about to scan any external storage, scan internal first
         // to ensure that we have ringtones ready to roll before a possibly very
         // long external storage scan
@@ -129,7 +152,7 @@
         // in the situation where a volume is ejected mid-scan
         final Uri broadcastUri;
         if (!MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
-            broadcastUri = Uri.fromFile(FileUtils.getVolumePath(context, volumeName));
+            broadcastUri = Uri.fromFile(volume.getPath());
         } else {
             broadcastUri = null;
         }
@@ -146,20 +169,24 @@
             Uri scanUri = resolver.insert(MediaStore.getMediaScannerUri(), values);
 
             if (broadcastUri != null) {
-                context.sendBroadcast(
-                        new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, broadcastUri));
+                context.sendBroadcastAsUser(
+                        new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, broadcastUri), owner);
             }
 
-            for (File dir : FileUtils.getVolumeScanPaths(context, volumeName)) {
-                provider.scanDirectory(dir, reason);
+            if (MediaStore.VOLUME_INTERNAL.equals(volumeName)) {
+                for (File dir : FileUtils.getVolumeScanPaths(context, volumeName)) {
+                    provider.scanDirectory(dir, reason);
+                }
+            } else {
+                provider.scanDirectory(volume.getPath(), reason);
             }
 
             resolver.delete(scanUri, null, null);
 
         } finally {
             if (broadcastUri != null) {
-                context.sendBroadcast(
-                        new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, broadcastUri));
+                context.sendBroadcastAsUser(
+                        new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, broadcastUri), owner);
             }
         }
     }
diff --git a/src/com/android/providers/media/MediaUpgradeReceiver.java b/src/com/android/providers/media/MediaUpgradeReceiver.java
index 320e1c2..abb326a 100644
--- a/src/com/android/providers/media/MediaUpgradeReceiver.java
+++ b/src/com/android/providers/media/MediaUpgradeReceiver.java
@@ -24,7 +24,6 @@
 import android.util.Log;
 
 import com.android.providers.media.util.ForegroundThread;
-import com.android.providers.media.util.Logging;
 import com.android.providers.media.util.Metrics;
 
 import java.io.File;
@@ -43,8 +42,6 @@
     static final String TAG = "MediaUpgradeReceiver";
     static final String PREF_DB_VERSION = "db_version";
 
-    private static final int UPDATE_RECORDING_VERSION = 1205;
-
     @Override
     public void onReceive(Context context, Intent intent) {
         // We are now running with the system up, but no apps started,
@@ -67,10 +64,6 @@
         prefs.edit().putInt(PREF_DB_VERSION, dbVersion).commit();
 
         try {
-            // Lookup the last from_version when upgrade database
-            prefs = context.getSharedPreferences(Logging.TAG, Context.MODE_PRIVATE);
-            final int fromVersion = prefs.getInt(DatabaseHelper.PREF_DB_FROM_VERSION, 0);
-
             File dbDir = context.getDatabasePath("foo").getParentFile();
             String[] files = dbDir.list();
             if (files == null) return;
@@ -86,13 +79,7 @@
                                 MediaProvider.MIGRATION_LISTENER, null);
                         helper.runWithTransaction((db) -> {
                             // Perform just enough to force database upgrade
-                            final int version = db.getVersion();
-                            // update the is_recording column for R OS upgraded to S OS
-                            if (fromVersion >= UPDATE_RECORDING_VERSION
-                                    && prefVersion < DatabaseHelper.VERSION_S) {
-                                helper.updateRecordingForSUpdate(db, helper.mInternal);
-                            }
-                            return version;
+                            return db.getVersion();
                         });
                         helper.close();
                     } catch (Throwable t) {
diff --git a/src/com/android/providers/media/MediaVolume.java b/src/com/android/providers/media/MediaVolume.java
index 577a040..461f67d 100644
--- a/src/com/android/providers/media/MediaVolume.java
+++ b/src/com/android/providers/media/MediaVolume.java
@@ -16,6 +16,8 @@
 
 package com.android.providers.media;
 
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.os.UserHandle;
 import android.os.storage.StorageVolume;
 import android.provider.MediaStore;
@@ -37,7 +39,7 @@
  * In addition to that, we keep the path and ID of the volume cached in here as well
  * for easy access.
  */
-public final class MediaVolume {
+public final class MediaVolume implements Parcelable {
     /**
      * Name of the volume.
      */
@@ -81,6 +83,13 @@
         this.mId = id;
     }
 
+    private MediaVolume (Parcel in) {
+        this.mName = in.readString();
+        this.mUser = in.readParcelable(null);
+        this.mPath  = new File(in.readString());
+        this.mId = in.readString();
+    }
+
     @Override
     public boolean equals(Object obj) {
         if (this == obj) return true;
@@ -115,4 +124,30 @@
 
         return new MediaVolume(name, null, null, null);
     }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mName);
+        dest.writeParcelable(mUser, flags);
+        dest.writeString(mPath.toString());
+        dest.writeString(mId);
+    }
+
+    public static final @android.annotation.NonNull Creator<MediaVolume> CREATOR
+            = new Creator<MediaVolume>() {
+        @Override
+        public MediaVolume createFromParcel(Parcel in) {
+            return new MediaVolume(in);
+        }
+
+        @Override
+        public MediaVolume[] newArray(int size) {
+            return new MediaVolume[size];
+        }
+    };
 }
diff --git a/src/com/android/providers/media/TranscodeHelper.java b/src/com/android/providers/media/TranscodeHelper.java
index e1ffbca..9a74ddc 100644
--- a/src/com/android/providers/media/TranscodeHelper.java
+++ b/src/com/android/providers/media/TranscodeHelper.java
@@ -437,8 +437,8 @@
         CountDownLatch latch = null;
         long startTime = SystemClock.elapsedRealtime();
         boolean result = false;
-        int errorCode = TranscodingSession.ERROR_NONE;
-        int failureReason = TRANSCODING_DATA__FAILURE_CAUSE__CAUSE_UNKNOWN;
+        int errorCode = TranscodingSession.ERROR_SERVICE_DIED;
+        int failureReason = TRANSCODING_DATA__FAILURE_CAUSE__TRANSCODING_SERVICE_ERROR;
 
         try {
             synchronized (mLock) {
@@ -481,6 +481,16 @@
                 transcodingSession.cancel();
             }
         } finally {
+            if (storageSession == null) {
+                Log.w(TAG, "Failed to create a StorageTranscodingSession");
+                // We were unable to even queue the request. Which means the media service is
+                // in a very bad state
+                reportTranscodingResult(uid, result, errorCode, failureReason,
+                        SystemClock.elapsedRealtime() - startTime, reason,
+                        src, dst, false /* hasAnr */);
+                return false;
+            }
+
             storageSession.notifyFinished(failureReason, errorCode);
             if (errorCode == TranscodingSession.ERROR_DROPPED_BY_SERVICE) {
                 // If the transcoding service drops a request for a uid the uid will be denied
diff --git a/src/com/android/providers/media/VolumeCache.java b/src/com/android/providers/media/VolumeCache.java
index d94ddfa..cbcd230 100644
--- a/src/com/android/providers/media/VolumeCache.java
+++ b/src/com/android/providers/media/VolumeCache.java
@@ -19,7 +19,6 @@
 import static com.android.providers.media.util.Logging.TAG;
 
 import android.content.Context;
-import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -35,6 +34,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.providers.media.util.FileUtils;
+import com.android.providers.media.util.UserCache;
 
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -54,6 +54,7 @@
     private final Object mLock = new Object();
 
     private final UserManager mUserManager;
+    private final UserCache mUserCache;
 
     @GuardedBy("mLock")
     private final ArrayList<MediaVolume> mExternalVolumes = new ArrayList<>();
@@ -64,12 +65,10 @@
     @GuardedBy("mLock")
     private Collection<File> mCachedInternalScanPaths;
 
-    @GuardedBy("mLock")
-    private final LongSparseArray<Context> mUserContexts = new LongSparseArray<>();
-
-    public VolumeCache(Context context) {
+    public VolumeCache(Context context, UserCache userCache) {
         mContext = context;
         mUserManager = context.getSystemService(UserManager.class);
+        mUserCache = userCache;
     }
 
     public @NonNull List<MediaVolume> getExternalVolumes() {
@@ -112,7 +111,7 @@
                 // Try again by using FileUtils below
             }
 
-            final Context userContext = getContextForUser(user);
+            final Context userContext = mUserCache.getContextForUser(user);
             return FileUtils.getVolumePath(userContext, volumeName);
         }
     }
@@ -134,7 +133,7 @@
             }
 
             // Nothing found above; let's ask directly
-            final Context userContext = getContextForUser(user);
+            final Context userContext = mUserCache.getContextForUser(user);
             final Collection<File> res = FileUtils.getVolumeScanPaths(userContext, volumeName);
 
             return res;
@@ -167,22 +166,6 @@
         return volume.getId();
     }
 
-    private @NonNull Context getContextForUser(UserHandle user) {
-        synchronized (mLock) {
-            Context userContext = mUserContexts.get(user.getIdentifier());
-            if (userContext != null) {
-                return userContext;
-            }
-            try {
-                userContext = mContext.createPackageContextAsUser("system", 0, user);
-                mUserContexts.put(user.getIdentifier(), userContext);
-                return userContext;
-            } catch (PackageManager.NameNotFoundException e) {
-                throw new RuntimeException("Failed to create context for user " + user, e);
-            }
-        }
-    }
-
     @GuardedBy("mLock")
     private void updateExternalVolumesForUserLocked(Context userContext) {
         final StorageManager sm = userContext.getSystemService(StorageManager.class);
@@ -210,17 +193,10 @@
                 Log.wtf(TAG, "Failed to update volume " + MediaStore.VOLUME_INTERNAL,e );
             }
             mExternalVolumes.clear();
-            for (UserHandle profile : mUserManager.getEnabledProfiles()) {
-                if (profile.equals(mContext.getUser())) {
-                    // Volumes of the user id that MediaProvider runs as
-                    updateExternalVolumesForUserLocked(mContext);
-                } else {
-                    Context userContext = getContextForUser(profile);
-                    if (userContext.getSystemService(UserManager.class).isMediaSharedWithParent()) {
-                        // This profile shares media with its parent - add its volumes, too
-                        updateExternalVolumesForUserLocked(userContext);
-                    }
-                }
+            List<UserHandle> users = mUserCache.updateAndGetUsers();
+            for (UserHandle user : users) {
+                Context userContext = mUserCache.getContextForUser(user);
+                updateExternalVolumesForUserLocked(userContext);
             }
         }
     }
diff --git a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
index 612889d..1f8f3cd 100644
--- a/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
+++ b/src/com/android/providers/media/fuse/ExternalStorageServiceImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.providers.media.fuse;
 
+import static com.android.providers.media.scan.MediaScanner.REASON_MOUNTED;
+
 import android.annotation.BytesLong;
 import android.content.ContentProviderClient;
 import android.os.Environment;
@@ -30,6 +32,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.providers.media.MediaProvider;
+import com.android.providers.media.MediaService;
 import com.android.providers.media.MediaVolume;
 
 import java.io.File;
@@ -83,8 +86,9 @@
 
         switch(vol.getState()) {
             case Environment.MEDIA_MOUNTED:
-                mediaProvider.attachVolume(MediaVolume.fromStorageVolume(vol),
-                        /* validate */ false);
+                MediaVolume volume = MediaVolume.fromStorageVolume(vol);
+                mediaProvider.attachVolume(volume, /* validate */ false);
+                MediaService.queueVolumeScan(mediaProvider.getContext(), volume, REASON_MOUNTED);
                 break;
             case Environment.MEDIA_UNMOUNTED:
             case Environment.MEDIA_EJECTING:
diff --git a/src/com/android/providers/media/metrics/PulledMetrics.java b/src/com/android/providers/media/metrics/PulledMetrics.java
new file mode 100644
index 0000000..f9c5fac
--- /dev/null
+++ b/src/com/android/providers/media/metrics/PulledMetrics.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2021 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.metrics;
+
+import static com.android.providers.media.MediaProviderStatsLog.GENERAL_EXTERNAL_STORAGE_ACCESS_STATS;
+import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA;
+
+import android.app.StatsManager;
+import android.content.Context;
+import android.util.Log;
+import android.util.StatsEvent;
+
+import androidx.annotation.NonNull;
+
+import com.android.providers.media.fuse.FuseDaemon;
+import com.android.providers.media.util.BackgroundThread;
+
+import java.util.List;
+
+/** A class to initialise and log metrics pulled by statsd. */
+public class PulledMetrics {
+    private static final String TAG = "PulledMetrics";
+
+    private static final StatsPullCallbackHandler STATS_PULL_CALLBACK_HANDLER =
+            new StatsPullCallbackHandler();
+
+    private static final StorageAccessMetrics storageAccessMetrics = new StorageAccessMetrics();
+
+    private static boolean isInitialized = false;
+
+    public static void initialize(Context context) {
+        if (isInitialized) {
+            return;
+        }
+
+        final StatsManager statsManager = context.getSystemService(StatsManager.class);
+        if (statsManager == null) {
+            Log.e(TAG, "Error retrieving StatsManager. Cannot initialize PulledMetrics.");
+        } else {
+            Log.d(TAG, "Registering callback with StatsManager");
+
+            try {
+                // use the same callback handler for registering for all the tags.
+                statsManager.setPullAtomCallback(TRANSCODING_DATA, null /* metadata */,
+                        BackgroundThread.getExecutor(),
+                        STATS_PULL_CALLBACK_HANDLER);
+                statsManager.setPullAtomCallback(
+                        GENERAL_EXTERNAL_STORAGE_ACCESS_STATS,
+                        /*metadata*/null,
+                        BackgroundThread.getExecutor(),
+                        STATS_PULL_CALLBACK_HANDLER);
+                isInitialized = true;
+            } catch (NullPointerException e) {
+                Log.w(TAG, "Pulled metrics not supported. Could not register.", e);
+            }
+        }
+    }
+
+    // Storage Access Metrics log functions
+
+    /**
+     * Logs the mime type that was accessed by the given {@code uid}.
+     * Does nothing if the stats puller is not initialized.
+     */
+    public static void logMimeTypeAccess(int uid, @NonNull String mimeType) {
+        if (!isInitialized) {
+            return;
+        }
+
+        storageAccessMetrics.logMimeType(uid, mimeType);
+    }
+
+    /**
+     * Logs the storage access and attributes it to the given {@code uid}.
+     *
+     * <p>Should only be called from a FUSE thread.
+     */
+    public static void logFileAccessViaFuse(int uid, @NonNull String file) {
+        if (!isInitialized) {
+            return;
+        }
+
+        storageAccessMetrics.logAccessViaFuse(uid, file);
+    }
+
+    /**
+     * Logs the storage access and attributes it to the given {@code uid}.
+     *
+     * <p>This is a no-op if it's called on a FUSE thread.
+     */
+    public static void logVolumeAccessViaMediaProvider(int uid, @NonNull String volumeName) {
+        if (!isInitialized) {
+            return;
+        }
+
+        // We don't log if it's a FUSE thread because logAccessViaFuse should handle that.
+        if (FuseDaemon.native_is_fuse_thread()) {
+            return;
+        }
+        storageAccessMetrics.logAccessViaMediaProvider(uid, volumeName);
+    }
+
+    private static class StatsPullCallbackHandler implements StatsManager.StatsPullAtomCallback {
+        @Override
+        public int onPullAtom(int atomTag, List<StatsEvent> data) {
+            // handle the tags appropriately.
+            List<StatsEvent> events = pullEvents(atomTag);
+            if (events == null) {
+                return StatsManager.PULL_SKIP;
+            }
+
+            data.addAll(events);
+            return StatsManager.PULL_SUCCESS;
+        }
+
+        private List<StatsEvent> pullEvents(int atomTag) {
+            switch (atomTag) {
+                case TRANSCODING_DATA:
+                    return TranscodeMetrics.pullStatsEvents();
+                case GENERAL_EXTERNAL_STORAGE_ACCESS_STATS:
+                    return storageAccessMetrics.pullStatsEvents();
+                default:
+                    return null;
+            }
+        }
+    }
+}
diff --git a/src/com/android/providers/media/metrics/StatsdPuller.java b/src/com/android/providers/media/metrics/StatsdPuller.java
deleted file mode 100644
index 224ff34..0000000
--- a/src/com/android/providers/media/metrics/StatsdPuller.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2021 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.metrics;
-
-import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA;
-
-import android.app.StatsManager;
-import android.content.Context;
-import android.util.Log;
-import android.util.StatsEvent;
-
-import com.android.providers.media.util.BackgroundThread;
-
-import java.util.List;
-
-/**
- * A "static" class providing the boilerplate for handling the pulling of metrics by statsd.
- * All individuals using this should accumulate their metrics per their policies.
- */
-public class StatsdPuller {
-    private static final String TAG = "StatsdPuller";
-
-    private static final StatsPullCallbackHandler STATS_PULL_CALLBACK_HANDLER =
-            new StatsPullCallbackHandler();
-
-    private static boolean isInitialized = false;
-
-    public static void initialize(Context context) {
-        if (isInitialized) {
-            return;
-        }
-
-        final StatsManager statsManager = context.getSystemService(StatsManager.class);
-        if (statsManager == null) {
-            Log.e(TAG, "Error retrieving StatsManager. Cannot initialize StatsdPuller.");
-        } else {
-            Log.i(TAG, "Registering callback with StatsManager");
-
-            try {
-                // use the same callback handler for registering for all the tags.
-                statsManager.setPullAtomCallback(TRANSCODING_DATA, null /* metadata */,
-                        BackgroundThread.getExecutor(),
-                        STATS_PULL_CALLBACK_HANDLER);
-                isInitialized = true;
-            } catch (NullPointerException e) {
-                Log.w(TAG, "Pulled metrics not supported. Could not register.", e);
-            }
-        }
-    }
-
-    public static boolean isInitialized() {
-        return isInitialized;
-    }
-
-    private static class StatsPullCallbackHandler implements StatsManager.StatsPullAtomCallback {
-        @Override
-        public int onPullAtom(int atomTag, List<StatsEvent> data) {
-            // handle the tags appropriately.
-            switch (atomTag) {
-                case TRANSCODING_DATA:
-                    return TranscodeMetrics.handleStatsEventDataRequest(atomTag,
-                            data);
-                default:
-                    throw new UnsupportedOperationException("Unknown atomTag = " + atomTag);
-            }
-        }
-    }
-}
diff --git a/src/com/android/providers/media/metrics/StorageAccessMetrics.java b/src/com/android/providers/media/metrics/StorageAccessMetrics.java
new file mode 100644
index 0000000..ce54e3f
--- /dev/null
+++ b/src/com/android/providers/media/metrics/StorageAccessMetrics.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2021 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.metrics;
+
+import static com.android.providers.media.MediaProviderStatsLog.GENERAL_EXTERNAL_STORAGE_ACCESS_STATS;
+
+import static java.util.stream.Collectors.toList;
+
+import android.os.Process;
+import android.os.SystemClock;
+import android.provider.MediaStore;
+import android.util.ArraySet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.StatsEvent;
+import android.util.proto.ProtoOutputStream;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.providers.media.MediaProviderStatsLog;
+import com.android.providers.media.util.FileUtils;
+import com.android.providers.media.util.MimeUtils;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Metrics for {@link MediaProviderStatsLog#GENERAL_EXTERNAL_STORAGE_ACCESS_STATS}. This class
+ * gathers stats separately for each UID that accesses external storage.
+ */
+class StorageAccessMetrics {
+
+    private static final String TAG = "StorageAccessMetrics";
+
+    @VisibleForTesting
+    static final int UID_SAMPLES_COUNT_LIMIT = 50;
+
+    private final int mMyUid = Process.myUid();
+
+    @GuardedBy("mLock")
+    private final SparseArray<PackageStorageAccessStats> mAccessStatsPerPackage =
+            new SparseArray<>();
+    @GuardedBy("mLock")
+    private long mStartTimeMillis = SystemClock.uptimeMillis();
+
+    private final Object mLock = new Object();
+
+
+    /**
+     * Logs the mime type that was accessed by the given {@code uid}.
+     */
+    void logMimeType(int uid, @NonNull String mimeType) {
+        if (mimeType == null) {
+            Log.w(TAG, "Attempted to log null mime type access");
+            return;
+        }
+
+        synchronized (mLock) {
+            getOrGeneratePackageStatsObjectLocked(uid).mMimeTypes.add(mimeType);
+        }
+    }
+
+    /**
+     * Logs the storage access and attributes it to the given {@code uid}.
+     *
+     * <p>Should only be called from a FUSE thread.
+     */
+    void logAccessViaFuse(int uid, @NonNull String file) {
+        // We don't log the access if it's MediaProvider accessing.
+        if (mMyUid == uid) {
+            return;
+        }
+
+        incrementFilePathAccesses(uid);
+        final String volumeName = MediaStore.getVolumeName(
+                FileUtils.getContentUriForPath(file));
+        logGeneralExternalStorageAccess(uid, volumeName);
+        logMimeTypeFromFile(uid, file);
+    }
+
+
+    /**
+     * Logs the storage access and attributes it to the given {@code uid}.
+     */
+    void logAccessViaMediaProvider(int uid, @NonNull String volumeName) {
+        // We also don't log the access if it's MediaProvider accessing.
+        if (mMyUid == uid) {
+            return;
+        }
+
+        logGeneralExternalStorageAccess(uid, volumeName);
+    }
+
+    /**
+     * Use this to log whenever a package accesses external storage via ContentResolver or FUSE.
+     * The given volume name helps us determine whether this was an access on primary or secondary
+     * storage.
+     */
+    private void logGeneralExternalStorageAccess(int uid, @NonNull String volumeName) {
+        switch (volumeName) {
+            case MediaStore.VOLUME_EXTERNAL:
+            case MediaStore.VOLUME_EXTERNAL_PRIMARY:
+                incrementTotalAccesses(uid);
+                break;
+            case MediaStore.VOLUME_INTERNAL:
+            case MediaStore.VOLUME_DEMO:
+            case MediaStore.MEDIA_SCANNER_VOLUME:
+                break;
+            default:
+                // Secondary external storage
+                incrementTotalAccesses(uid);
+                incrementSecondaryStorageAccesses(uid);
+        }
+    }
+
+    /**
+     * Logs that the mime type of the given {@param file} was accessed by the given {@param uid}.
+     */
+    private void logMimeTypeFromFile(int uid, @Nullable String file) {
+        logMimeType(uid, MimeUtils.resolveMimeType(new File(file)));
+    }
+
+    private void incrementTotalAccesses(int uid) {
+        synchronized (mLock) {
+            getOrGeneratePackageStatsObjectLocked(uid).mTotalAccesses += 1;
+        }
+    }
+
+    private void incrementFilePathAccesses(int uid) {
+        synchronized (mLock) {
+            getOrGeneratePackageStatsObjectLocked(uid).mFilePathAccesses += 1;
+        }
+    }
+
+    private void incrementSecondaryStorageAccesses(int uid) {
+        synchronized (mLock) {
+            getOrGeneratePackageStatsObjectLocked(uid).mSecondaryStorageAccesses += 1;
+        }
+    }
+
+    @GuardedBy("mLock")
+    private PackageStorageAccessStats getOrGeneratePackageStatsObjectLocked(int uid) {
+        PackageStorageAccessStats stats = mAccessStatsPerPackage.get(uid);
+        if (stats == null) {
+            stats = new PackageStorageAccessStats(uid);
+            mAccessStatsPerPackage.put(uid, stats);
+        }
+        return stats;
+    }
+
+    /**
+     * Returns the list of {@link StatsEvent} since latest reset, for a random subset of tracked
+     * uids if there are more than {@link #UID_SAMPLES_COUNT_LIMIT} in total. Returns {@code null}
+     * when the time since reset is non-positive.
+     */
+    @Nullable
+    List<StatsEvent> pullStatsEvents() {
+        synchronized (mLock) {
+            final long timeInterval = SystemClock.uptimeMillis() - mStartTimeMillis;
+            List<PackageStorageAccessStats> stats = getSampleStats();
+            resetStats();
+            return stats
+                    .stream()
+                    .map(s -> s.toNormalizedStats(timeInterval).toStatsEvent())
+                    .collect(toList());
+        }
+    }
+
+    @VisibleForTesting
+    List<PackageStorageAccessStats> getSampleStats() {
+        synchronized (mLock) {
+            List<PackageStorageAccessStats> result = new ArrayList<>();
+
+            List<Integer> sampledUids = new ArrayList<>();
+            for (int i = 0; i < mAccessStatsPerPackage.size(); i++) {
+                sampledUids.add(mAccessStatsPerPackage.keyAt(i));
+            }
+
+            if (sampledUids.size() > UID_SAMPLES_COUNT_LIMIT) {
+                Collections.shuffle(sampledUids);
+                sampledUids = sampledUids.subList(0, UID_SAMPLES_COUNT_LIMIT);
+            }
+            for (Integer uid : sampledUids) {
+                PackageStorageAccessStats stats = mAccessStatsPerPackage.get(uid);
+                result.add(stats);
+            }
+
+            return result;
+        }
+    }
+
+    private void resetStats() {
+        synchronized (mLock) {
+            mAccessStatsPerPackage.clear();
+            mStartTimeMillis = SystemClock.uptimeMillis();
+        }
+    }
+
+    @VisibleForTesting
+    static class PackageStorageAccessStats {
+        private final int mUid;
+        int mTotalAccesses = 0;
+        int mFilePathAccesses = 0;
+        int mSecondaryStorageAccesses = 0;
+
+        final ArraySet<String> mMimeTypes = new ArraySet<>();
+
+        PackageStorageAccessStats(int uid) {
+            this.mUid = uid;
+        }
+
+        PackageStorageAccessStats toNormalizedStats(long timeInterval) {
+            this.mTotalAccesses = normalizeAccessesPerDay(mTotalAccesses, timeInterval);
+            this.mFilePathAccesses = normalizeAccessesPerDay(mFilePathAccesses, timeInterval);
+            this.mSecondaryStorageAccesses =
+                    normalizeAccessesPerDay(mSecondaryStorageAccesses, timeInterval);
+            return this;
+        }
+
+        StatsEvent toStatsEvent() {
+            return StatsEvent.newBuilder()
+                    .setAtomId(GENERAL_EXTERNAL_STORAGE_ACCESS_STATS)
+                    .writeInt(mUid)
+                    .writeInt(mTotalAccesses)
+                    .writeInt(mFilePathAccesses)
+                    .writeInt(mSecondaryStorageAccesses)
+                    .writeByteArray(getMimeTypesAsProto().getBytes())
+                    .build();
+        }
+
+        private ProtoOutputStream getMimeTypesAsProto() {
+            ProtoOutputStream proto = new ProtoOutputStream();
+            for (int i = 0; i < mMimeTypes.size(); i++) {
+                String mime = mMimeTypes.valueAt(i);
+                proto.write(/*fieldId*/ProtoOutputStream.FIELD_TYPE_STRING
+                                | ProtoOutputStream.FIELD_COUNT_REPEATED
+                                | 1,
+                        mime);
+            }
+            return proto;
+        }
+
+        private static int normalizeAccessesPerDay(int value, long interval) {
+            if (interval <= 0) {
+                return -1;
+            }
+
+            double multiplier = Double.valueOf(TimeUnit.DAYS.toMillis(1)) / interval;
+            double normalizedValue = value * multiplier;
+            return Double.valueOf(normalizedValue).intValue();
+        }
+
+        @VisibleForTesting
+        int getUid() {
+            return mUid;
+        }
+    }
+}
diff --git a/src/com/android/providers/media/metrics/TranscodeMetrics.java b/src/com/android/providers/media/metrics/TranscodeMetrics.java
index c3dcc56..ec6a685 100644
--- a/src/com/android/providers/media/metrics/TranscodeMetrics.java
+++ b/src/com/android/providers/media/metrics/TranscodeMetrics.java
@@ -31,7 +31,7 @@
 /**
  * Stores metrics for transcode sessions to be shared with statsd.
  */
-public final class TranscodeMetrics {
+final class TranscodeMetrics {
     private static final List<TranscodingStatsData> TRANSCODING_STATS_DATA = new ArrayList<>();
 
     // PLEASE update these if there's a change in the proto message, per the limit set in
@@ -44,29 +44,26 @@
     // incoming data because of the hard limit on the size.
     private static int sTotalStatsDataCount = 0;
 
-    static int handleStatsEventDataRequest(int atomTag, List<StatsEvent> statsEvents) {
-        if (TRANSCODING_DATA != atomTag) {
-            return StatsManager.PULL_SKIP;
-        }
-
+    static List<StatsEvent> pullStatsEvents() {
         synchronized (TRANSCODING_STATS_DATA) {
             if (TRANSCODING_STATS_DATA.size() > STATS_DATA_SAMPLE_LIMIT) {
                 doRandomSampling();
             }
 
-            fillStatsEventDataList(atomTag, statsEvents);
+            List<StatsEvent> result = getStatsEvents();
             resetStatsData();
-            return StatsManager.PULL_SUCCESS;
+            return result;
         }
     }
 
-    private static void fillStatsEventDataList(int atomTag, List<StatsEvent> statsEvents) {
+    private static List<StatsEvent> getStatsEvents() {
         synchronized (TRANSCODING_STATS_DATA) {
+            List<StatsEvent> result = new ArrayList<>();
             StatsEvent event;
             int dataCountToFill = Math.min(TRANSCODING_STATS_DATA.size(), STATS_DATA_SAMPLE_LIMIT);
             for (int i = 0; i < dataCountToFill; ++i) {
                 TranscodingStatsData statsData = TRANSCODING_STATS_DATA.get(i);
-                event = StatsEvent.newBuilder().setAtomId(atomTag)
+                event = StatsEvent.newBuilder().setAtomId(TRANSCODING_DATA)
                         .writeString(statsData.mRequestorPackage)
                         .writeInt(statsData.mAccessType)
                         .writeLong(statsData.mFileSizeBytes)
@@ -76,8 +73,9 @@
                         .writeLong(statsData.mFrameRate)
                         .writeInt(statsData.mAccessReason).build();
 
-                statsEvents.add(event);
+                result.add(event);
             }
+            return result;
         }
     }
 
@@ -97,33 +95,17 @@
         }
     }
 
-    private static void resetStatsData() {
+    @VisibleForTesting
+    static void resetStatsData() {
         synchronized (TRANSCODING_STATS_DATA) {
             TRANSCODING_STATS_DATA.clear();
             sTotalStatsDataCount = 0;
         }
     }
 
-    /**
-     * Saves the statsd data that'd eventually be shared in the pull callback.
-     * The data is saved only if StatsdPuller is initialized.
-     * Everyone should always use this method for saving the data.
-     */
-    public static void saveStatsData(TranscodingStatsData transcodingStatsData) {
-        if (!StatsdPuller.isInitialized()) {
-            // no need to accumulate data if statsd is not going to ask for it.
-            return;
-        }
-
-        forceSaveStatsData(transcodingStatsData);
-    }
-
-    /**
-     * {@link StatsManager} does not register callback for Android unit test.  So, we have this
-     * method exposed for unit-tests so that they can save data.
-     */
+    /** Saves the statsd data that'd eventually be shared in the pull callback. */
     @VisibleForTesting
-    static void forceSaveStatsData(TranscodingStatsData transcodingStatsData) {
+    static void saveStatsData(TranscodingStatsData transcodingStatsData) {
         checkAndLimitStatsDataSizeAfterAddition(transcodingStatsData);
     }
 
@@ -171,7 +153,7 @@
     }
 
     /** This is the data to populate the proto shared to westworld. */
-    public static final class TranscodingStatsData {
+    static final class TranscodingStatsData {
         private final String mRequestorPackage;
         private final short mAccessType;
         private final long mFileSizeBytes;
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index 9a38945..8927bfa 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -1226,6 +1226,8 @@
         sAudioTypes.put(Environment.DIRECTORY_MUSIC, AudioColumns.IS_MUSIC);
         if (SdkLevel.isAtLeastS()) {
             sAudioTypes.put(Environment.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING);
+        } else {
+            sAudioTypes.put(FileUtils.DIRECTORY_RECORDINGS, AudioColumns.IS_RECORDING);
         }
     }
 
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index cf7c23a..a6311bd 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -964,6 +964,12 @@
     public static final Pattern PATTERN_OBB_OR_CHILD_PATH = Pattern.compile(
             "(?i)^/storage/[^/]+/(?:[0-9]+/)?Android/(?:obb)(/?.*)");
 
+    /**
+     * The recordings directory. This is used for R OS. For S OS or later,
+     * we use {@link Environment#DIRECTORY_RECORDINGS} directly.
+     */
+    public static final String DIRECTORY_RECORDINGS = "Recordings";
+
     @VisibleForTesting
     public static final String[] DEFAULT_FOLDER_NAMES;
     static {
@@ -995,6 +1001,7 @@
                     Environment.DIRECTORY_DCIM,
                     Environment.DIRECTORY_DOCUMENTS,
                     Environment.DIRECTORY_AUDIOBOOKS,
+                    DIRECTORY_RECORDINGS,
             };
         }
     }
@@ -1015,7 +1022,7 @@
      * Regex that matches user-ids under well-known storage paths.
      */
     private static final Pattern PATTERN_USER_ID = Pattern.compile(
-            "(?i)^/storage/emulated/([0-9]+)/");
+            "(?i)^/storage/emulated/([0-9]+)");
 
     private static final String CAMERA_RELATIVE_PATH =
             String.format("%s/%s/", Environment.DIRECTORY_DCIM, "Camera");
diff --git a/src/com/android/providers/media/util/UserCache.java b/src/com/android/providers/media/util/UserCache.java
new file mode 100644
index 0000000..276e3f8
--- /dev/null
+++ b/src/com/android/providers/media/util/UserCache.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2021 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.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.LongSparseArray;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * UserCache is a class that keeps track of all users that the current MediaProvider
+ * instance is responsible for. By default, it handles storage for the user it is running as,
+ * but as of Android API 31, it will also handle storage for profiles that share media
+ * with their parent - profiles for which @link{UserManager#isMediaSharedWithParent} is set.
+ *
+ * It also keeps a cache of user contexts, for improving these lookups.
+ *
+ * Note that we don't use the USER_ broadcasts for keeping this state up to date, because they
+ * aren't guaranteed to be received before the volume events for a user.
+ */
+public class UserCache {
+    final Object mLock = new Object();
+    final Context mContext;
+    final UserManager mUserManager;
+
+    @GuardedBy("mLock")
+    final LongSparseArray<Context> mUserContexts = new LongSparseArray<>();
+
+    @GuardedBy("mLock")
+    final ArrayList<UserHandle> mUsers = new ArrayList<>();
+
+    public UserCache(Context context) {
+        mContext = context;
+        mUserManager = context.getSystemService(UserManager.class);
+
+        update();
+    }
+
+    private void update() {
+        List<UserHandle> profiles = mUserManager.getEnabledProfiles();
+        synchronized (mLock) {
+            mUsers.clear();
+            // Add the user we're running as by default
+            mUsers.add(Process.myUserHandle());
+            // And find all profiles that share media with us
+            for (UserHandle profile : profiles) {
+                if (!profile.equals(mContext.getUser())) {
+                    // Check if it's a profile that shares media with us
+                    Context userContext = getContextForUser(profile);
+                    if (userContext.getSystemService(UserManager.class).isMediaSharedWithParent()) {
+                        mUsers.add(profile);
+                    }
+                }
+            }
+        }
+    }
+
+    public @NonNull List<UserHandle> updateAndGetUsers() {
+        update();
+        synchronized (mLock) {
+            return (List<UserHandle>) mUsers.clone();
+        }
+    }
+
+    public @NonNull List<UserHandle> getUsersCached() {
+        synchronized (mLock) {
+            return (List<UserHandle>) mUsers.clone();
+        }
+    }
+
+    public @NonNull Context getContextForUser(@NonNull UserHandle user) {
+        Context userContext;
+        synchronized (mLock) {
+            userContext = mUserContexts.get(user.getIdentifier());
+            if (userContext != null) {
+                return userContext;
+            }
+        }
+        try {
+            userContext = mContext.createPackageContextAsUser("system", 0, user);
+            synchronized (mLock) {
+                mUserContexts.put(user.getIdentifier(), userContext);
+            }
+            return userContext;
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new RuntimeException("Failed to create context for user " + user, e);
+        }
+    }
+
+    /**
+     *  Returns whether the passed in user shares media with its parent (or peer).
+     *  Note that the value returned here is based on cached data; it relies on
+     *  other callers to keep the user cache up-to-date.
+     *
+     * @param user user to check
+     * @return whether the user shares media with its parent
+     */
+    public boolean userSharesMediaWithParent(@NonNull UserHandle user) {
+        synchronized (mLock) {
+            return mUsers.contains(user);
+        }
+    }
+}
diff --git a/tests/src/com/android/providers/media/metrics/StorageAccessMetricsTest.java b/tests/src/com/android/providers/media/metrics/StorageAccessMetricsTest.java
new file mode 100644
index 0000000..5be69ff
--- /dev/null
+++ b/tests/src/com/android/providers/media/metrics/StorageAccessMetricsTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2021 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.metrics;
+
+import static com.android.providers.media.metrics.StorageAccessMetrics.PackageStorageAccessStats;
+import static com.android.providers.media.metrics.StorageAccessMetrics.UID_SAMPLES_COUNT_LIMIT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.provider.MediaStore;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+public class StorageAccessMetricsTest {
+
+    private static StorageAccessMetrics storageAccessMetrics;
+
+    @Before
+    public void setUp() {
+        storageAccessMetrics = new StorageAccessMetrics();
+    }
+
+    @Test
+    public void testLogMimeType() {
+        storageAccessMetrics.logMimeType(3, "my-mime-type");
+        storageAccessMetrics.logMimeType(3, null);
+        storageAccessMetrics.logMimeType(3, "my-mime-type-2");
+        storageAccessMetrics.logMimeType(3, "my-mime-type-2");
+        storageAccessMetrics.logMimeType(3, "my-mime-type-3");
+        storageAccessMetrics.logMimeType(3, "my-mime-type-2");
+        List<PackageStorageAccessStats> statsList =
+                storageAccessMetrics.getSampleStats();
+
+        assertThat(statsList).hasSize(1);
+
+        PackageStorageAccessStats stats = statsList.get(0);
+        assertThat(stats.getUid()).isEqualTo(3);
+        assertThat(stats.mTotalAccesses).isEqualTo(0);
+        assertThat(stats.mFilePathAccesses).isEqualTo(0);
+        assertThat(stats.mSecondaryStorageAccesses).isEqualTo(0);
+        assertThat(stats.mMimeTypes.stream().toArray())
+                .asList()
+                .containsExactly("my-mime-type", "my-mime-type-2", "my-mime-type-3");
+    }
+
+    @Test
+    public void testIncrementAccessViaFuse() {
+        storageAccessMetrics.logAccessViaFuse(3, "filename.txt");
+        List<PackageStorageAccessStats> statsList =
+                storageAccessMetrics.getSampleStats();
+
+        assertThat(statsList).hasSize(1);
+
+        PackageStorageAccessStats stats = statsList.get(0);
+        assertThat(stats.getUid()).isEqualTo(3);
+        assertThat(stats.mTotalAccesses).isEqualTo(0);
+        assertThat(stats.mFilePathAccesses).isEqualTo(1);
+        assertThat(stats.mSecondaryStorageAccesses).isEqualTo(0);
+        assertThat(stats.mMimeTypes.stream().toArray())
+                .asList()
+                .containsExactly("text/plain");
+    }
+
+    @Test
+    public void testIncrementAccessViaMediaProvider_externalVolumes() {
+        storageAccessMetrics.logAccessViaMediaProvider(3, MediaStore.VOLUME_EXTERNAL);
+        storageAccessMetrics.logAccessViaMediaProvider(
+                3, MediaStore.VOLUME_EXTERNAL_PRIMARY);
+        List<PackageStorageAccessStats> statsList =
+                storageAccessMetrics.getSampleStats();
+
+        assertThat(statsList).hasSize(1);
+
+        PackageStorageAccessStats stats = statsList.get(0);
+        assertThat(stats.getUid()).isEqualTo(3);
+        assertThat(stats.mTotalAccesses).isEqualTo(2);
+        assertThat(stats.mFilePathAccesses).isEqualTo(0);
+        assertThat(stats.mSecondaryStorageAccesses).isEqualTo(0);
+        assertThat(stats.mMimeTypes.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void testIncrementAccessViaMediaProvider_ignoredVolumes() {
+        storageAccessMetrics.logAccessViaMediaProvider(3, MediaStore.VOLUME_INTERNAL);
+        storageAccessMetrics.logAccessViaMediaProvider(3, MediaStore.VOLUME_DEMO);
+        storageAccessMetrics.logAccessViaMediaProvider(3, MediaStore.MEDIA_SCANNER_VOLUME);
+        List<PackageStorageAccessStats> statsList =
+                storageAccessMetrics.getSampleStats();
+
+        assertThat(statsList).isEmpty();
+    }
+
+    @Test
+    public void testIncrementAccessViaMediaProvider_secondaryVolumes() {
+        storageAccessMetrics.logAccessViaMediaProvider(3, "my-volume");
+        List<PackageStorageAccessStats> statsList =
+                storageAccessMetrics.getSampleStats();
+
+        assertThat(statsList).hasSize(1);
+
+        PackageStorageAccessStats stats = statsList.get(0);
+        assertThat(stats.getUid()).isEqualTo(3);
+        assertThat(stats.mTotalAccesses).isEqualTo(1);
+        assertThat(stats.mFilePathAccesses).isEqualTo(0);
+        assertThat(stats.mSecondaryStorageAccesses).isEqualTo(1);
+        assertThat(stats.mMimeTypes.size()).isEqualTo(0);
+    }
+
+    @Test
+    public void testUidCountGreaterThanLimit() {
+        int i = 0;
+        for (; i < UID_SAMPLES_COUNT_LIMIT; i++) {
+            storageAccessMetrics.logAccessViaFuse(i, "myfile.txt");
+        }
+        // Add 1 more
+        storageAccessMetrics.logAccessViaFuse(i, "myfile.txt");
+        // Pull stats
+        List<PackageStorageAccessStats> statsList =
+                storageAccessMetrics.getSampleStats();
+
+        assertThat(statsList).hasSize(UID_SAMPLES_COUNT_LIMIT);
+    }
+}
diff --git a/tests/src/com/android/providers/media/metrics/TranscodeMetricsTest.java b/tests/src/com/android/providers/media/metrics/TranscodeMetricsTest.java
index 5c55047..8b635df 100644
--- a/tests/src/com/android/providers/media/metrics/TranscodeMetricsTest.java
+++ b/tests/src/com/android/providers/media/metrics/TranscodeMetricsTest.java
@@ -16,11 +16,6 @@
 
 package com.android.providers.media.metrics;
 
-import static android.app.StatsManager.PULL_SKIP;
-import static android.app.StatsManager.PULL_SUCCESS;
-
-import static com.android.providers.media.MediaProviderStatsLog.TRANSCODING_DATA;
-
 import static com.google.common.truth.Truth.assertThat;
 
 import android.util.StatsEvent;
@@ -31,7 +26,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.ArrayList;
 import java.util.List;
 
 @RunWith(AndroidJUnit4.class)
@@ -52,27 +46,13 @@
     @After
     public void tearDown() {
         // this is to reset the saved data in TranscodeMetrics.
-        TranscodeMetrics.handleStatsEventDataRequest(TRANSCODING_DATA, new ArrayList<>());
-    }
-
-    @Test
-    public void testHandleStatsEventDataRequest_skipsUnknownTag() {
-        List<StatsEvent> statsEvents = new ArrayList<>();
-        int retValue = TranscodeMetrics.handleStatsEventDataRequest(UNKNOWN_ATOM_TAG, statsEvents);
-        assertThat(retValue).isEqualTo(PULL_SKIP);
-    }
-
-    @Test
-    public void testHandleStatsEventDataRequest_handlesKnownTag() {
-        List<StatsEvent> statsEvents = new ArrayList<>();
-        int retValue = TranscodeMetrics.handleStatsEventDataRequest(TRANSCODING_DATA, statsEvents);
-        assertThat(retValue).isEqualTo(PULL_SUCCESS);
+        TranscodeMetrics.pullStatsEvents();
     }
 
     @Test
     public void testSaveStatsData_doesNotGoBeyondHardLimit() {
         for (int i = 0; i < TranscodeMetrics.getStatsDataCountHardLimit() + 5; ++i) {
-            TranscodeMetrics.forceSaveStatsData(EMPTY_STATS_DATA);
+            TranscodeMetrics.saveStatsData(EMPTY_STATS_DATA);
         }
         assertThat(TranscodeMetrics.getSavedStatsDataCount()).isEqualTo(
                 TranscodeMetrics.getStatsDataCountHardLimit());
@@ -82,7 +62,7 @@
     public void testSaveStatsData_totalStatsDataCountEqualsPassedData() {
         int totalRequestsToPass = TranscodeMetrics.getStatsDataCountHardLimit() + 5;
         for (int i = 0; i < totalRequestsToPass; ++i) {
-            TranscodeMetrics.forceSaveStatsData(EMPTY_STATS_DATA);
+            TranscodeMetrics.saveStatsData(EMPTY_STATS_DATA);
         }
         assertThat(TranscodeMetrics.getTotalStatsDataCount()).isEqualTo(totalRequestsToPass);
     }
@@ -91,7 +71,7 @@
     public void testSaveStatsData_savedStatsDataCountEqualsPassedData_withinHardLimit() {
         int totalRequestsToPass = TranscodeMetrics.getStatsDataCountHardLimit() - 5;
         for (int i = 0; i < totalRequestsToPass; ++i) {
-            TranscodeMetrics.forceSaveStatsData(EMPTY_STATS_DATA);
+            TranscodeMetrics.saveStatsData(EMPTY_STATS_DATA);
         }
         assertThat(TranscodeMetrics.getSavedStatsDataCount()).isEqualTo(totalRequestsToPass);
     }
@@ -99,11 +79,10 @@
     @Test
     public void testHandleStatsEventDataRequest_resetsData() {
         for (int i = 0; i < TranscodeMetrics.getStatsDataCountHardLimit(); ++i) {
-            TranscodeMetrics.forceSaveStatsData(EMPTY_STATS_DATA);
+            TranscodeMetrics.saveStatsData(EMPTY_STATS_DATA);
         }
 
-        List<StatsEvent> statsEvents = new ArrayList<>();
-        TranscodeMetrics.handleStatsEventDataRequest(TRANSCODING_DATA, statsEvents);
+        List<StatsEvent> statsEvents = TranscodeMetrics.pullStatsEvents();
 
         assertThat(TranscodeMetrics.getSavedStatsDataCount()).isEqualTo(0);
         assertThat(TranscodeMetrics.getTotalStatsDataCount()).isEqualTo(0);
@@ -112,11 +91,10 @@
     @Test
     public void testHandleStatsEventDataRequest_fillsExactlySampleLimit_excessData() {
         for (int i = 0; i < TranscodeMetrics.getStatsDataCountHardLimit(); ++i) {
-            TranscodeMetrics.forceSaveStatsData(EMPTY_STATS_DATA);
+            TranscodeMetrics.saveStatsData(EMPTY_STATS_DATA);
         }
 
-        List<StatsEvent> statsEvents = new ArrayList<>();
-        TranscodeMetrics.handleStatsEventDataRequest(TRANSCODING_DATA, statsEvents);
+        List<StatsEvent> statsEvents = TranscodeMetrics.pullStatsEvents();
 
         assertThat(statsEvents.size()).isEqualTo(TranscodeMetrics.getStatsDataSampleLimit());
     }
@@ -125,11 +103,10 @@
     public void testHandleStatsEventDataRequest_fillsExactlyAsSaved_dataWithinSampleLimit() {
         int totalRequestsToPass = TranscodeMetrics.getStatsDataSampleLimit() - 5;
         for (int i = 0; i < totalRequestsToPass; ++i) {
-            TranscodeMetrics.forceSaveStatsData(EMPTY_STATS_DATA);
+            TranscodeMetrics.saveStatsData(EMPTY_STATS_DATA);
         }
 
-        List<StatsEvent> statsEvents = new ArrayList<>();
-        TranscodeMetrics.handleStatsEventDataRequest(TRANSCODING_DATA, statsEvents);
+        List<StatsEvent> statsEvents = TranscodeMetrics.pullStatsEvents();
 
         assertThat(statsEvents.size()).isEqualTo(totalRequestsToPass);
     }
diff --git a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
index 7facf05..c434bd2 100644
--- a/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/PermissionUtilsTest.java
@@ -89,14 +89,9 @@
     private static final TestApp LEGACY_TEST_APP = new TestApp("LegacyTestApp",
             "com.android.providers.media.testapp.legacy", 1, false,
             "LegacyMediaProviderTestApp.apk");
-    private static final Map<String, Integer> pidMap = new HashMap<>();
 
-    @BeforeClass
-    public static void startTestApps() throws Exception {
-        startTestApp(TEST_APP_WITH_STORAGE_PERMS);
-        startTestApp(TEST_APP_WITHOUT_PERMS);
-        startTestApp(LEGACY_TEST_APP);
-    }
+    // Permission checks are based on uid, so we can pass -1 pid and avoid starting the test apps.
+    private static final int TEST_APP_PID = -1;
 
     @Test
     public void testConstructor() {
@@ -149,25 +144,24 @@
     public void testDefaultPermissionsOnTestAppWithStoragePerms() throws Exception {
         String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
         int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
-        int testAppPid = pidMap.get(packageName);
         adoptShellPermission(UPDATE_APP_OPS_STATS);
 
         try {
-            assertThat(checkPermissionSelf(getContext(), testAppPid, testAppUid)).isFalse();
-            assertThat(checkPermissionShell(getContext(), testAppPid, testAppUid)).isFalse();
+            assertThat(checkPermissionSelf(getContext(), TEST_APP_PID, testAppUid)).isFalse();
+            assertThat(checkPermissionShell(getContext(), TEST_APP_PID, testAppUid)).isFalse();
             assertThat(
                     checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
                             null)).isFalse();
             assertThat(
-                    checkPermissionInstallPackages(getContext(), testAppPid, testAppUid,
+                    checkPermissionInstallPackages(getContext(), TEST_APP_PID, testAppUid,
                             packageName, null)).isFalse();
             assertThat(
-                    checkPermissionAccessMtp(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionAccessMtp(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
             assertThat(
-                    checkPermissionWriteStorage(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isTrue();
-            checkReadPermissions(testAppPid, testAppUid, packageName, true);
+            checkReadPermissions(TEST_APP_PID, testAppUid, packageName, true);
         } finally {
             dropShellPermission();
         }
@@ -177,36 +171,37 @@
     public void testDefaultPermissionsOnTestAppWithoutPerms() throws Exception {
         String packageName = TEST_APP_WITHOUT_PERMS.getPackageName();
         int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
-        int testAppPid = pidMap.get(packageName);
         adoptShellPermission(UPDATE_APP_OPS_STATS);
 
         try {
-            assertThat(checkPermissionSelf(getContext(), testAppPid, testAppUid)).isFalse();
-            assertThat(checkPermissionShell(getContext(), testAppPid, testAppUid)).isFalse();
+            assertThat(checkPermissionSelf(getContext(), TEST_APP_PID, testAppUid)).isFalse();
+            assertThat(checkPermissionShell(getContext(), TEST_APP_PID, testAppUid)).isFalse();
             assertThat(
-                    checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionManager(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
 
-            assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
-                    null)).isFalse();
+            assertThat(
+                    checkPermissionManageMedia(
+                            getContext(), TEST_APP_PID, testAppUid, packageName, null))
+                    .isFalse();
 
-            assertThat(checkPermissionAccessMediaLocation(getContext(), testAppPid, testAppUid,
+            assertThat(checkPermissionAccessMediaLocation(getContext(), TEST_APP_PID, testAppUid,
                     packageName, null)).isFalse();
 
             assertThat(
                     checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
                             null)).isFalse();
             assertThat(
-                    checkPermissionInstallPackages(getContext(), testAppPid, testAppUid,
+                    checkPermissionInstallPackages(getContext(), TEST_APP_PID, testAppUid,
                             packageName,
                             null)).isFalse();
             assertThat(
-                    checkPermissionAccessMtp(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionAccessMtp(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
             assertThat(
-                    checkPermissionWriteStorage(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
-            checkReadPermissions(testAppPid, testAppUid, packageName, false);
+            checkReadPermissions(TEST_APP_PID, testAppUid, packageName, false);
         } finally {
             dropShellPermission();
         }
@@ -216,35 +211,36 @@
     public void testDefaultPermissionsOnLegacyTestApp() throws Exception {
         String packageName = LEGACY_TEST_APP.getPackageName();
         int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
-        int testAppPid = pidMap.get(packageName);
         adoptShellPermission(UPDATE_APP_OPS_STATS);
 
         try {
-            assertThat(checkPermissionSelf(getContext(), testAppPid, testAppUid)).isFalse();
-            assertThat(checkPermissionShell(getContext(), testAppPid, testAppUid)).isFalse();
+            assertThat(checkPermissionSelf(getContext(), TEST_APP_PID, testAppUid)).isFalse();
+            assertThat(checkPermissionShell(getContext(), TEST_APP_PID, testAppUid)).isFalse();
             assertThat(
-                    checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionManager(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
 
-            assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
-                    null)).isFalse();
+            assertThat(
+                    checkPermissionManageMedia(
+                            getContext(), TEST_APP_PID, testAppUid, packageName, null))
+                    .isFalse();
 
-            assertThat(checkPermissionAccessMediaLocation(getContext(), testAppPid, testAppUid,
+            assertThat(checkPermissionAccessMediaLocation(getContext(), TEST_APP_PID, testAppUid,
                     packageName, null)).isTrue();
 
             assertThat(
                     checkIsLegacyStorageGranted(getContext(), testAppUid, packageName,
                             null)).isTrue();
             assertThat(
-                    checkPermissionInstallPackages(getContext(), testAppPid, testAppUid,
+                    checkPermissionInstallPackages(getContext(), TEST_APP_PID, testAppUid,
                             packageName, null)).isFalse();
             assertThat(
-                    checkPermissionAccessMtp(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionAccessMtp(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
             assertThat(
-                    checkPermissionWriteStorage(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionWriteStorage(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
-            checkReadPermissions(testAppPid, testAppUid, packageName, true);
+            checkReadPermissions(TEST_APP_PID, testAppUid, packageName, true);
         } finally {
             dropShellPermission();
         }
@@ -254,19 +250,18 @@
     public void testManageExternalStoragePermissionsOnTestApp() throws Exception {
         final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
         final int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
-        final int testAppPid = pidMap.get(packageName);
         final String op = AppOpsManager.permissionToOp(MANAGE_EXTERNAL_STORAGE);
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
             modifyAppOp(testAppUid, op, AppOpsManager.MODE_ERRORED);
 
-            assertThat(checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
+            assertThat(checkPermissionManager(getContext(), TEST_APP_PID, testAppUid, packageName,
                     null)).isFalse();
 
             modifyAppOp(testAppUid, op, AppOpsManager.MODE_ALLOWED);
 
-            assertThat(checkPermissionManager(getContext(), testAppPid, testAppUid, packageName,
+            assertThat(checkPermissionManager(getContext(), TEST_APP_PID, testAppUid, packageName,
                     null)).isTrue();
         } finally {
             dropShellPermission();
@@ -278,20 +273,23 @@
     public void testManageMediaPermissionsOnTestApp() throws Exception {
         final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
         final int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
-        final int testAppPid = pidMap.get(packageName);
         final String op = AppOpsManager.permissionToOp(MANAGE_MEDIA);
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
             modifyAppOp(testAppUid, op, AppOpsManager.MODE_ERRORED);
 
-            assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
-                    null)).isFalse();
+            assertThat(
+                    checkPermissionManageMedia(
+                            getContext(), TEST_APP_PID, testAppUid, packageName, null))
+                    .isFalse();
 
             modifyAppOp(testAppUid, op, AppOpsManager.MODE_ALLOWED);
 
-            assertThat(checkPermissionManageMedia(getContext(), testAppPid, testAppUid, packageName,
-                    null)).isTrue();
+            assertThat(
+                    checkPermissionManageMedia(
+                            getContext(), TEST_APP_PID, testAppUid, packageName, null))
+                    .isTrue();
         } finally {
             dropShellPermission();
         }
@@ -301,23 +299,22 @@
     public void testSystemGalleryPermissionsOnTestApp() throws Exception {
         String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
         int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
-        int testAppPid = pidMap.get(packageName);
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
-            checkPermissionsForGallery(testAppUid, testAppPid, packageName, false);
+            checkPermissionsForGallery(testAppUid, TEST_APP_PID, packageName, false);
 
             final String[] SYSTEM_GALERY_APPOPS =
                     {OPSTR_WRITE_MEDIA_IMAGES, OPSTR_WRITE_MEDIA_VIDEO};
             for (String op : SYSTEM_GALERY_APPOPS) {
                 modifyAppOp(testAppUid, op, AppOpsManager.MODE_ALLOWED);
             }
-            checkPermissionsForGallery(testAppUid, testAppPid, packageName, true);
+            checkPermissionsForGallery(testAppUid, TEST_APP_PID, packageName, true);
 
             for (String op : SYSTEM_GALERY_APPOPS) {
                 modifyAppOp(testAppUid, op, AppOpsManager.MODE_ERRORED);
             }
-            checkPermissionsForGallery(testAppUid, testAppPid, packageName, false);
+            checkPermissionsForGallery(testAppUid, TEST_APP_PID, packageName, false);
         } finally {
             dropShellPermission();
         }
@@ -353,22 +350,21 @@
         final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
         int testAppUid = getContext().getPackageManager().getPackageUid(
                 packageName, 0);
-        int testAppPid = pidMap.get(packageName);
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
             assertThat(
-                    checkPermissionReadVideo(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isTrue();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VIDEO, AppOpsManager.MODE_ERRORED);
             assertThat(
-                    checkPermissionReadVideo(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_VIDEO, AppOpsManager.MODE_ALLOWED);
             assertThat(
-                    checkPermissionReadVideo(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionReadVideo(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isTrue();
         } finally {
             dropShellPermission();
@@ -380,22 +376,21 @@
         final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
         int testAppUid = getContext().getPackageManager().getPackageUid(
                 packageName, 0);
-        int testAppPid = pidMap.get(packageName);
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
             assertThat(
-                    checkPermissionWriteAudio(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionWriteAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
 
             modifyAppOp(testAppUid, OPSTR_WRITE_MEDIA_AUDIO, AppOpsManager.MODE_ALLOWED);
             assertThat(
-                    checkPermissionWriteAudio(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionWriteAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isTrue();
 
             modifyAppOp(testAppUid, OPSTR_WRITE_MEDIA_AUDIO, AppOpsManager.MODE_ERRORED);
             assertThat(
-                    checkPermissionWriteAudio(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionWriteAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
         } finally {
             dropShellPermission();
@@ -407,22 +402,21 @@
         final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
         int testAppUid = getContext().getPackageManager().getPackageUid(
                 packageName, 0);
-        int testAppPid = pidMap.get(packageName);
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
             assertThat(
-                    checkPermissionReadAudio(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isTrue();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_AUDIO, AppOpsManager.MODE_ERRORED);
             assertThat(
-                    checkPermissionReadAudio(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_AUDIO, AppOpsManager.MODE_ALLOWED);
             assertThat(
-                    checkPermissionReadAudio(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionReadAudio(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isTrue();
         } finally {
             dropShellPermission();
@@ -433,22 +427,21 @@
     public void testReadImagesOnTestApp() throws Exception {
         final String packageName = TEST_APP_WITH_STORAGE_PERMS.getPackageName();
         int testAppUid = getContext().getPackageManager().getPackageUid(packageName, 0);
-        int testAppPid = pidMap.get(packageName);
         adoptShellPermission(UPDATE_APP_OPS_STATS, MANAGE_APP_OPS_MODES);
 
         try {
             assertThat(
-                    checkPermissionReadImages(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isTrue();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_IMAGES, AppOpsManager.MODE_ERRORED);
             assertThat(
-                    checkPermissionReadImages(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isFalse();
 
             modifyAppOp(testAppUid, OPSTR_READ_MEDIA_IMAGES, AppOpsManager.MODE_ALLOWED);
             assertThat(
-                    checkPermissionReadImages(getContext(), testAppPid, testAppUid, packageName,
+                    checkPermissionReadImages(getContext(), TEST_APP_PID, testAppUid, packageName,
                             null)).isTrue();
         } finally {
             dropShellPermission();
@@ -477,23 +470,10 @@
                     checkAppOpRequestInstallPackagesForSharedUid(getContext(), testAppUid,
                             packageName, null)).isFalse();
         } finally {
-            // App gets killed when we try to give it OPSTR_REQUEST_INSTALL_PACKAGES. Restart the
-            // app so it doesn't affect other tests.
-            startTestApp(TEST_APP_WITH_STORAGE_PERMS);
             dropShellPermission();
         }
     }
 
-    static private void startTestApp(TestApp testApp) throws Exception {
-        Intent intent = new Intent(Intent.ACTION_MAIN);
-        intent.setPackage(testApp.getPackageName());
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.addCategory(Intent.CATEGORY_LAUNCHER);
-        intent.putExtra(QUERY_TYPE, RUN_INFINITE_ACTIVITY);
-        getContext().startActivity(intent);
-        pidMap.put(testApp.getPackageName(), getPid(testApp.getPackageName()));
-    }
-
     static private void modifyAppOp(int uid, String op, int mode) {
         getContext().getSystemService(AppOpsManager.class).setUidMode(op, uid, mode);
     }
@@ -506,7 +486,9 @@
                 checkPermissionWriteImages(getContext(), pid, uid, packageName, null));
         assertEquals(expected,
                 checkPermissionWriteVideo(getContext(), pid, uid, packageName, null));
-        assertThat(checkPermissionWriteAudio(getContext(), pid, uid, packageName, null)).isFalse();
+        assertThat(
+                checkPermissionWriteAudio(getContext(), pid, uid, packageName, null))
+                .isFalse();
     }
 
     static private void checkReadPermissions(int pid, int uid, String packageName,