Add integration tests for BackgroundDexOptService

Add three tests:
1. Under normal conditions, check that dexopt upgrades test app to
$(getprop pm.dexopt.bg-dexopt).
2. Under low storage conditions and package is unused, check
that dexopt downgrades test app to $(getprop pm.dexopt.inactive).
3. Under low storage conditions and package is recently used, check
that dexopt upgrades test app to $(getprop pm.dexopt.bg-dexopt).

Test: atest -v BackgroundDexOptServiceIntegrationTests
BUG: 64807719

Change-Id: Iaa50d5120ea0255b38226bda0452e7e47f1ff5d0
diff --git a/tests/BackgroundDexOptServiceIntegrationTests/Android.mk b/tests/BackgroundDexOptServiceIntegrationTests/Android.mk
new file mode 100644
index 0000000..da1a08b
--- /dev/null
+++ b/tests/BackgroundDexOptServiceIntegrationTests/Android.mk
@@ -0,0 +1,34 @@
+#
+# Copyright (C) 2017 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android-support-test \
+
+LOCAL_PACKAGE_NAME := BackgroundDexOptServiceIntegrationTests
+LOCAL_COMPATIBILITY_SUITE := device-tests
+
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/BackgroundDexOptServiceIntegrationTests/AndroidManifest.xml b/tests/BackgroundDexOptServiceIntegrationTests/AndroidManifest.xml
new file mode 100644
index 0000000..afae155
--- /dev/null
+++ b/tests/BackgroundDexOptServiceIntegrationTests/AndroidManifest.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.frameworks.bgdexopttest">
+
+
+    <!-- Uses API introduced in O (26) -->
+    <uses-sdk
+        android:minSdkVersion="1"
+        android:targetSdkVersion="26" />
+
+    <uses-permission android:name="android.permission.DUMP" />
+    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+    <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" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="android.support.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.frameworks.bgdexopttest"
+        android:label="Integration test for BackgroundDexOptService" />
+</manifest>
diff --git a/tests/BackgroundDexOptServiceIntegrationTests/AndroidTest.xml b/tests/BackgroundDexOptServiceIntegrationTests/AndroidTest.xml
new file mode 100644
index 0000000..9bb1e28
--- /dev/null
+++ b/tests/BackgroundDexOptServiceIntegrationTests/AndroidTest.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<configuration description="Runs BackgroundDexOptService Integration Tests">
+    <!--DeviceSetup should go before TimeSetter because it stops automatic update of time-->
+    <target_preparer
+        class="com.android.tradefed.targetprep.DeviceSetup">
+        <option name="auto-update-time" value="OFF"/>
+        <option name="auto-update-timezone" value="OFF"/>
+        <option name="set-property" key="pm.dexopt.downgrade_after_inactive_days" value="2"/>
+        <option name="set-property" key="pm.dexopt.disable_bg_dexopt" value="true"/>
+        <option name="set-property" key="pm.dexopt.inactive" value="verify"/>
+        <option name="set-property" key="pm.dexopt.bg-dexopt" value="speed"/>
+        <option name="restore-settings" value="true"/>
+        <option name="restore-properties" value="true"/>
+    </target_preparer>
+
+    <!--Test app needs to be installed when we change its settings below-->
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="BackgroundDexOptServiceIntegrationTests.apk"/>
+        <option name="cleanup-apks" value="true"/>
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.SetPackagesRecentlyUsed">
+        <option name="package-recently-used-time" value="0d"/>
+        <option name="package-recently-used-name" value="com.android.frameworks.bgdexopttest"/>
+    </target_preparer>
+
+    <target_preparer class="com.android.tradefed.targetprep.RestartSystemServerTargetPreparer"/>
+
+    <target_preparer class="com.android.tradefed.targetprep.DeviceStorageFiller">
+        <!--32GB-->
+        <!--necessary because a package cannot create a file larger than 100GB-->
+        <option name="free-bytes" value="34359738368"/>
+    </target_preparer>
+
+    <option name="test-suite-tag" value="apct"/>
+    <option name="test-tag" value="BackgroundDexOptServiceIntegrationTests"/>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest">
+        <option name="package" value="com.android.frameworks.bgdexopttest"/>
+        <option name="runner" value="android.support.test.runner.AndroidJUnitRunner"/>
+    </test>
+</configuration>
diff --git a/tests/BackgroundDexOptServiceIntegrationTests/src/com/android/server/pm/BackgroundDexOptServiceIntegrationTests.java b/tests/BackgroundDexOptServiceIntegrationTests/src/com/android/server/pm/BackgroundDexOptServiceIntegrationTests.java
new file mode 100644
index 0000000..3734412
--- /dev/null
+++ b/tests/BackgroundDexOptServiceIntegrationTests/src/com/android/server/pm/BackgroundDexOptServiceIntegrationTests.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.pm;
+
+import android.app.AlarmManager;
+import android.content.Context;
+import android.os.Environment;
+import android.os.SystemProperties;
+import android.os.storage.StorageManager;
+import android.support.test.InstrumentationRegistry;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Integration tests for {@link BackgroundDexOptService}.
+ *
+ * Tests various scenarios around BackgroundDexOptService.
+ * 1. Under normal conditions, check that dexopt upgrades test app to
+ * $(getprop pm.dexopt.bg-dexopt).
+ * 2. Under low storage conditions and package is unused, check
+ * that dexopt downgrades test app to $(getprop pm.dexopt.inactive).
+ * 3. Under low storage conditions and package is recently used, check
+ * that dexopt upgrades test app to $(getprop pm.dexopt.bg-dexopt).
+ *
+ * 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
+ * plus installed far enough in the past. If a test case requires that this package has not been
+ * recently used, it sets the time forward more than
+ * `getprop pm.dexopt.downgrade_after_inactive_days` days.
+ *
+ * For tests that require low storage, the phone is filled up.
+ *
+ * Run with "atest BackgroundDexOptServiceIntegrationTests".
+ */
+@RunWith(JUnit4.class)
+public final class BackgroundDexOptServiceIntegrationTests {
+
+    private static final String TAG = BackgroundDexOptServiceIntegrationTests.class.getSimpleName();
+
+    // Name of package to test on.
+    private static final String PACKAGE_NAME = "com.android.frameworks.bgdexopttest";
+    // Name of file used to fill up storage.
+    private static final String BIG_FILE = "bigfile";
+    private static final String BG_DEXOPT_COMPILER_FILTER = SystemProperties.get(
+            "pm.dexopt.bg-dexopt");
+    private static final String DOWNGRADE_COMPILER_FILTER = SystemProperties.get(
+            "pm.dexopt.inactive");
+    private static final long DOWNGRADE_AFTER_DAYS = SystemProperties.getLong(
+            "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;
+
+    // The file used to fill up storage.
+    private File mBigFile;
+
+    // Remember start time.
+    @BeforeClass
+    public static void setUpAll() {
+        if (!SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false)) {
+            throw new RuntimeException(
+                    "bg-dexopt is not disabled (set pm.dexopt.disable_bg_dexopt to true)");
+        }
+        if (DOWNGRADE_AFTER_DAYS < 1) {
+            throw new RuntimeException(
+                    "pm.dexopt.downgrade_after_inactive_days must be at least 1");
+        }
+        if ("quicken".equals(BG_DEXOPT_COMPILER_FILTER)) {
+            throw new RuntimeException("pm.dexopt.bg-dexopt should not be \"quicken\"");
+        }
+        if ("quicken".equals(DOWNGRADE_COMPILER_FILTER)) {
+            throw new RuntimeException("pm.dexopt.inactive should not be \"quicken\"");
+        }
+    }
+
+
+    private static Context getContext() {
+        return InstrumentationRegistry.getTargetContext();
+    }
+
+    @Before
+    public void setUp() throws IOException {
+        File dataDir = getContext().getDataDir();
+        mBigFile = new File(dataDir, BIG_FILE);
+    }
+
+    @After
+    public void tearDown() {
+        if (mBigFile.exists()) {
+            boolean result = mBigFile.delete();
+            if (!result) {
+                throw new RuntimeException("Couldn't delete big file");
+            }
+        }
+    }
+
+    // Return the content of the InputStream as a String.
+    private static String inputStreamToString(InputStream is) throws IOException {
+        char[] buffer = new char[1024];
+        StringBuilder builder = new StringBuilder();
+        try (InputStreamReader reader = new InputStreamReader(is)) {
+            for (; ; ) {
+                int count = reader.read(buffer, 0, buffer.length);
+                if (count < 0) {
+                    break;
+                }
+                builder.append(buffer, 0, count);
+            }
+        }
+        return builder.toString();
+    }
+
+    // Run the command and return the stdout.
+    private static String runShellCommand(String cmd) throws IOException {
+        Log.i(TAG, String.format("running command: '%s'", cmd));
+        long startTime = System.nanoTime();
+        Process p = Runtime.getRuntime().exec(cmd);
+        int res;
+        try {
+            res = p.waitFor();
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+        String stdout = inputStreamToString(p.getInputStream());
+        String stderr = inputStreamToString(p.getErrorStream());
+        long elapsedTime = System.nanoTime() - startTime;
+        Log.i(TAG, String.format("ran command: '%s' in %d ms with return code %d", cmd,
+                TimeUnit.NANOSECONDS.toMillis(elapsedTime), res));
+        Log.i(TAG, "stdout");
+        Log.i(TAG, stdout);
+        Log.i(TAG, "stderr");
+        Log.i(TAG, stderr);
+        if (res != 0) {
+            throw new RuntimeException(String.format("failed command: '%s'", cmd));
+        }
+        return stdout;
+    }
+
+    // Run the command and return the stdout split by lines.
+    private static String[] runShellCommandSplitLines(String cmd) throws IOException {
+        return runShellCommand(cmd).split("\n");
+    }
+
+    // Return the compiler filter of a package.
+    private static String getCompilerFilter(String pkg) throws IOException {
+        String cmd = String.format("dumpsys package %s", pkg);
+        String[] lines = runShellCommandSplitLines(cmd);
+        final String substr = "compilation_filter=";
+        for (String line : lines) {
+            int startIndex = line.indexOf(substr);
+            if (startIndex < 0) {
+                continue;
+            }
+            startIndex += substr.length();
+            int endIndex = line.indexOf(']', startIndex);
+            return line.substring(startIndex, endIndex);
+        }
+        throw new RuntimeException("Couldn't find compiler filter in dumpsys package");
+    }
+
+    // Return the number of bytes available in the data partition.
+    private static long getDataDirUsableSpace() {
+        return Environment.getDataDirectory().getUsableSpace();
+    }
+
+    // Fill up the storage until there are bytesRemaining number of bytes available in the data
+    // partition. Writes to the current package's data directory.
+    private void fillUpStorage(long bytesRemaining) throws IOException {
+        Log.i(TAG, String.format("Filling up storage with %d bytes remaining", bytesRemaining));
+        logSpaceRemaining();
+        long numBytesToAdd = getDataDirUsableSpace() - bytesRemaining;
+        String cmd = String.format("fallocate -l %d %s", numBytesToAdd, mBigFile.getAbsolutePath());
+        runShellCommand(cmd);
+        logSpaceRemaining();
+    }
+
+    // Fill up storage so that device is in low storage condition.
+    private void fillUpToLowStorage() throws IOException {
+        fillUpStorage((long) (getStorageLowBytes() * LOW_STORAGE_MULTIPLIER));
+    }
+
+    // TODO(aeubanks): figure out how to get scheduled bg-dexopt to run
+    private static void runBackgroundDexOpt() throws IOException {
+        runShellCommand("cmd package bg-dexopt-job " + PACKAGE_NAME);
+    }
+
+    // Set the time ahead of the last use time of the test app in days.
+    private static void setTimeFutureDays(long futureDays) {
+        setTimeFutureMillis(TimeUnit.DAYS.toMillis(futureDays));
+    }
+
+    // Set the time ahead of the last use time of the test app in milliseconds.
+    private static void setTimeFutureMillis(long futureMillis) {
+        long currentTime = System.currentTimeMillis();
+        setTime(currentTime + futureMillis);
+    }
+
+    private static void setTime(long time) {
+        AlarmManager am = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
+        am.setTime(time);
+    }
+
+    // Return the number of free bytes when the data partition is considered low on storage.
+    private static long getStorageLowBytes() {
+        StorageManager storageManager = (StorageManager) getContext().getSystemService(
+                Context.STORAGE_SERVICE);
+        return storageManager.getStorageLowBytes(Environment.getDataDirectory());
+    }
+
+    // Log the amount of space remaining in the data directory.
+    private static void logSpaceRemaining() throws IOException {
+        runShellCommand("df -h /data");
+    }
+
+    // Compile the given package with the given compiler filter.
+    private static void compilePackageWithFilter(String pkg, String filter) throws IOException {
+        runShellCommand(String.format("cmd package compile -f -m %s %s", filter, pkg));
+    }
+
+    // Test that background dexopt under normal conditions succeeds.
+    @Test
+    public void testBackgroundDexOpt() throws IOException {
+        // Set filter to quicken.
+        compilePackageWithFilter(PACKAGE_NAME, "verify");
+        Assert.assertEquals("verify", getCompilerFilter(PACKAGE_NAME));
+
+        runBackgroundDexOpt();
+
+        // Verify that bg-dexopt is successful.
+        Assert.assertEquals(BG_DEXOPT_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME));
+    }
+
+    // Test that background dexopt under low storage conditions upgrades used packages.
+    @Test
+    public void testBackgroundDexOptDowngradeSkipRecentlyUsedPackage() throws IOException {
+        // Should be less than DOWNGRADE_AFTER_DAYS.
+        long deltaDays = DOWNGRADE_AFTER_DAYS - 1;
+        try {
+            // 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 did not happen.
+            Assert.assertEquals(BG_DEXOPT_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME));
+        } finally {
+            // Reset time.
+            setTimeFutureDays(-deltaDays);
+        }
+    }
+
+    // Test that background dexopt under low storage conditions downgrades unused packages.
+    @Test
+    public void testBackgroundDexOptDowngradeSuccessful() throws IOException {
+        // Should be more than DOWNGRADE_AFTER_DAYS.
+        long deltaDays = DOWNGRADE_AFTER_DAYS + 1;
+        try {
+            // 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);
+        }
+    }
+
+}