Downgrade the compilation filter of unused apps.

This CL extends the downgrade functionality introduced earlier that was using
only sysprop for enabling the feature and adds DeviceConfig flags that allow
remote configuration. The SystemProperties used before are not removed and
used in case the DeviceConfig flag for enabling the feature is false and the
sysprop is set.

Manual testing:
1. Set the required flags (adb shell device_config put
package_manager_service downgrade_unused_apps_enabled "true" and adb shell
device_config put package_manager_service inactive_app_threshold_days "10")
2. Device should have less than 1 GB free space (use fallocate to create big
files)
3. Device has an app not used in the last 10 days: e.g. com.facebook.katana
4. Check compilation filter using adb shell dumpsys package com.facebook.katana
(should be speed-profile)
5. Run adb shell cmd package bg-dexopt-job
6. Check again compilation filter, should be verify

Bug: 139047287
Test: Integration tests added and manual testing
Change-Id: I874a0f82e9488fe1334b6c021cec865f4274b15a
diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
index c712431..08e55d3 100644
--- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java
+++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java
@@ -34,6 +34,8 @@
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.storage.StorageManager;
+import android.provider.DeviceConfig;
+import android.text.TextUtils;
 import android.util.ArraySet;
 import android.util.Log;
 import android.util.StatsLog;
@@ -84,6 +86,12 @@
 
     // Used for calculating space threshold for downgrading unused apps.
     private static final int LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE = 2;
+    private static final int DEFAULT_INACTIVE_APP_THRESHOLD_DAYS = 10;
+
+    private static final String DOWNGRADE_UNUSED_APPS_ENABLED = "downgrade_unused_apps_enabled";
+    private static final String INACTIVE_APP_THRESHOLD_DAYS = "inactive_app_threshold_days";
+    private static final String LOW_STORAGE_MULTIPLIER_FOR_DOWNGRADE =
+            "low_storage_threshold_multiplier_for_downgrade";
 
     /**
      * Set of failed packages remembered across job runs.
@@ -103,8 +111,6 @@
     private final AtomicBoolean mExitPostBootUpdate = new AtomicBoolean(false);
 
     private final File mDataDir = Environment.getDataDirectory();
-    private static final long mDowngradeUnusedAppsThresholdInMillis =
-            getDowngradeUnusedAppsThresholdInMillis();
 
     public static void schedule(Context context) {
         if (isBackgroundDexoptDisabled()) {
@@ -346,14 +352,14 @@
             // Only downgrade apps when space is low on device.
             // Threshold is selected above the lowStorageThreshold so that we can pro-actively clean
             // up disk before user hits the actual lowStorageThreshold.
-            final long lowStorageThresholdForDowngrade = LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE
+            final long lowStorageThresholdForDowngrade = getLowThresholdMultiplierForDowngrade()
                     * lowStorageThreshold;
             boolean shouldDowngrade = shouldDowngrade(lowStorageThresholdForDowngrade);
             Log.d(TAG, "Should Downgrade " + shouldDowngrade);
             if (shouldDowngrade) {
                 Set<String> unusedPackages =
-                        pm.getUnusedPackages(mDowngradeUnusedAppsThresholdInMillis);
-                Log.d(TAG, "Unsused Packages " +  String.join(",", unusedPackages));
+                        pm.getUnusedPackages(getDowngradeUnusedAppsThresholdInMillis());
+                Log.d(TAG, "Unused Packages " +  String.join(",", unusedPackages));
 
                 if (!unusedPackages.isEmpty()) {
                     for (String pkg : unusedPackages) {
@@ -362,12 +368,9 @@
                             // Should be aborted by the scheduler.
                             return abortCode;
                         }
-                        if (downgradePackage(pm, pkg, /*isForPrimaryDex*/ true)) {
+                        if (downgradePackage(pm, pkg)) {
                             updatedPackages.add(pkg);
                         }
-                        if (supportSecondaryDex) {
-                            downgradePackage(pm, pkg, /*isForPrimaryDex*/ false);
-                        }
                     }
 
                     pkgs = new ArraySet<>(pkgs);
@@ -415,39 +418,45 @@
      * Try to downgrade the package to a smaller compilation filter.
      * eg. if the package is in speed-profile the package will be downgraded to verify.
      * @param pm PackageManagerService
