1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.pm;
18 
19 import static org.junit.Assume.assumeFalse;
20 
21 import android.app.AlarmManager;
22 import android.content.Context;
23 import android.os.Environment;
24 import android.os.ParcelFileDescriptor;
25 import android.os.PowerManager;
26 import android.os.SystemProperties;
27 import android.os.storage.StorageManager;
28 import android.util.Log;
29 
30 import androidx.test.InstrumentationRegistry;
31 
32 import org.junit.After;
33 import org.junit.Assert;
34 import org.junit.Before;
35 import org.junit.BeforeClass;
36 import org.junit.Ignore;
37 import org.junit.Test;
38 import org.junit.runner.RunWith;
39 import org.junit.runners.JUnit4;
40 
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.io.InputStreamReader;
46 import java.util.concurrent.TimeUnit;
47 
48 /**
49  * Integration tests for {@link BackgroundDexOptService}.
50  *
51  * Tests various scenarios around BackgroundDexOptService.
52  * 1. Under normal conditions, check that dexopt upgrades test app to
53  * $(getprop pm.dexopt.bg-dexopt).
54  * 2. Under low storage conditions and package is unused, check
55  * that dexopt downgrades test app to $(getprop pm.dexopt.inactive).
56  * 3. Under low storage conditions and package is recently used, check
57  * that dexopt upgrades test app to $(getprop pm.dexopt.bg-dexopt).
58  *
59  * Each test case runs "cmd package bg-dexopt-job com.android.frameworks.bgdexopttest".
60  *
61  * The setup for these tests make sure this package has been configured to have been recently used
62  * plus installed far enough in the past. If a test case requires that this package has not been
63  * recently used, it sets the time forward more than
64  * `getprop pm.dexopt.downgrade_after_inactive_days` days.
65  *
66  * For tests that require low storage, the phone is filled up.
67  *
68  * Run with "atest BackgroundDexOptServiceIntegrationTests".
69  */
70 @RunWith(JUnit4.class)
71 public final class BackgroundDexOptServiceIntegrationTests {
72 
73     private static final String TAG = BackgroundDexOptServiceIntegrationTests.class.getSimpleName();
74 
75     // Name of package to test on.
76     private static final String PACKAGE_NAME = "com.android.frameworks.bgdexopttest";
77     // Name of file used to fill up storage.
78     private static final String BIG_FILE = "bigfile";
79     private static final String BG_DEXOPT_COMPILER_FILTER = SystemProperties.get(
80             "pm.dexopt.bg-dexopt");
81     private static final String DOWNGRADE_COMPILER_FILTER = SystemProperties.get(
82             "pm.dexopt.inactive");
83     private static final long DOWNGRADE_AFTER_DAYS = SystemProperties.getLong(
84             "pm.dexopt.downgrade_after_inactive_days", 0);
85     // Needs to be between 1.0 and 2.0.
86     private static final double LOW_STORAGE_MULTIPLIER = 1.5;
87 
88     // The file used to fill up storage.
89     private File mBigFile;
90 
91     // Remember start time.
92     @BeforeClass
setUpAll()93     public static void setUpAll() {
94         if (!SystemProperties.getBoolean("pm.dexopt.disable_bg_dexopt", false)) {
95             throw new RuntimeException(
96                     "bg-dexopt is not disabled (set pm.dexopt.disable_bg_dexopt to true)");
97         }
98         if (DOWNGRADE_AFTER_DAYS < 1) {
99             throw new RuntimeException(
100                     "pm.dexopt.downgrade_after_inactive_days must be at least 1");
101         }
102         if ("quicken".equals(BG_DEXOPT_COMPILER_FILTER)) {
103             throw new RuntimeException("pm.dexopt.bg-dexopt should not be \"quicken\"");
104         }
105         if ("quicken".equals(DOWNGRADE_COMPILER_FILTER)) {
106             throw new RuntimeException("pm.dexopt.inactive should not be \"quicken\"");
107         }
108     }
109 
110 
getContext()111     private static Context getContext() {
112         return InstrumentationRegistry.getTargetContext();
113     }
114 
115     @Before
setUp()116     public void setUp() throws IOException {
117         assumeFalse(SystemProperties.getBoolean("dalvik.vm.useartservice", false));
118         File dataDir = getContext().getDataDir();
119         mBigFile = new File(dataDir, BIG_FILE);
120     }
121 
122     @After
tearDown()123     public void tearDown() {
124         if (mBigFile.exists()) {
125             boolean result = mBigFile.delete();
126             if (!result) {
127                 throw new RuntimeException("Couldn't delete big file");
128             }
129         }
130     }
131 
132     // Return the content of the InputStream as a String.
inputStreamToString(InputStream is)133     private static String inputStreamToString(InputStream is) throws IOException {
134         char[] buffer = new char[1024];
135         StringBuilder builder = new StringBuilder();
136         try (InputStreamReader reader = new InputStreamReader(is)) {
137             for (; ; ) {
138                 int count = reader.read(buffer, 0, buffer.length);
139                 if (count < 0) {
140                     break;
141                 }
142                 builder.append(buffer, 0, count);
143             }
144         }
145         return builder.toString();
146     }
147 
148     // Run the command and return the stdout.
runShellCommand(String cmd)149     private static String runShellCommand(String cmd) throws IOException {
150         Log.i(TAG, String.format("running command: '%s'", cmd));
151         ParcelFileDescriptor pfd = InstrumentationRegistry.getInstrumentation().getUiAutomation()
152                 .executeShellCommand(cmd);
153         byte[] buf = new byte[512];
154         int bytesRead;
155         FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
156         StringBuilder stdout = new StringBuilder();
157         while ((bytesRead = fis.read(buf)) != -1) {
158             stdout.append(new String(buf, 0, bytesRead));
159         }
160         fis.close();
161         Log.i(TAG, "stdout");
162         Log.i(TAG, stdout.toString());
163         return stdout.toString();
164     }
165 
166     // Run the command and return the stdout split by lines.
runShellCommandSplitLines(String cmd)167     private static String[] runShellCommandSplitLines(String cmd) throws IOException {
168         return runShellCommand(cmd).split("\n");
169     }
170 
171     // Return the compiler filter of a package.
getCompilerFilter(String pkg)172     private static String getCompilerFilter(String pkg) throws IOException {
173         String cmd = String.format("dumpsys package %s", pkg);
174         String[] lines = runShellCommandSplitLines(cmd);
175         final String substr = "[status=";
176         for (String line : lines) {
177             int startIndex = line.indexOf(substr);
178             if (startIndex < 0) {
179                 continue;
180             }
181             startIndex += substr.length();
182             int endIndex = line.indexOf(']', startIndex);
183             return line.substring(startIndex, endIndex);
184         }
185         throw new RuntimeException("Couldn't find compiler filter in dumpsys package");
186     }
187 
188     // Return the number of bytes available in the data partition.
getDataDirUsableSpace()189     private static long getDataDirUsableSpace() {
190         return Environment.getDataDirectory().getUsableSpace();
191     }
192 
193     // Fill up the storage until there are bytesRemaining number of bytes available in the data
194     // partition. Writes to the current package's data directory.
fillUpStorage(long bytesRemaining)195     private void fillUpStorage(long bytesRemaining) throws IOException {
196         Log.i(TAG, String.format("Filling up storage with %d bytes remaining", bytesRemaining));
197         logSpaceRemaining();
198         long numBytesToAdd = getDataDirUsableSpace() - bytesRemaining;
199         String cmd = String.format("fallocate -l %d %s", numBytesToAdd, mBigFile.getAbsolutePath());
200         runShellCommand(cmd);
201         logSpaceRemaining();
202     }
203 
204     // Fill up storage so that device is in low storage condition.
fillUpToLowStorage()205     private void fillUpToLowStorage() throws IOException {
206         fillUpStorage((long) (getStorageLowBytes() * LOW_STORAGE_MULTIPLIER));
207     }
208 
runBackgroundDexOpt()209     private static void runBackgroundDexOpt() throws IOException {
210         runBackgroundDexOpt("Success");
211     }
212 
213     // TODO(aeubanks): figure out how to get scheduled bg-dexopt to run
runBackgroundDexOpt(String expectedStatus)214     private static void runBackgroundDexOpt(String expectedStatus) throws IOException {
215         String result = runShellCommand("cmd package bg-dexopt-job " + PACKAGE_NAME);
216         if (!result.trim().equals(expectedStatus)) {
217             throw new IllegalStateException("Expected status: " + expectedStatus
218                 + "; Received: " + result.trim());
219         }
220     }
221 
222     // Set the time ahead of the last use time of the test app in days.
setTimeFutureDays(long futureDays)223     private static void setTimeFutureDays(long futureDays) {
224         setTimeFutureMillis(TimeUnit.DAYS.toMillis(futureDays));
225     }
226 
227     // Set the time ahead of the last use time of the test app in milliseconds.
setTimeFutureMillis(long futureMillis)228     private static void setTimeFutureMillis(long futureMillis) {
229         long currentTime = System.currentTimeMillis();
230         setTime(currentTime + futureMillis);
231     }
232 
setTime(long time)233     private static void setTime(long time) {
234         AlarmManager am = (AlarmManager) getContext().getSystemService(Context.ALARM_SERVICE);
235         am.setTime(time);
236     }
237 
238     // Return the number of free bytes when the data partition is considered low on storage.
getStorageLowBytes()239     private static long getStorageLowBytes() {
240         StorageManager storageManager = (StorageManager) getContext().getSystemService(
241                 Context.STORAGE_SERVICE);
242         return storageManager.getStorageLowBytes(Environment.getDataDirectory());
243     }
244 
245     // Log the amount of space remaining in the data directory.
logSpaceRemaining()246     private static void logSpaceRemaining() throws IOException {
247         runShellCommand("df -h /data");
248     }
249 
250     // Compile the given package with the given compiler filter.
compilePackageWithFilter(String pkg, String filter)251     private static void compilePackageWithFilter(String pkg, String filter) throws IOException {
252         runShellCommand(String.format("cmd package compile -f -m %s %s", filter, pkg));
253     }
254 
255     // Override the thermal status of the device
overrideThermalStatus(int status)256     public static void overrideThermalStatus(int status) throws IOException {
257         runShellCommand("cmd thermalservice override-status " + status);
258     }
259 
260     // Reset the thermal status of the device
resetThermalStatus()261     public static void resetThermalStatus() throws IOException {
262         runShellCommand("cmd thermalservice reset");
263     }
264 
265     // Test that background dexopt under normal conditions succeeds.
266     @Test
testBackgroundDexOpt()267     public void testBackgroundDexOpt() throws IOException {
268         // Set filter to quicken.
269         compilePackageWithFilter(PACKAGE_NAME, "verify");
270         Assert.assertEquals("verify", getCompilerFilter(PACKAGE_NAME));
271 
272         runBackgroundDexOpt();
273 
274         // Verify that bg-dexopt is successful.
275         Assert.assertEquals(BG_DEXOPT_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME));
276     }
277 
278     // Test that background dexopt under low storage conditions upgrades used packages.
279     @Test
testBackgroundDexOptDowngradeSkipRecentlyUsedPackage()280     public void testBackgroundDexOptDowngradeSkipRecentlyUsedPackage() throws IOException {
281         // Should be less than DOWNGRADE_AFTER_DAYS.
282         long deltaDays = DOWNGRADE_AFTER_DAYS - 1;
283         try {
284             // Set time to future.
285             setTimeFutureDays(deltaDays);
286 
287             // Set filter to verify.
288             compilePackageWithFilter(PACKAGE_NAME, "verify");
289             Assert.assertEquals("verify", getCompilerFilter(PACKAGE_NAME));
290 
291             // Fill up storage to trigger low storage threshold.
292             fillUpToLowStorage();
293 
294             runBackgroundDexOpt();
295 
296             // Verify that downgrade did not happen.
297             Assert.assertEquals(BG_DEXOPT_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME));
298         } finally {
299             // Reset time.
300             setTimeFutureDays(-deltaDays);
301         }
302     }
303 
304     // Test that background dexopt under low storage conditions downgrades unused packages.
305     @Test
306     @Ignore("b/251438180: This test has been failing for a long time; temporarily disable it while"
307             + " we investigate this issue.")
testBackgroundDexOptDowngradeSuccessful()308     public void testBackgroundDexOptDowngradeSuccessful() throws IOException {
309         // Should be more than DOWNGRADE_AFTER_DAYS.
310         long deltaDays = DOWNGRADE_AFTER_DAYS + 1;
311         try {
312             // Set time to future.
313             setTimeFutureDays(deltaDays);
314 
315             // Set filter to speed-profile.
316             compilePackageWithFilter(PACKAGE_NAME, "speed-profile");
317             Assert.assertEquals("speed-profile", getCompilerFilter(PACKAGE_NAME));
318 
319             // Fill up storage to trigger low storage threshold.
320             fillUpToLowStorage();
321 
322             runBackgroundDexOpt();
323 
324             // Verify that downgrade is successful.
325             Assert.assertEquals(DOWNGRADE_COMPILER_FILTER, getCompilerFilter(PACKAGE_NAME));
326         } finally {
327             // Reset time.
328             setTimeFutureDays(-deltaDays);
329         }
330     }
331 
332     // Test that background dexopt job doesn't trigger if the device is under thermal throttling.
333     @Test
testBackgroundDexOptThermalThrottling()334     public void testBackgroundDexOptThermalThrottling() throws IOException {
335         try {
336             compilePackageWithFilter(PACKAGE_NAME, "verify");
337             overrideThermalStatus(PowerManager.THERMAL_STATUS_MODERATE);
338             // The bgdexopt task should fail when onStartJob is run
339             runBackgroundDexOpt("Failure");
340             Assert.assertEquals("verify", getCompilerFilter(PACKAGE_NAME));
341         } finally {
342             resetThermalStatus();
343         }
344     }
345 }
346