1 /*
2  * Copyright (C) 2014 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.compatibility.common.util;
18 
19 import static org.junit.Assert.assertTrue;
20 import static org.junit.Assert.fail;
21 
22 import android.app.ActivityManager;
23 import android.app.ActivityManager.MemoryInfo;
24 import android.app.Instrumentation;
25 import android.app.UiAutomation;
26 import android.content.Context;
27 import android.os.ParcelFileDescriptor;
28 import android.os.StatFs;
29 import android.util.Log;
30 
31 import androidx.annotation.NonNull;
32 import androidx.test.InstrumentationRegistry;
33 
34 import com.android.modules.utils.build.SdkLevel;
35 
36 import java.io.ByteArrayOutputStream;
37 import java.io.FileInputStream;
38 import java.io.IOException;
39 import java.util.concurrent.Callable;
40 import java.util.concurrent.TimeoutException;
41 import java.util.concurrent.atomic.AtomicReference;
42 import java.util.function.Predicate;
43 
44 public class SystemUtil {
45     private static final String TAG = "CtsSystemUtil";
46     private static final long TIMEOUT_MILLIS = 10000;
47 
getFreeDiskSize(Context context)48     public static long getFreeDiskSize(Context context) {
49         final StatFs statFs = new StatFs(context.getFilesDir().getAbsolutePath());
50         return (long)statFs.getAvailableBlocks() * statFs.getBlockSize();
51     }
52 
getFreeMemory(Context context)53     public static long getFreeMemory(Context context) {
54         final MemoryInfo info = new MemoryInfo();
55         ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryInfo(info);
56         return info.availMem;
57     }
58 
getTotalMemory(Context context)59     public static long getTotalMemory(Context context) {
60         final MemoryInfo info = new MemoryInfo();
61         ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryInfo(info);
62         return info.totalMem;
63     }
64 
65     /**
66      * Executes a shell command using shell user identity, and return the standard output in string
67      * <p>Note: calling this function requires API level 21 or above
68      * @param instrumentation {@link Instrumentation} instance, obtained from a test running in
69      * instrumentation framework
70      * @param cmd the command to run
71      * @return the standard output of the command
72      * @throws Exception
73      */
runShellCommand(Instrumentation instrumentation, String cmd)74     public static String runShellCommand(Instrumentation instrumentation, String cmd)
75             throws IOException {
76         return runShellCommand(instrumentation.getUiAutomation(), cmd);
77     }
78 
79     /**
80      * Executes a shell command using shell user identity, and return the standard output in string
81      * <p>Note: calling this function requires API level 21 or above
82      * @param automation {@link UiAutomation} instance, obtained from a test running in
83      * instrumentation framework
84      * @param cmd the command to run
85      * @return the standard output of the command
86      * @throws Exception
87      */
runShellCommand(UiAutomation automation, String cmd)88     public static String runShellCommand(UiAutomation automation, String cmd)
89             throws IOException {
90         return new String(runShellCommandByteOutput(automation, cmd));
91     }
92 
93     /**
94      * Executes a shell command using shell user identity, and return the standard output as a byte
95      * array
96      * <p>Note: calling this function requires API level 21 or above
97      *
98      * @param automation {@link UiAutomation} instance, obtained from a test running in
99      *                   instrumentation framework
100      * @param cmd        the command to run
101      * @return the standard output of the command as a byte array
102      */
runShellCommandByteOutput(UiAutomation automation, String cmd)103     static byte[] runShellCommandByteOutput(UiAutomation automation, String cmd)
104             throws IOException {
105         checkCommandBeforeRunning(cmd);
106         ParcelFileDescriptor pfd = automation.executeShellCommand(cmd);
107         try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd)) {
108             return FileUtils.readInputStreamFully(fis);
109         }
110     }
111 
checkCommandBeforeRunning(String cmd)112     private static void checkCommandBeforeRunning(String cmd) {
113         Log.v(TAG, "Running command: " + cmd);
114         if (cmd.startsWith("pm grant ") || cmd.startsWith("pm revoke ")) {
115             throw new UnsupportedOperationException("Use UiAutomation.grantRuntimePermission() "
116                     + "or revokeRuntimePermission() directly, which are more robust.");
117         }
118     }
119 
120     /**
121      * Simpler version of {@link #runShellCommand(Instrumentation, String)}.
122      */
runShellCommand(String cmd)123     public static String runShellCommand(String cmd) {
124         try {
125             return runShellCommand(InstrumentationRegistry.getInstrumentation(), cmd);
126         } catch (IOException e) {
127             fail("Failed reading command output: " + e);
128             return "";
129         }
130     }
131 
132     /**
133      * Like {@link #runShellCommand(String)} but throws if anything was printed to stderr on S+, and
134      * delegates to {@link #runShellCommand(String)} on older platforms for compatibility.
135      */
runShellCommandOrThrow(String cmd)136     public static String runShellCommandOrThrow(String cmd) {
137         if (!SdkLevel.isAtLeastS()) {
138             return runShellCommand(cmd);
139         }
140         UiAutomation automation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
141         try {
142             checkCommandBeforeRunning(cmd);
143 
144             ParcelFileDescriptor[] fds = automation.executeShellCommandRwe(cmd);
145             ParcelFileDescriptor fdOut = fds[0];
146             ParcelFileDescriptor fdIn = fds[1];
147             ParcelFileDescriptor fdErr = fds[2];
148 
149             if (fdIn != null) {
150                 try {
151                     // not using stdin
152                     fdIn.close();
153                 } catch (Exception e) {
154                     // Ignore
155                 }
156             }
157 
158             String out;
159             String err;
160             try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdOut)) {
161                 out = new String(FileUtils.readInputStreamFully(fis));
162             }
163             try (FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(fdErr)) {
164                 err = new String(FileUtils.readInputStreamFully(fis));
165             }
166             if (!err.isEmpty()) {
167                 fail("Command failed:\n$ " + cmd +
168                         "\n\nstderr:\n" + err +
169                         "\n\nstdout:\n" + out);
170             }
171             return out;
172         } catch (IOException e) {
173             fail("Failed reading command output: " + e);
174             return "";
175         }
176     }
177 
178     /**
179      * Same as {@link #runShellCommand(String)}, with optionally
180      * check the result using {@code resultChecker}.
181      */
runShellCommand(String cmd, Predicate<String> resultChecker)182     public static String runShellCommand(String cmd, Predicate<String> resultChecker) {
183         final String result = runShellCommand(cmd);
184         if (resultChecker != null) {
185             assertTrue("Assertion failed. Command was: " + cmd + "\n"
186                     + "Output was:\n" + result,
187                     resultChecker.test(result));
188         }
189         return result;
190     }
191 
192     /**
193      * Same as {@link #runShellCommand(String)}, but fails if the output is not empty.
194      */
runShellCommandForNoOutput(String cmd)195     public static String runShellCommandForNoOutput(String cmd) {
196         final String result = runShellCommand(cmd);
197         assertTrue("Command failed. Command was: " + cmd + "\n"
198                 + "Didn't expect any output, but the output was:\n" + result,
199                 result.length() == 0);
200         return result;
201     }
202 
203     /**
204      * Runs a command and print the result on logcat.
205      */
runCommandAndPrintOnLogcat(String logtag, String cmd)206     public static void runCommandAndPrintOnLogcat(String logtag, String cmd) {
207         Log.i(logtag, "Executing: " + cmd);
208         final String output = runShellCommand(cmd);
209         for (String line : output.split("\\n", -1)) {
210             Log.i(logtag, line);
211         }
212     }
213 
214     /**
215      * Runs a command and return the section matching the patterns.
216      *
217      * @see TextUtils#extractSection
218      */
runCommandAndExtractSection(String cmd, String extractionStartRegex, boolean startInclusive, String extractionEndRegex, boolean endInclusive)219     public static String runCommandAndExtractSection(String cmd,
220             String extractionStartRegex, boolean startInclusive,
221             String extractionEndRegex, boolean endInclusive) {
222         return TextUtils.extractSection(runShellCommand(cmd), extractionStartRegex, startInclusive,
223                 extractionEndRegex, endInclusive);
224     }
225 
226     /**
227      * Runs a {@link ThrowingSupplier} adopting Shell's permissions, and returning the result.
228      */
runWithShellPermissionIdentity(@onNull ThrowingSupplier<T> supplier)229     public static <T> T runWithShellPermissionIdentity(@NonNull ThrowingSupplier<T> supplier) {
230         final UiAutomation automan = InstrumentationRegistry.getInstrumentation().getUiAutomation();
231         AtomicReference<T> result = new AtomicReference<>();
232         runWithShellPermissionIdentity(automan, () -> result.set(supplier.get()));
233         return result.get();
234     }
235 
236     /**
237      * Runs a {@link ThrowingSupplier} adopting a subset of Shell's permissions,
238      * and returning the result.
239      */
runWithShellPermissionIdentity(@onNull ThrowingSupplier<T> supplier, String... permissions)240     public static <T> T runWithShellPermissionIdentity(@NonNull ThrowingSupplier<T> supplier,
241             String... permissions) {
242         final UiAutomation automan = InstrumentationRegistry.getInstrumentation().getUiAutomation();
243         AtomicReference<T> result = new AtomicReference<>();
244         runWithShellPermissionIdentity(automan, () -> result.set(supplier.get()), permissions);
245         return result.get();
246     }
247 
248     /**
249      * Runs a {@link ThrowingRunnable} adopting Shell's permissions.
250      */
runWithShellPermissionIdentity(@onNull ThrowingRunnable runnable)251     public static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable runnable) {
252         final UiAutomation automan = InstrumentationRegistry.getInstrumentation().getUiAutomation();
253         runWithShellPermissionIdentity(automan, runnable);
254     }
255 
256     /**
257      * Runs a {@link ThrowingRunnable} adopting a subset of Shell's permissions.
258      */
runWithShellPermissionIdentity(@onNull ThrowingRunnable runnable, String... permissions)259     public static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable runnable,
260             String... permissions) {
261         final UiAutomation automan = InstrumentationRegistry.getInstrumentation().getUiAutomation();
262         runWithShellPermissionIdentity(automan, runnable, permissions);
263     }
264 
265     /**
266      * Runs a {@link ThrowingRunnable} adopting Shell's permissions, where you can specify the
267      * uiAutomation used.
268      */
runWithShellPermissionIdentity( @onNull UiAutomation automan, @NonNull ThrowingRunnable runnable)269     public static void runWithShellPermissionIdentity(
270             @NonNull UiAutomation automan, @NonNull ThrowingRunnable runnable) {
271         runWithShellPermissionIdentity(automan, runnable, null /* permissions */);
272     }
273 
274     /**
275      * Runs a {@link ThrowingRunnable} adopting Shell's permissions, where you can specify the
276      * uiAutomation used.
277      * @param automan UIAutomation to use.
278      * @param runnable The code to run with Shell's identity.
279      * @param permissions A subset of Shell's permissions. Passing {@code null} will use all
280      *                    available permissions.
281      */
runWithShellPermissionIdentity(@onNull UiAutomation automan, @NonNull ThrowingRunnable runnable, String... permissions)282     public static void runWithShellPermissionIdentity(@NonNull UiAutomation automan,
283             @NonNull ThrowingRunnable runnable, String... permissions) {
284         automan.adoptShellPermissionIdentity(permissions);
285         try {
286             runnable.run();
287         } catch (Exception e) {
288             throw new RuntimeException("Caught exception", e);
289         } finally {
290             automan.dropShellPermissionIdentity();
291         }
292     }
293 
294     /**
295      * Calls a {@link Callable} adopting Shell's permissions.
296      */
callWithShellPermissionIdentity(@onNull Callable<T> callable)297     public static <T> T callWithShellPermissionIdentity(@NonNull Callable<T> callable)
298             throws Exception {
299         final UiAutomation automan = InstrumentationRegistry.getInstrumentation().getUiAutomation();
300         automan.adoptShellPermissionIdentity();
301         try {
302             return callable.call();
303         } finally {
304             automan.dropShellPermissionIdentity();
305         }
306     }
307 
308     /**
309      * Calls a {@link Callable} adopting Shell's permissions.
310      *
311      * @param callable The code to call with Shell's identity.
312      * @param permissions A subset of Shell's permissions. Passing {@code null} will use all
313      *                    available permissions.     */
callWithShellPermissionIdentity(@onNull Callable<T> callable, String... permissions)314     public static <T> T callWithShellPermissionIdentity(@NonNull Callable<T> callable,
315             String... permissions) throws Exception {
316         final UiAutomation automan = InstrumentationRegistry.getInstrumentation().getUiAutomation();
317         automan.adoptShellPermissionIdentity(permissions);
318         try {
319             return callable.call();
320         } finally {
321             automan.dropShellPermissionIdentity();
322         }
323     }
324 
325     /**
326      * Make sure that a {@link Runnable} eventually finishes without throwing a {@link
327      * Exception}.
328      *
329      * @param r The {@link Runnable} to run.
330      */
eventually(@onNull ThrowingRunnable r)331     public static void eventually(@NonNull ThrowingRunnable r) {
332         eventually(r, TIMEOUT_MILLIS);
333     }
334 
335     /**
336      * Make sure that a {@link Runnable} eventually finishes without throwing a {@link
337      * Exception}.
338      *
339      * @param r The {@link Runnable} to run.
340      * @param r The number of milliseconds to wait for r to not throw
341      */
eventually(@onNull ThrowingRunnable r, long timeoutMillis)342     public static void eventually(@NonNull ThrowingRunnable r, long timeoutMillis) {
343         long start = System.currentTimeMillis();
344 
345         while (true) {
346             try {
347                 r.run();
348                 return;
349             } catch (Throwable e) {
350                 if (System.currentTimeMillis() - start < timeoutMillis) {
351                     try {
352                         Thread.sleep(100);
353                     } catch (InterruptedException ignored) {
354                         throw new RuntimeException(e);
355                     }
356                 } else {
357                     throw new RuntimeException(e);
358                 }
359             }
360         }
361     }
362 
363     /**
364      * Make sure that a {@link Callable} eventually finishes without throwing a {@link
365      * Exception}.
366      *
367      * @param c The {@link Callable} to run.
368      *
369      * @return The return value of {@code c}
370      */
getEventually(@onNull Callable<T> c)371     public static <T> T getEventually(@NonNull Callable<T> c) throws Exception {
372         return getEventually(c, TIMEOUT_MILLIS);
373     }
374 
375     /**
376      * Make sure that a {@link Callable} eventually finishes without throwing a {@link
377      * Exception}.
378      *
379      * @param c The {@link Callable} to run.
380      * @param timeoutMillis The number of milliseconds to wait for r to not throw
381      *
382      * @return The return value of {@code c}
383      */
getEventually(@onNull Callable<T> c, long timeoutMillis)384     public static <T> T getEventually(@NonNull Callable<T> c, long timeoutMillis) throws Exception {
385         long start = System.currentTimeMillis();
386 
387         while (true) {
388             try {
389                 return c.call();
390             } catch (Throwable e) {
391                 if (System.currentTimeMillis() - start < timeoutMillis) {
392                     try {
393                         Thread.sleep(100);
394                     } catch (InterruptedException ignored) {
395                         throw new RuntimeException(e);
396                     }
397                 } else {
398                     throw e;
399                 }
400             }
401         }
402     }
403 }
404