-     * @param pkg The package to be downgraded.
-     * @param isForPrimaryDex. Apps can have several dex file, primary and secondary.
-     * @return true if the package was downgraded.
+     * @param pkg The package to be downgraded
+     * @return true if the package was downgraded
      */
-    private boolean downgradePackage(PackageManagerService pm, String pkg,
-            boolean isForPrimaryDex) {
+    private boolean downgradePackage(PackageManagerService pm, String pkg) {
         Log.d(TAG, "Downgrading " + pkg);
-        boolean dex_opt_performed = false;
+        boolean downgradedPrimary = false;
         int reason = PackageManagerService.REASON_INACTIVE_PACKAGE_DOWNGRADE;
         int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE
                 | DexoptOptions.DEXOPT_IDLE_BACKGROUND_JOB
                 | DexoptOptions.DEXOPT_DOWNGRADE;
+
         long package_size_before = getPackageSize(pm, pkg);
-
-        if (isForPrimaryDex) {
-            // This applies for system apps or if packages location is not a directory, i.e.
-            // monolithic install.
-            if (!pm.canHaveOatDir(pkg)) {
-                // For apps that don't have the oat directory, instead of downgrading,
-                // remove their compiler artifacts from dalvik cache.
-                pm.deleteOatArtifactsOfPackage(pkg);
-            } else {
-                dex_opt_performed = performDexOptPrimary(pm, pkg, reason, dexoptFlags);
-            }
+        // An aggressive downgrade deletes the oat files.
+        boolean aggressive = false;
+        // This applies for system apps or if packages location is not a directory, i.e.
+        // monolithic install.
+        if (!pm.canHaveOatDir(pkg)) {
+            // For apps that don't have the oat directory, instead of downgrading,
+            // remove their compiler artifacts from dalvik cache.
+            pm.deleteOatArtifactsOfPackage(pkg);
+            aggressive = true;
+            downgradedPrimary = true;
         } else {
-            dex_opt_performed = performDexOptSecondary(pm, pkg, reason, dexoptFlags);
+            downgradedPrimary = performDexOptPrimary(pm, pkg, reason, dexoptFlags);
+
+            if (supportSecondaryDex()) {
+                performDexOptSecondary(pm, pkg, reason, dexoptFlags);
+            }
         }
 
-        if (dex_opt_performed) {
+        // This metric aims to log the storage savings when downgrading.
+        // The way disk size is measured using getPackageSize only looks at the primary apks.
+        // Any logs that are due to secondary dex files will show 0% size reduction and pollute
+        // the metrics.
+        if (downgradedPrimary) {
             StatsLog.write(StatsLog.APP_DOWNGRADED, pkg, package_size_before,
-                    getPackageSize(pm, pkg), /*aggressive=*/ false);
+                    getPackageSize(pm, pkg), aggressive);
         }
-        return dex_opt_performed;
+        return downgradedPrimary;
     }
 
     private boolean supportSecondaryDex() {
@@ -471,7 +480,7 @@
      * concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized.
      * @param pm An instance of PackageManagerService
      * @param pkg The package to be downgraded.
-     * @param isForPrimaryDex. Apps can have several dex file, primary and secondary.
+     * @param isForPrimaryDex Apps can have several dex file, primary and secondary.
      * @return true if the package was downgraded.
      */
     private boolean optimizePackage(PackageManagerService pm, String pkg,
@@ -588,12 +597,6 @@
         // the checks above. This check is not "live" - the value is determined by a background
         // restart with a period of ~1 minute.
         PackageManagerService pm = (PackageManagerService)ServiceManager.getService("package");
-        if (pm.isStorageLow()) {
-            if (DEBUG_DEXOPT) {
-                Log.i(TAG, "Low storage, skipping this run");
-            }
-            return false;
-        }
 
         final ArraySet<String> pkgs = pm.getOptimizablePackages();
         if (pkgs.isEmpty()) {
@@ -643,17 +646,77 @@
     }
 
     private static long getDowngradeUnusedAppsThresholdInMillis() {
+        long defaultValue = Long.MAX_VALUE;
+        if (isDowngradeFeatureEnabled()) {
+            return getInactiveAppsThresholdMillis();
+        }
         final String sysPropKey = "pm.dexopt.downgrade_after_inactive_days";
         String sysPropValue = SystemProperties.get(sysPropKey);
         if (sysPropValue == null || sysPropValue.isEmpty()) {
             Log.w(TAG, "SysProp " + sysPropKey + " not set");
-            return Long.MAX_VALUE;
+            return defaultValue;
         }
-        return TimeUnit.DAYS.toMillis(Long.parseLong(sysPropValue));
+        try {
+            return TimeUnit.DAYS.toMillis(Long.parseLong(sysPropValue));
+        } catch (NumberFormatException e) {
+            Log.w(TAG, "Couldn't parse long for pm.dexopt.downgrade_after_inactive_days: "
+                    + sysPropValue + ". Returning default value instead.");
+            return defaultValue;
+        }
     }
 
     private static boolean isBackgroundDexoptDisabled() {
         return SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt" /* key */,
                 false /* default */);
     }
+
+    private static boolean isDowngradeFeatureEnabled() {
+        // DeviceConfig enables the control of on device features via remotely configurable flags,
+        // compared to SystemProperties which is only a way of sharing info system-widely, but are
+        // not configurable on the server-side.
+        String downgradeUnusedAppsEnabledFlag =
+                DeviceConfig.getProperty(
+                        DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                        DOWNGRADE_UNUSED_APPS_ENABLED);
+        return !TextUtils.isEmpty(downgradeUnusedAppsEnabledFlag)
+                && Boolean.parseBoolean(downgradeUnusedAppsEnabledFlag);
+    }
+
+    private static long getInactiveAppsThresholdMillis() {
+        long defaultValue = TimeUnit.DAYS.toMillis(DEFAULT_INACTIVE_APP_THRESHOLD_DAYS);
+        String inactiveAppThresholdDaysFlag =
+                DeviceConfig.getProperty(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                        INACTIVE_APP_THRESHOLD_DAYS);
+        if (!TextUtils.isEmpty(inactiveAppThresholdDaysFlag)) {
+            try {
+                return TimeUnit.DAYS.toMillis(Long.parseLong(inactiveAppThresholdDaysFlag));
+            } catch (NumberFormatException e) {
+                Log.w(TAG, "Couldn't parse long for " + INACTIVE_APP_THRESHOLD_DAYS + " flag: "
+                        + inactiveAppThresholdDaysFlag + ". Returning default value instead.");
+                return defaultValue;
+            }
+        }
+        return defaultValue;
+    }
+
+    private static int getLowThresholdMultiplierForDowngrade() {
+        int defaultValue = LOW_THRESHOLD_MULTIPLIER_FOR_DOWNGRADE;
+        if (isDowngradeFeatureEnabled()) {
+            String lowStorageThresholdMultiplierFlag =
+                    DeviceConfig.getProperty(DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                            LOW_STORAGE_MULTIPLIER_FOR_DOWNGRADE);
+            if (!TextUtils.isEmpty(lowStorageThresholdMultiplierFlag)) {
+                try {
+                    return Integer.parseInt(lowStorageThresholdMultiplierFlag);
+                } catch (NumberFormatException e) {
+                    Log.w(TAG, "Couldn't parse long for "
+                            + LOW_STORAGE_MULTIPLIER_FOR_DOWNGRADE + " flag: "
+                            + lowStorageThresholdMultiplierFlag
+                            + ". Returning default value instead.");
+                    return defaultValue;
+                }
+            }
+        }
+        return defaultValue;
+    }
 }
diff --git a/tests/BackgroundDexOptServiceIntegrationTests/AndroidManifest.xml b/tests/BackgroundDexOptServiceIntegrationTests/AndroidManifest.xml
index aec9f77..7291dc7 100644
--- a/tests/BackgroundDexOptServiceIntegrationTests/AndroidManifest.xml
+++ b/tests/BackgroundDexOptServiceIntegrationTests/AndroidManifest.xml
@@ -28,6 +28,8 @@
     <uses-permission android:name="android.permission.SET_TIME" />
     <uses-permission android:name="android.permission.UPDATE_DEVICE_STATS" />
     <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" />
+    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
 
     <application>
         <uses-library android:name="android.test.runner" />
diff --git a/tests/BackgroundDexOptServiceIntegrationTests/src/com/android/server/pm/BackgroundDexOptServiceIntegrationTests.java b/tests/BackgroundDexOptServiceIntegrationTests/src/com/android/server/pm/BackgroundDexOptServiceIntegrationTests.java
index 7d826f7..4cd56c3 100644
--- a/tests/BackgroundDexOptServiceIntegrationTests/src/com/android/server/pm/BackgroundDexOptServiceIntegrationTests.java
+++ b/tests/BackgroundDexOptServiceIntegrationTests/src/com/android/server/pm/BackgroundDexOptServiceIntegrationTests.java
@@ -22,6 +22,7 @@
 import android.os.ParcelFileDescriptor;
 import android.os.SystemProperties;
 import android.os.storage.StorageManager;
+import android.provider.DeviceConfig;
 import android.util.Log;
 
 import androidx.test.InstrumentationRegistry;
@@ -30,7 +31,9 @@
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
@@ -52,6 +55,13 @@
  * 3. Under low storage conditions and package is recently used, check
  * that dexopt upgrades test app to $(getprop pm.dexopt.bg-dexopt).
  *
+ * When downgrade feature is on (downgrade_unused_apps_enabled flag is set to true):
+ * 4  On low storage, check that the inactive packages are downgraded.
+ * 5. On low storage, check that used packages are upgraded.
+ * 6. On storage completely full, dexopt fails.
+ * 7. Not on low storage, unused packages are upgraded.
+ * 8. Low storage, unused app is downgraded. When app is used again, app is upgraded.
+ *
  * Each test case runs "cmd package bg-dexopt-job com.android.frameworks.bgdexopttest".
  *
  * The setup for these tests make sure this package has been configured to have been recently used
@@ -59,6 +69,10 @@
  * recently used, it sets the time forward more than
  * `getprop pm.dexopt.downgrade_after_inactive_days` days.
  *
+ * For some of the tests, the DeviceConfig flags inactive_app_threshold_days and
+ * downgrade_unused_apps_enabled are set. These turn on/off the downgrade unused apps feature for
+ * all devices and set the time threshold for unused apps.
+ *
  * For tests that require low storage, the phone is filled up.
  *
  * Run with "atest BackgroundDexOptServiceIntegrationTests".
@@ -80,10 +94,14 @@
             "pm.dexopt.downgrade_after_inactive_days", 0);
     // Needs to be between 1.0 and 2.0.
     private static final double LOW_STORAGE_MULTIPLIER = 1.5;
+    private static final int DOWNGRADE_FEATURE_PKG_INACTIVE_AFTER_DAYS = 15;
 
     // The file used to fill up storage.
     private File mBigFile;
 
+    @Rule
+    public ExpectedException mExpectedException = ExpectedException.none();
+
     // Remember start time.
     @BeforeClass
     public static void setUpAll() {
@@ -196,11 +214,27 @@
         logSpaceRemaining();
     }
 
+    private void fillUpStorageCompletely() throws IOException {
+        fillUpStorage((getStorageLowBytes()));
+    }
+
     // Fill up storage so that device is in low storage condition.
     private void fillUpToLowStorage() throws IOException {
         fillUpStorage((long) (getStorageLowBytes() * LOW_STORAGE_MULTIPLIER));
     }
 
+    private void setInactivePackageThreshold(int threshold) {
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                "inactive_app_threshold_days", Integer.toString(threshold), false);
+    }
+
+    private void enableDowngradeFeature(boolean enabled) {
+        DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE,
+                "downgrade_unused_apps_enabled", Boolean.toString(enabled), false);
+    }
+
     // TODO(aeubanks): figure out how to get scheduled bg-dexopt to run
     private static void runBackgroundDexOpt() throws IOException {
         String result = runShellCommand("cmd package bg-dexopt-job " + PACKAGE_NAME);
@@ -244,7 +278,7 @@
 
     // Test that background dexopt under normal conditions succeeds.
     @Test
-    public void testBackgroundDexOpt() throws IOException {
+    public void testBackgroundDexOpt_normalConditions_dexOptSucceeds() throws IOException {
         // Set filter to quicken.
         compilePackageWithFilter(PACKAGE_NAME, "verify");
         Assert.assertEquals("verify", getCompilerFilter(PACKAGE_NAME));
@@ -257,17 +291,16 @@
 
     // Test that background dexopt under low storage conditions upgrades used packages.
     @Test
-    public void testBackgroundDexOptDowngradeSkipRecentlyUsedPackage() throws IOException {
+    public void testBackgroundDexOpt_lowStorage_usedPkgsUpgraded() throws IOException {
         // Should be less than DOWNGRADE_AFTER_DAYS.
         long deltaDays = DOWNGRADE_AFTER_DAYS - 1;
         try {
+            enableDowngradeFeature(false);
             // Set time to future.
             setTimeFutureDays(deltaDays);
-
             // Set filter to quicken.
             compilePackageWithFilter(PACKAGE_NAME, "quicken");
             Assert.assertEquals("quicken", getCompilerFilter(PACKAGE_NAME));
-
             // Fill up storage to trigger low storage threshold.
             fillUpToLowStorage();
 
@@ -282,18 +315,20 @@
     }
 
     // Test that background dexopt under low storage conditions downgrades unused packages.
+    // This happens if the system property pm.dexopt.downgrade_after_inactive_days is set
+    // (e.g. on Android Go devices).
     @Test
-    public void testBackgroundDexOptDowngradeSuccessful() throws IOException {
+    public void testBackgroundDexOpt_lowStorage_unusedPkgsDowngraded()
+            throws IOException {
         // Should be more than DOWNGRADE_AFTER_DAYS.
         long deltaDays = DOWNGRADE_AFTER_DAYS + 1;
         try {
+            enableDowngradeFeature(false);
             // Set time to future.
             setTimeFutureDays(deltaDays);
-
             // Set filter to quicken.
             compilePackageWithFilter(PACKAGE_NAME, "quicken");
             Assert.assertEquals("quicken", getCompilerFilter(PACKAGE_NAME));
-
             // Fill up storage to trigger low storage threshold.
             fillUpToLowStorage();
 
@@ -307,4 +342,134 @@
         }
     }
 
+    // Test that the background dexopt downgrades inactive packages when the downgrade feature is
+    // enabled.
+    @Test
+    public void testBackgroundDexOpt_downgradeFeatureEnabled_lowStorage_inactivePkgsDowngraded()
+            throws IOException {
+        // Should be more than DOWNGRADE_FEATURE_PKG_INACTIVE_AFTER_DAYS.
+        long deltaDays = DOWNGRADE_FEATURE_PKG_INACTIVE_AFTER_DAYS + 1;
+        try {
+            enableDowngradeFeature(true);
+            setInactivePackageThreshold(DOWNGRADE_FEATURE_PKG_INACTIVE_AFTER_DAYS);
+            // Set time to future.
+            setTimeFutureDays(deltaDays);
+            // Set filter to quicken.
+            compilePackageWithFilter(PACKAGE_NAME, "quicken");
+            Assert.assertEquals("quicken", getCompilerFilter(PACKAGE_NAME));
+            // Fill up storage to trigger low storage threshold.
+            fillUpToLowStorage();
+
+            runBackgroundDexOpt();
+
+            // Verify that downgrade is successful.
+            Assert.assertEquals(DOWNGRADE_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME));
+        } finally {
+            // Reset time.
+            setTimeFutureDays(-deltaDays);
+        }
+    }
+
+    // Test that the background dexopt upgrades used packages when the downgrade feature is enabled.
+    // This test doesn't fill the device storage completely, but to a multiplier of the low storage
+    // threshold and this is why apps can still be optimized.
+    @Test
+    public void testBackgroundDexOpt_downgradeFeatureEnabled_lowStorage_usedPkgsUpgraded()
+            throws IOException {
+        enableDowngradeFeature(true);
+        // Set filter to quicken.
+        compilePackageWithFilter(PACKAGE_NAME, "quicken");
+        Assert.assertEquals("quicken", getCompilerFilter(PACKAGE_NAME));
+        // Fill up storage to trigger low storage threshold.
+        fillUpToLowStorage();
+
+        runBackgroundDexOpt();
+
+        /// Verify that bg-dexopt is successful in upgrading the used packages.
+        Assert.assertEquals(BG_DEXOPT_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME));
+    }
+
+    // Test that the background dexopt fails and doesn't change the compilation filter of used
+    // packages when the downgrade feature is enabled and the storage is filled up completely.
+    // The bg-dexopt shouldn't optimise nor downgrade these packages.
+    @Test
+    public void testBackgroundDexOpt_downgradeFeatureEnabled_fillUpStorageCompletely_dexOptFails()
+            throws IOException {
+        enableDowngradeFeature(true);
+        String previousCompilerFilter = getCompilerFilter(PACKAGE_NAME);
+
+        // Fill up storage completely, without using a multiplier for the low storage threshold.
+        fillUpStorageCompletely();
+
+        // When the bg dexopt runs with the storage filled up completely, it will fail.
+        mExpectedException.expect(IllegalStateException.class);
+        runBackgroundDexOpt();
+
+        /// Verify that bg-dexopt doesn't change the compilation filter of used apps.
+        Assert.assertEquals(previousCompilerFilter, getCompilerFilter(PACKAGE_NAME));
+    }
+
+    // Test that the background dexopt upgrades the unused packages when the downgrade feature is
+    // on if the device is not low on storage.
+    @Test
+    public void testBackgroundDexOpt_downgradeFeatureEnabled_notLowStorage_unusedPkgsUpgraded()
+            throws IOException {
+        // Should be more than DOWNGRADE_FEATURE_PKG_INACTIVE_AFTER_DAYS.
+        long deltaDays = DOWNGRADE_FEATURE_PKG_INACTIVE_AFTER_DAYS + 1;
+        try {
+            enableDowngradeFeature(true);
+            setInactivePackageThreshold(DOWNGRADE_FEATURE_PKG_INACTIVE_AFTER_DAYS);
+            // Set time to future.
+            setTimeFutureDays(deltaDays);
+            // Set filter to quicken.
+            compilePackageWithFilter(PACKAGE_NAME, "quicken");
+            Assert.assertEquals("quicken", getCompilerFilter(PACKAGE_NAME));
+
+            runBackgroundDexOpt();
+
+            // Verify that bg-dexopt is successful in upgrading the unused packages when the device
+            // is not low on storage.
+            Assert.assertEquals(BG_DEXOPT_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME));
+        } finally {
+            // Reset time.
+            setTimeFutureDays(-deltaDays);
+        }
+    }
+
+    // Test that when an unused package (which was downgraded) is used again, it's re-optimized when
+    // bg-dexopt runs again.
+    @Test
+    public void testBackgroundDexOpt_downgradeFeatureEnabled_downgradedPkgsUpgradedAfterUse()
+            throws IOException {
+        // Should be more than DOWNGRADE_FEATURE_PKG_INACTIVE_AFTER_DAYS.
+        long deltaDays = DOWNGRADE_FEATURE_PKG_INACTIVE_AFTER_DAYS + 1;
+        try {
+            enableDowngradeFeature(true);
+            setInactivePackageThreshold(DOWNGRADE_FEATURE_PKG_INACTIVE_AFTER_DAYS);
+            // Set time to future.
+            setTimeFutureDays(deltaDays);
+            // Fill up storage to trigger low storage threshold.
+            fillUpToLowStorage();
+            // Set filter to quicken.
+            compilePackageWithFilter(PACKAGE_NAME, "quicken");
+            Assert.assertEquals("quicken", getCompilerFilter(PACKAGE_NAME));
+
+            runBackgroundDexOpt();
+
+            // Verify that downgrade is successful.
+            Assert.assertEquals(DOWNGRADE_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME));
+
+            // Reset time.
+            setTimeFutureDays(-deltaDays);
+            deltaDays = 0;
+            runBackgroundDexOpt();
+
+            // Verify that bg-dexopt is successful in upgrading the unused packages that were used
+            // again.
+            Assert.assertEquals(BG_DEXOPT_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME));
+        } finally {
+            // Reset time.
+            setTimeFutureDays(-deltaDays);
+        }
+    }
 }