Merge "Fix ENAMETOOLONG issue when set trash or pending to file"
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index b58c930..e0b9983 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -28,6 +28,8 @@
     <!-- Permissions required for reading device configs -->
     <uses-permission android:name="android.permission.READ_DEVICE_CONFIG"/>
 
+    <uses-permission android:name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND"/>
+
     <application
             android:name="com.android.providers.media.MediaApplication"
             android:label="@string/app_label"
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 3af81a2..74ff71e 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -35,6 +35,9 @@
         },
         {
             "name": "fuse_node_test"
+        },
+        {
+            "name": "MediaProviderTranscodeTests"
         }
     ],
     "postsubmit": [
diff --git a/apex/apex_manifest.json b/apex/apex_manifest.json
index ffef8fb..6d8da53 100644
--- a/apex/apex_manifest.json
+++ b/apex/apex_manifest.json
@@ -1,4 +1,4 @@
 {
   "name": "com.android.mediaprovider",
-  "version": 300000000
+  "version": 309999900
 }
diff --git a/apex/framework/java/android/provider/MediaStore.java b/apex/framework/java/android/provider/MediaStore.java
index 4894a32..57ad915 100644
--- a/apex/framework/java/android/provider/MediaStore.java
+++ b/apex/framework/java/android/provider/MediaStore.java
@@ -1741,6 +1741,44 @@
             public static final int MEDIA_TYPE_DOCUMENT = 6;
 
             /**
+             * Modifier of the database row
+             *
+             * Specifies the last modifying operation of the database row. This
+             * does not give any information on the package that modified the
+             * database row.
+             * Initially, this column will be populated by
+             * {@link ContentResolver}#insert and media scan operations. And,
+             * the column will be used to identify if the file was previously
+             * scanned.
+             * @hide
+             */
+            @Column(value = Cursor.FIELD_TYPE_INTEGER)
+            public static final String _MODIFIER = "_modifier";
+
+            /**
+             * Constant for the {@link #_MODIFIER} column indicating
+             * that the last modifier of the database row is FUSE operation.
+             * @hide
+             */
+            public static final int _MODIFIER_FUSE = 1;
+
+            /**
+             * Constant for the {@link #_MODIFIER} column indicating
+             * that the last modifier of the database row is explicit
+             * {@link ContentResolver} operation from app.
+             * @hide
+             */
+            public static final int _MODIFIER_CR = 2;
+
+            /**
+             * Constant for the {@link #_MODIFIER} column indicating
+             * that the last modifier of the database row is a media scan
+             * operation.
+             * @hide
+             */
+            public static final int _MODIFIER_MEDIA_SCAN = 3;
+
+            /**
              * Status of the transcode file
              *
              * For apps that do not support modern media formats for video, we
diff --git a/jni/Android.bp b/jni/Android.bp
index 2223e5e..dfb2da3 100644
--- a/jni/Android.bp
+++ b/jni/Android.bp
@@ -60,6 +60,12 @@
         "-google-runtime-int",
     ],
 
+    target: {
+        android32: {
+            cflags: ["-DNO_FUSE_PASSTHROUGH_32BIT"],
+        },
+    },
+
     sdk_version: "current",
     stl: "c++_static",
     min_sdk_version: "30",
diff --git a/jni/FuseDaemon.cpp b/jni/FuseDaemon.cpp
index a1350bf..c5f4ab7 100644
--- a/jni/FuseDaemon.cpp
+++ b/jni/FuseDaemon.cpp
@@ -540,7 +540,13 @@
 
     if (fuse->passthrough) {
         if (conn->capable & FUSE_CAP_PASSTHROUGH) {
+#ifdef NO_FUSE_PASSTHROUGH_32BIT
+            // TODO(b/175151591) Linux compatibility layer for FUSE_DEV_IOC_PASSTHROUGH_OPEN
+            LOG(WARNING) << "Passthrough feature not supported on 32-bit userspace";
+            fuse->passthrough = false;
+#else
             mask |= FUSE_CAP_PASSTHROUGH;
+#endif
         } else {
             LOG(WARNING) << "Passthrough feature not supported by the kernel";
             fuse->passthrough = false;
diff --git a/res/raw/transcode_compat_manifest b/res/raw/transcode_compat_manifest
new file mode 100644
index 0000000..156a939
--- /dev/null
+++ b/res/raw/transcode_compat_manifest
@@ -0,0 +1,44 @@
+com.google.android.GoogleCamera,1
+com.google.android.apps.nbu.files,1
+com.instagram.android,1
+com.whatsapp,1
+com.google.android.apps.photos,1
+com.linecorp.b612.android,1
+im.thebot.messenger,1
+com.tencent.mm,1
+com.skype.raider,1
+com.facebook.katana,1
+com.facebook.orca,1
+com.imo.android.imous,1
+com.snapchat.android,1
+com.yy.biu,1
+com.google.android.talk,1
+com.twitter.android,1
+com.zhiliaoapp.musically,1
+com.nexstreaming.app.kinemasterfree,1
+com.magisto,1
+com.google.android.apps.tachyon,1
+com.kwai.bulldog,1
+com.funcamerastudio.videomaker,1
+com.avcrbt.funimate,1
+com.wondershare.filmorago,1
+com.vivashow.share.video.chat,1
+com.xvideostudio.videoeditor,1
+com.camerasideas.instashot,1
+com.gaana,1
+com.barfi.fun.videos,1
+com.hike.chat.stickers,1
+com.next.innovation.takatak,1
+com.stupeflix.replay,1
+com.sand.airdroid,1
+com.metago.astro,1
+pl.solidexplore2,1
+mobi.fileexplorer,1
+com.dewmobile.kuaiya.play,1
+com.amaze.filemanager,1
+in.mohalla.sharechat,1
+com.adobe.premiererush.videoeditor,1
+com.amazon.drive,1
+com.box.android,1
+com.microsoft.skydrive,1
+com.dropbox.android,1
diff --git a/src/com/android/providers/media/DatabaseHelper.java b/src/com/android/providers/media/DatabaseHelper.java
index 859d2ea..0daeee6 100644
--- a/src/com/android/providers/media/DatabaseHelper.java
+++ b/src/com/android/providers/media/DatabaseHelper.java
@@ -806,7 +806,8 @@
                 + "f_number TEXT DEFAULT NULL, iso INTEGER DEFAULT NULL,"
                 + "scene_capture_type INTEGER DEFAULT NULL, generation_added INTEGER DEFAULT 0,"
                 + "generation_modified INTEGER DEFAULT 0, xmp BLOB DEFAULT NULL,"
-                + "_transcode_status INTEGER DEFAULT 0, _video_codec_type TEXT DEFAULT NULL)");
+                + "_transcode_status INTEGER DEFAULT 0, _video_codec_type TEXT DEFAULT NULL,"
+                + "_modifier INTEGER DEFAULT 0)");
 
         db.execSQL("CREATE TABLE log (time DATETIME, message TEXT)");
         if (!mInternal) {
@@ -911,12 +912,13 @@
                     // When migrating pending or trashed files, we might need to
                     // rename them on disk to match new schema
                     if (volumePath != null) {
+                        final String oldData = values.getAsString(MediaColumns.DATA);
                         FileUtils.computeDataFromValues(values, new File(volumePath),
                                 /*isForFuse*/ false);
                         final String recomputedData = values.getAsString(MediaColumns.DATA);
