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