-                        if (!Objects.equals(data, recomputedData)) {
+                        if (!Objects.equals(oldData, recomputedData)) {
                             try {
-                                renameWithRetry(data, recomputedData);
+                                renameWithRetry(oldData, recomputedData);
                             } catch (IOException e) {
                                 // We only have one shot to migrate data, so log and
                                 // keep marching forward
@@ -1495,6 +1497,10 @@
         db.execSQL("UPDATE files SET date_modified=0 WHERE media_type=2;");
     }
 
+    private static void updateAddModifier(SQLiteDatabase db, boolean internal) {
+        db.execSQL("ALTER TABLE files ADD COLUMN _modifier INTEGER DEFAULT 0;");
+    }
+
     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)) {
@@ -1559,7 +1565,7 @@
     static final int VERSION_R = 1115;
     // Leave some gaps in database version tagging to allow R schema changes
     // to go independent of S schema changes.
-    static final int VERSION_S = 1201;
+    static final int VERSION_S = 1202;
     static final int VERSION_LATEST = VERSION_S;
 
     /**
@@ -1711,6 +1717,9 @@
             if (fromVersion < 1201) {
                 updateAddVideoCodecType(db, internal);
             }
+            if (fromVersion < 1202) {
+                updateAddModifier(db, internal);
+            }
 
             // If this is the legacy database, it's not worth recomputing data
             // values locally, since they'll be recomputed after the migration
@@ -1783,7 +1792,7 @@
      * to retry several times before giving up.
      * The retry logic is mainly added to avoid test flakiness.
      */
-    private static String writeToPlaylistFileWithRetry(@NonNull File playlistFile,
+    private static void writeToPlaylistFileWithRetry(@NonNull File playlistFile,
             @NonNull Playlist playlist) throws IOException {
         final long start = SystemClock.elapsedRealtime();
         while (true) {
@@ -1795,6 +1804,7 @@
                 playlistFile.getParentFile().mkdirs();
                 playlistFile.createNewFile();
                 playlist.write(playlistFile);
+                return;
             } catch (IOException e) {
                 Log.i(TAG, "Failed to migrate playlist file, retrying " + e);
             }
diff --git a/src/com/android/providers/media/MediaProvider.java b/src/com/android/providers/media/MediaProvider.java
index e238c8d..16d5620 100644
--- a/src/com/android/providers/media/MediaProvider.java
+++ b/src/com/android/providers/media/MediaProvider.java
@@ -143,6 +143,7 @@
 import android.preference.PreferenceManager;
 import android.provider.BaseColumns;
 import android.provider.Column;
+import android.provider.DeviceConfig;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Audio;
 import android.provider.MediaStore.Audio.AudioColumns;
@@ -3442,6 +3443,15 @@
             values.put(FileColumns.MEDIA_TYPE, mediaType);
         }
 
+        if (isCallingPackageSelf() && values.containsKey(FileColumns._MODIFIER)) {
+            // We can't identify if the call is coming from media scan, hence
+            // we let ModernMediaScanner send FileColumns._MODIFIER value.
+        } else if (isFuseThread()) {
+            values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_FUSE);
+        } else {
+            values.put(FileColumns._MODIFIER, FileColumns._MODIFIER_CR);
+        }
+
         final long rowId;
         {
             if (mediaType == FileColumns.MEDIA_TYPE_PLAYLIST) {
@@ -6155,11 +6165,6 @@
                 // This means either no playlist members match the query or VolumeNotFoundException
                 // was thrown. So we don't have anything to delete.
                 count = 0;
-            } else if (indexes.length > 1 &&
-                    getCallingPackageTargetSdkVersion() >= Build.VERSION_CODES.R) {
-                throw new FallbackException("Failed to update playlist",
-                        new IllegalStateException("Query matches more than one playlist member"),
-                        android.os.Build.VERSION_CODES.R);
             } else {
                 count = playlist.removeMultiple(indexes);
             }
@@ -6903,11 +6908,11 @@
                 boolean maybeHidden = !mNonHiddenPaths.containsKey(key);
 
                 if (maybeHidden) {
-                    File topNoMedia = FileUtils.getTopLevelNoMedia(new File(path));
-                    if (topNoMedia == null) {
+                    File topNoMediaDir = FileUtils.getTopLevelNoMedia(new File(path));
+                    if (topNoMediaDir == null) {
                         mNonHiddenPaths.put(key, 0);
                     } else {
-                        mMediaScanner.onDirectoryDirty(topNoMedia);
+                        mMediaScanner.onDirectoryDirty(topNoMediaDir);
                     }
                 }
             }
@@ -7775,6 +7780,28 @@
         return FuseDaemon.native_is_fuse_thread();
     }
 
+    @VisibleForTesting
+    public boolean getBooleanDeviceConfig(String key, boolean defaultValue) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, key,
+                    defaultValue);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    @VisibleForTesting
+    public String getStringDeviceConfig(String key, String defaultValue) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return DeviceConfig.getString(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT, key,
+                    defaultValue);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
     @Deprecated
     private boolean checkCallingPermissionAudio(boolean forWrite, String callingPackage) {
         if (forWrite) {
diff --git a/src/com/android/providers/media/TranscodeHelper.java b/src/com/android/providers/media/TranscodeHelper.java
index fe602f0..18122e5 100644
--- a/src/com/android/providers/media/TranscodeHelper.java
+++ b/src/com/android/providers/media/TranscodeHelper.java
@@ -52,6 +52,7 @@
 import android.os.Environment;
 import android.os.Handler;
 import android.os.SystemProperties;
+import android.os.UserHandle;
 import android.provider.MediaStore;
 import android.provider.MediaStore.Files.FileColumns;
 import android.util.ArrayMap;
@@ -69,14 +70,15 @@
 import com.android.providers.media.util.ForegroundThread;
 import com.android.providers.media.util.SQLiteQueryBuilder;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.RandomAccessFile;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -102,6 +104,7 @@
     private static final String TRANSCODE_DEFAULT_DEVICE_CONFIG_KEY = "transcode_default";
     private static final String TRANSCODE_USER_CONTROL_SYS_PROP_KEY =
             "persist.sys.fuse.transcode_user_control";
+    private static final String TRANSCODE_COMPAT_MANIFEST_KEY = "transcode_compat_manifest";
 
     /**
      * Force enable an app to support the HEVC media capability
@@ -169,16 +172,23 @@
     private static final int TYPE_UPDATE = 2;
     static final String DIRECTORY_CAMERA = "Camera";
 
+    private final Object mLock = new Object();
+    private final Context mContext;
     private final MediaProvider mMediaProvider;
     private final PackageManager mPackageManager;
     private final MediaTranscodeManager mMediaTranscodeManager;
     private final File mTranscodeDirectory;
-    @GuardedBy("mTranscodingSessions")
+    @GuardedBy("mLock")
     private final Map<String, TranscodingSession> mTranscodingSessions = new ArrayMap<>();
-    @GuardedBy("mTranscodingSessions")
+    @GuardedBy("mLock")
     private final SparseArray<CountDownLatch> mTranscodingLatches = new SparseArray<>();
     private final TranscodeUiNotifier mTranscodingUiNotifier;
     private final TranscodeMetrics mTranscodingMetrics;
+    @GuardedBy("mLock")
+    private final Map<String, Long> mAppCompatMediaCapabilities = new ArrayMap<>();
+
+    @GuardedBy("mLock")
+    private boolean mIsTranscodeEnabled;
 
     private static final String[] TRANSCODE_CACHE_INFO_PROJECTION =
             {FileColumns._ID, FileColumns._TRANSCODE_STATUS};
@@ -195,14 +205,18 @@
     };
 
     public TranscodeHelper(Context context, MediaProvider mediaProvider) {
+        mContext = context;
         mPackageManager = context.getPackageManager();
         mMediaTranscodeManager = context.getSystemService(MediaTranscodeManager.class);
         mMediaProvider = mediaProvider;
-        mTranscodeDirectory =
-                FileUtils.buildPath(Environment.getExternalStorageDirectory(), DIRECTORY_TRANSCODE);
+        mTranscodeDirectory = new File("/storage/emulated/" + UserHandle.myUserId(),
+                DIRECTORY_TRANSCODE);
         mTranscodeDirectory.mkdirs();
         mTranscodingMetrics = new TranscodeMetrics();
         mTranscodingUiNotifier = new TranscodeUiNotifier(context, mTranscodingMetrics);
+        mIsTranscodeEnabled = isTranscodeEnabled();
+
+        parseTranscodeCompatManifest();
     }
 
     /**
@@ -239,7 +253,7 @@
         TranscodingSession session = null;
         CountDownLatch latch = null;
 
-        synchronized (mTranscodingSessions) {
+        synchronized (mLock) {
             session = mTranscodingSessions.get(src);
             if (session == null) {
                 latch = new CountDownLatch(1);
@@ -313,8 +327,10 @@
 
     // TODO(b/173491972): Generalize to consider other file/app media capabilities beyond hevc
     public boolean shouldTranscode(String path, int uid, Bundle bundle) {
-        if (!getBooleanProperty(TRANSCODE_ENABLED_SYS_PROP_KEY,
-                TRANSCODE_ENABLED_DEVICE_CONFIG_KEY)) {
+        boolean isTranscodeEnabled = isTranscodeEnabled();
+        updateConfigs(isTranscodeEnabled);
+
+        if (!isTranscodeEnabled) {
             return false;
         }
 
@@ -365,34 +381,23 @@
         LocalCallingIdentity identity = mMediaProvider.getCachedCallingIdentityForTranscoding(uid);
         final String[] callingPackages = identity.getSharedPackageNames();
 
-        // Check allowPackages and manifest supported packages
-        List<String> allowPackages = Arrays.asList(ALLOW_LIST);
+        // Check manifest supported packages and mAppCompatMediaCapabilities
+        // If we are here then the file supports HEVC, so we only check if the package is in the
+        // mAppCompatCapabilities.  If it's there, we will respect that value.
         for (String callingPackage : callingPackages) {
-            if (allowPackages.contains(callingPackage)) {
-                return false;
-            } else if (checkManifestSupport(callingPackage, identity)) {
+            if (checkManifestSupport(callingPackage, identity)) {
                 return false;
             }
-        }
 
-        // Check transcodePackages
-        List<String> transcodePackages = Arrays.asList(
-                SystemProperties.get("persist.sys.fuse.transcode_packages").split(","));
-        for (String callingPackage : callingPackages) {
-            if (transcodePackages.contains(callingPackage)) {
-                return true;
+            synchronized (mLock) {
+                if (mAppCompatMediaCapabilities.containsKey(callingPackage)) {
+                    return mAppCompatMediaCapabilities.get(callingPackage) == 0;
+                }
             }
         }
 
-        // Check transcodeUids
-        List<String> transcodeUids = Arrays.asList(
-                SystemProperties.get("persist.sys.fuse.transcode_uids").split(","));
-        if (transcodeUids.contains(String.valueOf(uid))) {
-            return true;
-        }
-
         return getBooleanProperty(TRANSCODE_DEFAULT_SYS_PROP_KEY,
-                TRANSCODE_DEFAULT_DEVICE_CONFIG_KEY);
+                TRANSCODE_DEFAULT_DEVICE_CONFIG_KEY, true /* defaultValue */);
     }
 
     public boolean supportsTranscode(String path) {
@@ -432,7 +437,6 @@
             identity.setApplicationMediaCapabilitiesFlags(capabilitiesToFlags(capability));
             return capability.isVideoMimeTypeSupported(MediaFormat.MIMETYPE_VIDEO_HEVC);
         } catch (NameNotFoundException | UnsupportedOperationException e) {
-            Log.d(TAG, "No valid media capability defined for " + packageName, e);
             return false;
         }
     }
@@ -461,18 +465,15 @@
         return flags;
     }
 
-    private boolean getBooleanProperty(String sysPropKey, String deviceConfigKey) {
+    private boolean getBooleanProperty(String sysPropKey, String deviceConfigKey,
+            boolean defaultValue) {
         // If the user wants to override the default, respect that; otherwise use the DeviceConfig
         // which is filled with the values sent from server.
-        // TODO(b/169327180): Until we figure out a nice way to fix tests for using DeviceConfig,
-        // we are going to read the sysPropKey only. So, simply commenting out the code for now.
-        // if (SystemProperties.getBoolean(TRANSCODE_USER_CONTROL_SYS_PROP_KEY, false)) {
-        return SystemProperties.getBoolean(sysPropKey, false);
-        // }
+        if (SystemProperties.getBoolean(TRANSCODE_USER_CONTROL_SYS_PROP_KEY, false)) {
+            return SystemProperties.getBoolean(sysPropKey, defaultValue);
+        }
 
-        // return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
-        // deviceConfigKey,
-        //         false);
+        return mMediaProvider.getBooleanDeviceConfig(deviceConfigKey, defaultValue);
     }
 
     private Pair<Long, Integer> getTranscodeCacheInfoFromDB(String path) {
@@ -485,7 +486,7 @@
     }
 
     public boolean isTranscodeFileCached(String path, String transcodePath) {
-        if (SystemProperties.getBoolean("fuse.sys.disable_transcode_cache", false)) {
+        if (SystemProperties.getBoolean("sys.fuse.disable_transcode_cache", false)) {
             // Caching is disabled. Hence, delete the cached transcode file.
             return false;
         }
@@ -604,7 +605,7 @@
 
     private void finishTranscodingResult(int uid, String src, TranscodingSession session,
             CountDownLatch latch) {
-        synchronized (mTranscodingSessions) {
+        synchronized (mLock) {
             latch.countDown();
             session.cancel();
             mTranscodingSessions.remove(src);
@@ -669,6 +670,128 @@
         return qb.query(getDatabaseHelperForUri(uri), projection, extras, null);
     }
 
+    private boolean isTranscodeEnabled() {
+        return getBooleanProperty(TRANSCODE_ENABLED_SYS_PROP_KEY,
+                TRANSCODE_ENABLED_DEVICE_CONFIG_KEY, true /* defaultValue */);
+    }
+
+    private void updateConfigs(boolean transcodeEnabled) {
+        synchronized (mLock) {
+            boolean isTranscodeEnabledChanged = transcodeEnabled != mIsTranscodeEnabled;
+            boolean isDebug = SystemProperties.getBoolean("sys.fuse.transcode_debug", false);
+
+            if (isTranscodeEnabledChanged || isDebug) {
+                Log.i(TAG, "Reloading transcode configs. transcodeEnabled: " + transcodeEnabled
+                        + ". lastTranscodeEnabled: " + mIsTranscodeEnabled  + ". isDebug: "
+                        + isDebug);
+
+                mIsTranscodeEnabled = transcodeEnabled;
+                parseTranscodeCompatManifest();
+            }
+        }
+    }
+
+    private void parseTranscodeCompatManifest() {
+        synchronized (mLock) {
+            // Clear the transcode_compat manifest before parsing. If transcode is disabled,
+            // nothing will be parsed, effectively leaving the compat manifest empty.
+            mAppCompatMediaCapabilities.clear();
+            if (!mIsTranscodeEnabled) {
+                return;
+            }
+
+            if (!parseTranscodeCompatManifestFromDeviceConfigLocked()) {
+                Log.i(TAG, "Failed parsing transcode compat manifest from device config "
+                        + "attempting resource...");
+                parseTranscodeCompatManifestFromResourceLocked();
+            }
+        }
+    }
+
+    /** @return {@code true} if the manifest was parsed successfully, {@code false} otherwise */
+    private boolean parseTranscodeCompatManifestFromDeviceConfigLocked() {
+        final String[] manifest = mMediaProvider.getStringDeviceConfig(
+                TRANSCODE_COMPAT_MANIFEST_KEY, "").split(",");
+
+        if (manifest.length == 0) {
+            Log.i(TAG, "Empty device config transcode compat manifest");
+            return false;
+        }
+        if ((manifest.length % 2) != 0) {
+            Log.w(TAG, "Uneven number of items in device config transcode compat manifest");
+            return false;
+        }
+
+        String packageName = "";
+        Long packageCompatValue;
+        int i = 0;
+        while (i < manifest.length - 1) {
+            try {
+                packageName = manifest[i++];
+                packageCompatValue = Long.valueOf(manifest[i++]);
+                synchronized (mLock) {
+                    // Lock is already held, explicitly hold again to make error prone happy
+                    mAppCompatMediaCapabilities.put(packageName, packageCompatValue);
+                }
+            } catch (NumberFormatException e) {
+                Log.w(TAG, "Failed to parse media capability from device config for package: "
+                        + packageName, e);
+            }
+        }
+
+        synchronized (mLock) {
+            // Lock is already held, explicitly hold again to make error prone happy
+            int size = mAppCompatMediaCapabilities.size();
+            Log.i(TAG, "Parsed " + size + " packages from device config");
+            return size != 0;
+        }
+    }
+
+    /** @return {@code true} if the manifest was parsed successfully, {@code false} otherwise */
+    private boolean parseTranscodeCompatManifestFromResourceLocked() {
+        InputStream inputStream = mContext.getResources().openRawResource(
+                R.raw.transcode_compat_manifest);
+        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
+        try {
+            while(reader.ready()) {
+                String line = reader.readLine();
+                String packageName = "";
+                Long packageCompatValue;
+
+                if (line == null) {
+                    Log.w(TAG, "Unexpected null line while parsing transcode compat manifest");
+                    continue;
+                }
+
+                String[] lineValues = line.split(",");
+                if (lineValues.length != 2) {
+                    Log.w(TAG, "Failed to read line while parsing transcode compat manifest");
+                    continue;
+                }
+                try {
+                    packageName = lineValues[0];
+                    packageCompatValue = Long.valueOf(lineValues[1]);
+                    synchronized (mLock) {
+                        // Lock is already held, explicitly hold again to make error prone happy
+                        mAppCompatMediaCapabilities.put(packageName, packageCompatValue);
+                    }
+                } catch (NumberFormatException e) {
+                    Log.w(TAG, "Failed to parse media capability from resource for package: "
+                            + packageName, e);
+                }
+            }
+        } catch (IOException e) {
+            Log.w(TAG, "Failed to read transcode compat manifest", e);
+        }
+
+        synchronized (mLock) {
+            // Lock is already held, explicitly hold again to make error prone happy
+            int size = mAppCompatMediaCapabilities.size();
+            Log.i(TAG, "Parsed " + size + " packages from resource");
+            return size != 0;
+        }
+    }
+
     private void logEvent(String event, @Nullable TranscodingSession session) {
         Log.d(TAG, event + (session == null ? "" : session));
     }
diff --git a/src/com/android/providers/media/scan/ModernMediaScanner.java b/src/com/android/providers/media/scan/ModernMediaScanner.java
index e408033..5151be9 100644
--- a/src/com/android/providers/media/scan/ModernMediaScanner.java
+++ b/src/com/android/providers/media/scan/ModernMediaScanner.java
@@ -150,12 +150,17 @@
     // TODO: deprecate playlist editing
     // TODO: deprecate PARENT column, since callers can't see directories
 
-    @GuardedBy("sDateFormat")
-    private static final SimpleDateFormat sDateFormat;
+    @GuardedBy("S_DATE_FORMAT")
+    private static final SimpleDateFormat S_DATE_FORMAT;
+    @GuardedBy("S_DATE_FORMAT_WITH_MILLIS")
+    private static final SimpleDateFormat S_DATE_FORMAT_WITH_MILLIS;
 
     static {
-        sDateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
-        sDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+        S_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss");
+        S_DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
+
+        S_DATE_FORMAT_WITH_MILLIS = new SimpleDateFormat("yyyyMMdd'T'HHmmss.SSS");
+        S_DATE_FORMAT_WITH_MILLIS.setTimeZone(TimeZone.getTimeZone("UTC"));
     }
 
     private static final int BATCH_SIZE = 32;
@@ -163,7 +168,8 @@
     // |excludeDirs * 2| < 1000 which is the max SQL expression size
     // Because we add |excludeDir| and |excludeDir/| in the SQL expression to match dir and subdirs
     // See SQLITE_MAX_EXPR_DEPTH in sqlite3.c
-    private static final int MAX_EXCLUDE_DIRS = 450;
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    static final int MAX_EXCLUDE_DIRS = 450;
 
     private static final Pattern PATTERN_VISIBLE = Pattern.compile(
             "(?i)^/storage/[^/]+(?:/[0-9]+)?$");
@@ -338,6 +344,12 @@
          * indicates that one or more of the current file's parents is a hidden directory.
          */
         private int mHiddenDirCount;
+        /**
+         * Indicates if the nomedia directory tree is dirty. When a nomedia directory is dirty, we
+         * mark the top level nomedia as dirty. Hence if one of the sub directory in the nomedia
+         * directory is dirty, we consider the whole top level nomedia directory tree as dirty.
+         */
+        private boolean mIsDirectoryTreeDirty;
 
         public Scan(File root, int reason, @Nullable String ownerPackage)
                 throws FileNotFoundException {
@@ -620,7 +632,13 @@
             }
 
             synchronized (mPendingCleanDirectories) {
-                if (FileUtils.isDirectoryDirty(dir.toFile())) {
+                if (mIsDirectoryTreeDirty) {
+                    // Directory tree is dirty, continue scanning subtree.
+                } else if (FileUtils.isDirectoryDirty(FileUtils.getTopLevelNoMedia(dir.toFile()))) {
+                    // Track the directory dirty status for directory tree in mIsDirectoryDirty.
+                    // This removes additional dirty state check for subdirectories of nomedia
+                    // directory.
+                    mIsDirectoryTreeDirty = true;
                     mPendingCleanDirectories.add(dir.toFile().getPath());
                 } else {
                     Log.d(TAG, "Skipping preVisitDirectory " + dir.toFile());
@@ -690,7 +708,7 @@
             queryArgs.putInt(MediaStore.QUERY_ARG_MATCH_FAVORITE, MediaStore.MATCH_INCLUDE);
             final String[] projection = new String[] {FileColumns._ID, FileColumns.DATE_MODIFIED,
                     FileColumns.SIZE, FileColumns.MIME_TYPE, FileColumns.MEDIA_TYPE,
-                    FileColumns.IS_PENDING};
+                    FileColumns.IS_PENDING, FileColumns._MODIFIER};
 
             final Matcher matcher = FileUtils.PATTERN_EXPIRES_FILE.matcher(realFile.getName());
             // If IS_PENDING is set by FUSE, we should scan the file and update IS_PENDING to zero.
@@ -705,6 +723,8 @@
                     final String mimeType = c.getString(3);
                     final int mediaType = c.getInt(4);
                     isPendingFromFuse &= c.getInt(5) != 0;
+                    final boolean isScanned =
+                            c.getInt(6) == FileColumns._MODIFIER_MEDIA_SCAN;
 
                     // Remember visiting this existing item, even if we skipped
                     // due to it being unchanged; this is needed so we don't
@@ -722,7 +742,7 @@
                             mimeType.equalsIgnoreCase(actualMimeType);
                     final boolean sameMediaType = (actualMediaType == mediaType);
                     final boolean isSame = sameTime && sameSize && sameMediaType && sameMimeType
-                            && !isPendingFromFuse;
+                            && !isPendingFromFuse && isScanned;
                     if (attrs.isDirectory() || isSame) {
                         if (LOGV) Log.v(TAG, "Skipping unchanged " + file);
                         return FileVisitResult.CONTINUE;
@@ -741,6 +761,7 @@
                 Trace.endSection();
             }
             if (op != null) {
+                op.withValue(FileColumns._MODIFIER, FileColumns._MODIFIER_MEDIA_SCAN);
                 // Add owner package name to new insertions when package name is provided.
                 if (op.build().isInsert() && !attrs.isDirectory() && mOwnerPackage != null) {
                     op.withValue(MediaColumns.OWNER_PACKAGE_NAME, mOwnerPackage);
@@ -777,10 +798,14 @@
             // Now that we're finished scanning this directory, release lock to
             // allow other parallel scans to proceed
             releaseDirectoryLock(dir);
-            synchronized (mPendingCleanDirectories) {
-                if (mPendingCleanDirectories.remove(dir.toFile().getPath())) {
-                    // If |dir| is still clean, then persist
-                    FileUtils.setDirectoryDirty(dir.toFile(), false /* isDirty */);
+
+            if (mIsDirectoryTreeDirty) {
+                synchronized (mPendingCleanDirectories) {
+                    if (mPendingCleanDirectories.remove(dir.toFile().getPath())) {
+                        // If |dir| is still clean, then persist
+                        FileUtils.setDirectoryDirty(dir.toFile(), false /* isDirty */);
+                        mIsDirectoryTreeDirty = false;
+                    }
                 }
             }
             return FileVisitResult.CONTINUE;
@@ -1441,15 +1466,31 @@
     static @NonNull Optional<Long> parseOptionalDate(@Nullable String date) {
         if (TextUtils.isEmpty(date)) return Optional.empty();
         try {
-            synchronized (sDateFormat) {
-                final long value = sDateFormat.parse(date).getTime();
-                return (value > 0) ? Optional.of(value) : Optional.empty();
+            synchronized (S_DATE_FORMAT_WITH_MILLIS) {
+                return parseDateWithFormat(date, S_DATE_FORMAT_WITH_MILLIS);
             }
         } catch (ParseException e) {
+            // Log and try without millis as well
+            Log.d(TAG, String.format(
+                    "Parsing date with millis failed for [%s]. We will retry without millis",
+                    date));
+        }
+        try {
+            synchronized (S_DATE_FORMAT) {
+                return parseDateWithFormat(date, S_DATE_FORMAT);
+            }
+        } catch (ParseException e) {
+            Log.d(TAG, String.format("Parsing date without millis failed for [%s]", date));
             return Optional.empty();
         }
     }
 
+    private static Optional<Long> parseDateWithFormat(
+            @Nullable String date, SimpleDateFormat dateFormat) throws ParseException {
+        final long value = dateFormat.parse(date).getTime();
+        return (value > 0) ? Optional.of(value) : Optional.empty();
+    }
+
     @VisibleForTesting
     static @NonNull Optional<Integer> parseOptionalYear(@Nullable String value) {
         final Optional<String> parsedValue = parseOptional(value);
diff --git a/src/com/android/providers/media/util/FileUtils.java b/src/com/android/providers/media/util/FileUtils.java
index 7777ac3..0d6494b 100644
--- a/src/com/android/providers/media/util/FileUtils.java
+++ b/src/com/android/providers/media/util/FileUtils.java
@@ -1420,17 +1420,17 @@
      * Returns {@code null} if there's no .nomedia in hierarchy
      */
     public static File getTopLevelNoMedia(@NonNull File file) {
-        File topNoMedia = null;
+        File topNoMediaDir = null;
 
         File parent = file;
         while (parent != null) {
             File nomedia = new File(parent, ".nomedia");
             if (nomedia.exists()) {
-                topNoMedia = nomedia;
+                topNoMediaDir = parent;
             }
             parent = parent.getParentFile();
         }
 
-        return topNoMedia;
+        return topNoMediaDir;
     }
 }
diff --git a/tests/Android.bp b/tests/Android.bp
index cab61cf..8054a05 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -17,9 +17,11 @@
         "main_res",
         "res",
     ],
+
     srcs: [
         ":framework-mediaprovider-sources",
         ":mediaprovider-sources",
+        ":mediaprovider-testutils",
         "src/**/*.java",
     ],
 
@@ -50,3 +52,8 @@
         ],
     },
 }
+
+filegroup {
+    name: "mediaprovider-testutils",
+    srcs: ["utils/**/*.java"],
+}
diff --git a/tests/client/Android.bp b/tests/client/Android.bp
index 26a0a0b..2b35634 100644
--- a/tests/client/Android.bp
+++ b/tests/client/Android.bp
@@ -10,6 +10,7 @@
 
     srcs: [
         "src/**/*.java",
+        ":mediaprovider-testutils",
     ],
 
     libs: [
diff --git a/tests/client/src/com/android/providers/media/client/PerformanceTest.java b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
index 21cdb8b..867d459 100644
--- a/tests/client/src/com/android/providers/media/client/PerformanceTest.java
+++ b/tests/client/src/com/android/providers/media/client/PerformanceTest.java
@@ -36,6 +36,8 @@
 import androidx.test.filters.LargeTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.providers.media.tests.utils.Timer;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -281,6 +283,7 @@
         renameFilesTimer.dumpResults();
         deleteTimer.dumpResults();
     }
+
     private void doDirOperations(int size, Timer createTimer, Timer readTimer,
             Timer renameDirTimer, Timer renameFilesTimer, Timer deleteTimer) throws Exception {
         createTimer.start();
@@ -337,52 +340,6 @@
         return new HashSet<>(uris);
     }
 
-    /**
-     * Timer that can be started/stopped with nanosecond accuracy, and later
-     * averaged based on the number of times it was cycled.
-     */
-    static class Timer {
-        private final String name;
-        private int count;
-        private long duration;
-        private long start;
-
-        public Timer(String name) {
-            this.name = name;
-        }
-
-        public void start() {
-            if (start != 0) {
-                throw new IllegalStateException();
-            } else {
-                start = SystemClock.elapsedRealtimeNanos();
-            }
-        }
-
-        public void stop() {
-            if (start == 0) {
-                throw new IllegalStateException();
-            } else {
-                duration += (SystemClock.elapsedRealtimeNanos() - start);
-                start = 0;
-                count++;
-            }
-        }
-
-        public long getAverageDurationMillis() {
-            return TimeUnit.MILLISECONDS.convert(duration / count, TimeUnit.NANOSECONDS);
-        }
-
-        public void dumpResults() {
-            final long duration = getAverageDurationMillis();
-            Log.v(TAG, name + ": " + duration + "ms");
-
-            final Bundle results = new Bundle();
-            results.putLong(name, duration);
-            InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
-        }
-    }
-
     private static class Timers {
         public final Timer actionInsert = new Timer("action_insert");
         public final Timer actionUpdate = new Timer("action_update");
diff --git a/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java b/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java
index 5df8423..c4ea8e8 100644
--- a/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java
+++ b/tests/client/src/com/android/providers/media/client/PlaylistPerformanceTest.java
@@ -37,7 +37,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.providers.media.client.PerformanceTest.Timer;
+import com.android.providers.media.tests.utils.Timer;
 
 import org.junit.After;
 import org.junit.Before;
diff --git a/tests/src/com/android/providers/media/scan/MediaScannerTest.java b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
index 3d28820..d938180 100644
--- a/tests/src/com/android/providers/media/scan/MediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/MediaScannerTest.java
@@ -85,6 +85,16 @@
                 public boolean isFuseThread() {
                     return asFuseThread;
                 }
+
+                @Override
+                public boolean getBooleanDeviceConfig(String key, boolean defaultValue) {
+                    return defaultValue;
+                }
+
+                @Override
+                public String getStringDeviceConfig(String key, String defaultValue) {
+                    return defaultValue;
+                }
             };
             mProvider.attachInfo(this, info);
             mResolver.addProvider(MediaStore.AUTHORITY, mProvider);
diff --git a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
index 9b7f0d2..e7e890b 100644
--- a/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
+++ b/tests/src/com/android/providers/media/scan/ModernMediaScannerTest.java
@@ -18,6 +18,7 @@
 
 import static com.android.providers.media.scan.MediaScanner.REASON_UNKNOWN;
 import static com.android.providers.media.scan.MediaScannerTest.stage;
+import static com.android.providers.media.scan.ModernMediaScanner.MAX_EXCLUDE_DIRS;
 import static com.android.providers.media.scan.ModernMediaScanner.shouldScanPathAndIsPathHidden;
 import static com.android.providers.media.scan.ModernMediaScanner.isFileAlbumArt;
 import static com.android.providers.media.scan.ModernMediaScanner.parseOptional;
@@ -36,6 +37,8 @@
 import static com.android.providers.media.util.FileUtils.isDirectoryHidden;
 import static com.android.providers.media.util.FileUtils.isFileHidden;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -68,6 +71,7 @@
 
 import com.android.providers.media.R;
 import com.android.providers.media.scan.MediaScannerTest.IsolatedContext;
+import com.android.providers.media.tests.utils.Timer;
 import com.android.providers.media.util.FileUtils;
 
 import com.google.common.io.ByteStreams;
@@ -90,6 +94,11 @@
     // TODO: scan directory-vs-files and confirm identical results
 
     private static final String TAG = "ModernMediaScannerTest";
+    /**
+     * Number of times we should repeat an operation to get an average/max.
+     */
+    private static final int COUNT_REPEAT = 5;
+
     private File mDir;
 
     private Context mIsolatedContext;
@@ -228,7 +237,12 @@
 
     @Test
     public void testParseOptionalDate() throws Exception {
-        assertEquals(1577836800000L, (long) parseOptionalDate("20200101T000000").get());
+        assertThat(parseOptionalDate("20200101T000000")).isEqualTo(Optional.of(1577836800000L));
+        assertThat(parseOptionalDate("20200101T211205")).isEqualTo(Optional.of(1577913125000L));
+        assertThat(parseOptionalDate("20200101T211205.000Z"))
+                .isEqualTo(Optional.of(1577913125000L));
+        assertThat(parseOptionalDate("20200101T211205.123Z"))
+                .isEqualTo(Optional.of(1577913125123L));
     }
 
     @Test
@@ -731,12 +745,12 @@
 
     @Test
     public void testScan_Nomedia_Dir() throws Exception {
-        final File red = new File(mDir, "red");
-        final File blue = new File(mDir, "blue");
-        red.mkdirs();
-        blue.mkdirs();
-        stage(R.raw.test_image, new File(red, "red.jpg"));
-        stage(R.raw.test_image, new File(blue, "blue.jpg"));
+        final File redDir = new File(mDir, "red");
+        final File blueDir = new File(mDir, "blue");
+        redDir.mkdirs();
+        blueDir.mkdirs();
+        stage(R.raw.test_image, new File(redDir, "red.jpg"));
+        stage(R.raw.test_image, new File(blueDir, "blue.jpg"));
 
         mModern.scanDirectory(mDir, REASON_UNKNOWN);
 
@@ -744,7 +758,7 @@
         assertQueryCount(2, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
 
         // Hide one directory, rescan, and confirm hidden
-        final File redNomedia = new File(red, ".nomedia");
+        final File redNomedia = new File(redDir, ".nomedia");
         redNomedia.createNewFile();
         mModern.scanDirectory(mDir, REASON_UNKNOWN);
         assertQueryCount(1, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
@@ -756,6 +770,35 @@
     }
 
     @Test
+    public void testScan_MaxExcludeNomediaDirs_DoesNotThrowException() throws Exception {
+        // Create MAX_EXCLUDE_DIRS + 50 nomedia dirs in mDir
+        // (Need to add 50 as MAX_EXCLUDE_DIRS is a safe limit;
+        // 499 would have been too close to the exception limit)
+        // Mark them as non-dirty so that they are excluded from scans
+        for (int i = 0 ; i < (MAX_EXCLUDE_DIRS + 50) ; i++) {
+            createCleanNomediaDir(mDir);
+        }
+
+        final File redDir = new File(mDir, "red");
+        redDir.mkdirs();
+        stage(R.raw.test_image, new File(redDir, "red.jpg"));
+
+        assertQueryCount(0, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+        mModern.scanDirectory(mDir, REASON_UNKNOWN);
+        assertQueryCount(1, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+    }
+
+    private void createCleanNomediaDir(File dir) throws Exception {
+        final File nomediaDir = new File(dir, "test_" + System.nanoTime());
+        nomediaDir.mkdirs();
+        final File nomedia = new File(nomediaDir, ".nomedia");
+        nomedia.createNewFile();
+
+        FileUtils.setDirectoryDirty(nomediaDir, false);
+        assertThat(FileUtils.isDirectoryDirty(nomediaDir)).isFalse();
+    }
+
+    @Test
     public void testScan_Nomedia_File() throws Exception {
         final File image = new File(mDir, "image.jpg");
         final File nomedia = new File(mDir, ".nomedia");
@@ -1039,12 +1082,56 @@
 
         mModern.scanDirectory(mDir, REASON_UNKNOWN);
 
-        try (Cursor cursor = mIsolatedResolver
-                .query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
-                 new String[] { MediaColumns.XMP }, null, null, null)) {
-             assertEquals(1, cursor.getCount());
-             cursor.moveToFirst();
-             assertEquals(0, cursor.getBlob(0).length);
+        try (Cursor cursor = mIsolatedResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
+                new String[] { MediaColumns.XMP }, null, null, null)) {
+            assertEquals(1, cursor.getCount());
+            cursor.moveToFirst();
+            assertEquals(0, cursor.getBlob(0).length);
         }
     }
+
+    @Test
+    public void testNoOpScan_NoMediaDirs() throws Exception {
+        File nomedia = new File(mDir, ".nomedia");
+        assertThat(nomedia.createNewFile()).isTrue();
+        for (int i = 0; i < 100; i++) {
+            File file = new File(mDir, "file_" + System.nanoTime());
+            assertThat(file.createNewFile()).isTrue();
+        }
+        Timer firstDirScan = new Timer("firstDirScan");
+        firstDirScan.start();
+        // Time taken : preVisitDirectory + 100 visitFiles
+        mModern.scanDirectory(mDir, REASON_UNKNOWN);
+        firstDirScan.stop();
+        firstDirScan.dumpResults();
+
+        // Time taken : preVisitDirectory
+        Timer noOpDirScan = new Timer("noOpDirScan");
+        for (int i = 0 ; i < COUNT_REPEAT ; i++) {
+            noOpDirScan.start();
+            mModern.scanDirectory(mDir, REASON_UNKNOWN);
+            noOpDirScan.stop();
+        }
+        noOpDirScan.dumpResults();
+        assertThat(noOpDirScan.getMaxDurationMillis()).isLessThan(
+                firstDirScan.getMaxDurationMillis());
+
+        // renaming directory for non-M_E_S apps does a scan of the directory as well;
+        // so subsequent scans should be noOp as the directory is not dirty.
+        File renamedTestDir = new File(mIsolatedContext.getExternalMediaDirs()[0],
+                "renamed_test_" + System.nanoTime());
+        assertThat(mDir.renameTo(renamedTestDir)).isTrue();
+
+        Timer renamedDirScan = new Timer("renamedDirScan");
+        renamedDirScan.start();
+        // Time taken : preVisitDirectory
+        mModern.scanDirectory(renamedTestDir, REASON_UNKNOWN);
+        renamedDirScan.stop();
+        renamedDirScan.dumpResults();
+        assertThat(renamedDirScan.getMaxDurationMillis()).isLessThan(
+                firstDirScan.getMaxDurationMillis());
+
+        // This is essential for folder cleanup in tearDown
+        mDir = renamedTestDir;
+    }
 }
diff --git a/tests/src/com/android/providers/media/util/FileUtilsTest.java b/tests/src/com/android/providers/media/util/FileUtilsTest.java
index 35a6017..2e85421 100644
--- a/tests/src/com/android/providers/media/util/FileUtilsTest.java
+++ b/tests/src/com/android/providers/media/util/FileUtilsTest.java
@@ -730,7 +730,7 @@
         File nomedia = new File(dirInDownload, ".nomedia");
         assertTrue(nomedia.createNewFile());
 
-        assertEquals(nomedia, FileUtils.getTopLevelNoMedia(new File(dirInDownload, "foo")));
+        assertEquals(dirInDownload, FileUtils.getTopLevelNoMedia(new File(dirInDownload, "foo")));
     }
 
     @Test
@@ -744,7 +744,7 @@
         File nomedia = new File(dirInTopDirInDownload, ".nomedia");
         assertTrue(nomedia.createNewFile());
 
-        assertEquals(topNomedia,
+        assertEquals(topDirInDownload,
                 FileUtils.getTopLevelNoMedia(new File(dirInTopDirInDownload, "foo")));
     }
 
diff --git a/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java b/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java
index 236f626..003f957 100644
--- a/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java
+++ b/tests/transcode/src/com/android/providers/media/transcode/TranscodeTest.java
@@ -35,11 +35,11 @@
 import android.media.ApplicationMediaCapabilities;
 import android.media.MediaFormat;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.provider.MediaStore;
-import android.system.Os;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -50,6 +50,7 @@
 import java.util.List;
 
 import org.junit.After;
+import org.junit.Assume;
 import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -85,12 +86,15 @@
 
     @Before
     public void setUp() throws Exception {
+        // TODO(b/171789917): Cuttlefish doesn't support transcoding yet
+        Assume.assumeFalse(Build.MODEL.contains("Cuttlefish"));
+
         TranscodeTestUtils.pollForExternalStorageState();
         TranscodeTestUtils.grantPermission(getContext().getPackageName(),
                 Manifest.permission.READ_EXTERNAL_STORAGE);
         TranscodeTestUtils.pollForPermission(Manifest.permission.READ_EXTERNAL_STORAGE, true);
         TranscodeTestUtils.enableSeamlessTranscoding();
-        TranscodeTestUtils.disableTranscodingForAllUids();
+        TranscodeTestUtils.disableTranscodingForAllPackages();
     }
 
     @After
@@ -110,7 +114,7 @@
 
             ParcelFileDescriptor pfdOriginal = open(modernFile, false);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
             ParcelFileDescriptor pfdTranscoded = open(modernFile, false);
 
             assertFileContent(modernFile, modernFile, pfdOriginal, pfdTranscoded, false);
@@ -139,7 +143,7 @@
             }
             ParcelFileDescriptor pfdOriginal1 = open(modernFile, false);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             for (File file : noTranscodeFiles) {
                 pfdOriginal1.seekTo(0);
@@ -164,7 +168,7 @@
         try {
             TranscodeTestUtils.stageHEVCVideoFile(modernFile);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
             ParcelFileDescriptor pfdTranscoded1 = open(modernFile, false);
             ParcelFileDescriptor pfdTranscoded2 = open(modernFile, false);
 
@@ -186,7 +190,7 @@
 
             ParcelFileDescriptor pfdOriginal = open(uri, false, null /* bundle */);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             ParcelFileDescriptor pfdTranscoded = open(uri, false, null /* bundle */);
 
@@ -217,7 +221,7 @@
 
             ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             for (int i = 0; i < noTranscodeUris.size(); i++) {
                 pfdOriginal1.seekTo(0);
@@ -244,7 +248,7 @@
         try {
             Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             ParcelFileDescriptor pfdTranscoded1 = open(uri, false, null /* bundle */);
             ParcelFileDescriptor pfdTranscoded2 = open(uri, false, null /* bundle */);
@@ -265,12 +269,12 @@
         try {
             TranscodeTestUtils.stageHEVCVideoFile(modernFile);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             assertTrue(modernFile.delete());
             assertFalse(modernFile.exists());
 
-            TranscodeTestUtils.disableTranscodingForAllUids();
+            TranscodeTestUtils.disableTranscodingForAllPackages();
 
             assertFalse(modernFile.exists());
         } finally {
@@ -289,13 +293,13 @@
         try {
             TranscodeTestUtils.stageHEVCVideoFile(modernFile);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             assertTrue(modernFile.renameTo(destFile));
             assertTrue(destFile.exists());
             assertFalse(modernFile.exists());
 
-            TranscodeTestUtils.disableTranscodingForAllUids();
+            TranscodeTestUtils.disableTranscodingForAllPackages();
 
             assertTrue(destFile.exists());
             assertFalse(modernFile.exists());
@@ -317,7 +321,7 @@
 
             assertTranscode(modernFile, false);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             assertTranscode(modernFile, true);
         } finally {
@@ -334,7 +338,7 @@
         File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
         try {
             TranscodeTestUtils.stageHEVCVideoFile(modernFile);
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             assertTranscode(modernFile, true);
             assertTranscode(modernFile, false);
@@ -353,7 +357,7 @@
         File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
         try {
             Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             assertTranscode(uri, true);
             assertTranscode(uri, false);
@@ -373,7 +377,7 @@
         File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
         try {
             Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             assertTranscode(uri, true);
             assertTranscode(modernFile, false);
@@ -392,7 +396,7 @@
         File modernFile = new File(DIR_CAMERA, HEVC_FILE_NAME);
         try {
             Uri uri = TranscodeTestUtils.stageHEVCVideoFile(modernFile);
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             assertTranscode(modernFile, true);
             assertTranscode(uri, false);
@@ -411,7 +415,7 @@
         File destFile = new File(DIR_CAMERA, "renamed_" + HEVC_FILE_NAME);
         try {
             TranscodeTestUtils.stageHEVCVideoFile(modernFile);
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             assertTranscode(modernFile, true);
 
@@ -432,7 +436,7 @@
 
             ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             Bundle bundle = new Bundle();
             bundle.putBoolean(MediaStore.EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT, true);
@@ -452,7 +456,7 @@
 
             ParcelFileDescriptor pfdOriginal = open(uri, false, null /* bundle */);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             Bundle bundle = new Bundle();
             bundle.putBoolean(MediaStore.EXTRA_ACCEPT_ORIGINAL_MEDIA_FORMAT, false);
@@ -472,7 +476,7 @@
 
             ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             Bundle bundle = new Bundle();
             ApplicationMediaCapabilities capabilities =
@@ -495,7 +499,7 @@
 
             ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             Bundle bundle = new Bundle();
             ApplicationMediaCapabilities capabilities =
@@ -518,7 +522,7 @@
 
             ParcelFileDescriptor pfdOriginal1 = open(uri, false, null /* bundle */);
 
-            TranscodeTestUtils.enableTranscodingForUid(Os.getuid());
+            TranscodeTestUtils.enableTranscodingForPackage(getContext().getPackageName());
 
             Bundle bundle = new Bundle();
             ApplicationMediaCapabilities capabilities =
diff --git a/tests/transcode/src/com/android/providers/media/transcode/TranscodeTestUtils.java b/tests/transcode/src/com/android/providers/media/transcode/TranscodeTestUtils.java
index b618b8c..682dd1b 100644
--- a/tests/transcode/src/com/android/providers/media/transcode/TranscodeTestUtils.java
+++ b/tests/transcode/src/com/android/providers/media/transcode/TranscodeTestUtils.java
@@ -108,24 +108,25 @@
     }
 
     public static void enableSeamlessTranscoding() throws Exception {
+        // This is required so that MediaProvider handles device config changes
+        executeShellCommand("setprop sys.fuse.transcode_debug true");
+        // This is required so that setprop changes take precedence over device_config changes
         executeShellCommand("setprop persist.sys.fuse.transcode_user_control true");
         executeShellCommand("setprop persist.sys.fuse.transcode_enabled true");
+        executeShellCommand("setprop persist.sys.fuse.transcode_default false");
     }
 
     public static void disableSeamlessTranscoding() throws Exception {
+        executeShellCommand("setprop sys.fuse.transcode_debug false");
         executeShellCommand("setprop persist.sys.fuse.transcode_user_control true");
         executeShellCommand("setprop persist.sys.fuse.transcode_enabled false");
+        executeShellCommand("setprop persist.sys.fuse.transcode_default false");
+        disableTranscodingForAllPackages();
     }
 
-    public static void enableTranscodingForUid(int uid) throws IOException {
-        final String command = "setprop persist.sys.fuse.transcode_uids "
-                + String.valueOf(uid);
-        executeShellCommand(command);
-    }
-
-    public static void enableTranscodingForPackage(String packageName) throws IOException {
-        final String command = "setprop persist.sys.fuse.transcode_packages " + packageName;
-        executeShellCommand(command);
+    public static void enableTranscodingForPackage(String packageName) throws Exception {
+        executeShellCommand("device_config put storage_native_boot transcode_compat_manifest "
+                + packageName + ",0");
     }
 
     public static void forceEnableAppCompatHevc(String packageName) throws IOException {
@@ -143,11 +144,8 @@
         executeShellCommand(command);
     }
 
-    public static void disableTranscodingForAllUids() throws IOException {
-        String command = "setprop persist.sys.fuse.transcode_uids -1";
-        executeShellCommand(command);
-        command = "setprop persist.sys.fuse.transcode_packages -1";
-        executeShellCommand(command);
+    public static void disableTranscodingForAllPackages() throws IOException {
+        executeShellCommand("device_config delete storage_native_boot transcode_compat_manifest");
     }
 
     /**
diff --git a/tests/utils/src/com/android/providers/media/tests/utils/Timer.java b/tests/utils/src/com/android/providers/media/tests/utils/Timer.java
new file mode 100644
index 0000000..6d8caca
--- /dev/null
+++ b/tests/utils/src/com/android/providers/media/tests/utils/Timer.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.providers.media.tests.utils;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+
+import android.util.Log;
+
+import androidx.test.InstrumentationRegistry;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * General Timer function for MediaProvider tests.
+ */
+public class Timer {
+    private static final String TAG = "Timer";
+
+    private final String name;
+    private int count;
+    private long duration;
+    private long start;
+    private long maxRunDuration = 0;
+
+    public Timer(String name) {
+        this.name = name;
+    }
+
+    public void start() {
+        if (start != 0) {
+            throw new IllegalStateException();
+        } else {
+            start = SystemClock.elapsedRealtimeNanos();
+        }
+    }
+
+    public void stop() {
+        long currentRunDuration = 0;
+        if (start == 0) {
+            throw new IllegalStateException();
+        } else {
+            currentRunDuration = (SystemClock.elapsedRealtimeNanos() - start);
+            maxRunDuration = (currentRunDuration > maxRunDuration) ? currentRunDuration :
+                maxRunDuration;
+            duration += currentRunDuration;
+            start = 0;
+            count++;
+        }
+    }
+
+    public long getAverageDurationMillis() {
+        return TimeUnit.MILLISECONDS.convert(duration / count, TimeUnit.NANOSECONDS);
+    }
+
+    public long getMaxDurationMillis() {
+        return TimeUnit.MILLISECONDS.convert(maxRunDuration, TimeUnit.NANOSECONDS);
+    }
+
+    public void dumpResults() {
+        final long duration = getAverageDurationMillis();
+        Log.v(TAG, name + ": " + duration + "ms");
+
+        final Bundle results = new Bundle();
+        results.putLong(name, duration);
+        InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
+    }
+}
diff --git a/transcode.sh b/transcode.sh
new file mode 100644
index 0000000..b36eccc
--- /dev/null
+++ b/transcode.sh
@@ -0,0 +1,17 @@
+# For extracting a transcode_compat_manifest from a csv file in the format
+# package_name,hevc_support,slow_motion_support,hdr_10_support,hdr_10_plus_support,hdr_hlg_support,hdr_dolby_vision_support,hevc_support_shift,slow_motion_support_shift,hdr_10_support_shift,hd_10_plus_support_shift,hdr_hlg_support_shift,hdr_dolby_vision_support_shift,media_capability
+# com.foo,1,0,0,0,0,0,1,0,0,0,0,0,1
+# ....
+function transcode_compat_manifest() {
+    # Cat file
+    # Remove CLRF (DOS format)
+    # Remove first line (header)
+    # Extract first and last columns in each line
+    # For device_config convert new lines (\n) to comma(,)
+    # For device_config remove trailing comma(,)
+    case "$1" in
+        -r) cat $2 | tr -d '\r' | sed 1d | awk -F "," '{new_var=$1","$NF; print new_var}';;
+        -d) cat $2 | tr -d '\r' | sed 1d | awk -F "," '{new_var=$1","$NF; print new_var}' | tr '\n' ',' | sed 's/,$//g';;
+        *) "Enter '-d' for device_config, '-r' for resource";;
+    esac
+}