1 /*
2  * Copyright (C) 2020 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 android.cts.statsdatom.lib;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import android.service.battery.BatteryServiceDumpProto;
23 
24 import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
25 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
26 import com.android.ddmlib.testrunner.TestResult.TestStatus;
27 import com.android.internal.os.StatsdConfigProto.StatsdConfig;
28 import com.android.tradefed.build.IBuildInfo;
29 import com.android.tradefed.device.CollectingByteOutputReceiver;
30 import com.android.tradefed.device.DeviceNotAvailableException;
31 import com.android.tradefed.device.ITestDevice;
32 import com.android.tradefed.log.LogUtil;
33 import com.android.tradefed.log.LogUtil.CLog;
34 import com.android.tradefed.result.CollectingTestListener;
35 import com.android.tradefed.result.TestDescription;
36 import com.android.tradefed.result.TestResult;
37 import com.android.tradefed.result.TestRunResult;
38 import com.android.tradefed.util.Pair;
39 
40 import com.google.protobuf.InvalidProtocolBufferException;
41 import com.google.protobuf.MessageLite;
42 import com.google.protobuf.Parser;
43 
44 import java.io.FileNotFoundException;
45 import java.util.Map;
46 import javax.annotation.Nonnull;
47 import javax.annotation.Nullable;
48 
49 /**
50  * Contains utility functions for interacting with the device.
51  * Largely copied from incident's ProtoDumpTestCase.
52  */
53 public final class DeviceUtils {
54     public static final String STATSD_ATOM_TEST_APK = "CtsStatsdAtomApp.apk";
55     public static final String STATSD_ATOM_TEST_PKG = "com.android.server.cts.device.statsdatom";
56 
57     private static final String TEST_RUNNER = "androidx.test.runner.AndroidJUnitRunner";
58 
59     private static final String KEY_ACTION = "action";
60 
61     // feature names
62     public static final String FEATURE_WATCH = "android.hardware.type.watch";
63 
64     public static final String DUMP_BATTERY_CMD = "dumpsys battery";
65 
66     /**
67      * Runs device side tests.
68      *
69      * @param device Can be retrieved by running getDevice() in a class that extends DeviceTestCase
70      * @param pkgName Test package name, such as "com.android.server.cts.statsdatom"
71      * @param testClassName Test class name which can either be a fully qualified name or "." + a
72      *     class name; if null, all test in the package will be run
73      * @param testMethodName Test method name; if null, all tests in class or package will be run
74      * @return {@link TestRunResult} of this invocation
75      * @throws DeviceNotAvailableException
76      */
runDeviceTests(ITestDevice device, String pkgName, @Nullable String testClassName, @Nullable String testMethodName)77     public static @Nonnull TestRunResult runDeviceTests(ITestDevice device, String pkgName,
78             @Nullable String testClassName, @Nullable String testMethodName)
79             throws DeviceNotAvailableException {
80         if (testClassName != null && testClassName.startsWith(".")) {
81             testClassName = pkgName + testClassName;
82         }
83 
84         RemoteAndroidTestRunner testRunner = new RemoteAndroidTestRunner(
85                 pkgName, TEST_RUNNER, device.getIDevice());
86         if (testClassName != null && testMethodName != null) {
87             testRunner.setMethodName(testClassName, testMethodName);
88         } else if (testClassName != null) {
89             testRunner.setClassName(testClassName);
90         }
91 
92         CollectingTestListener listener = new CollectingTestListener();
93         assertThat(device.runInstrumentationTests(testRunner, listener)).isTrue();
94 
95         final TestRunResult result = listener.getCurrentRunResults();
96         if (result.isRunFailure()) {
97             throw new Error("Failed to successfully run device tests for "
98                     + result.getName() + ": " + result.getRunFailureMessage());
99         }
100         if (result.getNumTests() == 0) {
101             throw new Error("No tests were run on the device");
102         }
103         if (result.hasFailedTests()) {
104             StringBuilder errorBuilder = new StringBuilder("On-device tests failed:\n");
105             for (Map.Entry<TestDescription, TestResult> resultEntry :
106                     result.getTestResults().entrySet()) {
107                 if (!resultEntry.getValue().getStatus().equals(TestStatus.PASSED)) {
108                     errorBuilder.append(resultEntry.getKey().toString());
109                     errorBuilder.append(":\n");
110                     errorBuilder.append(resultEntry.getValue().getStackTrace());
111                 }
112             }
113             throw new AssertionError(errorBuilder.toString());
114         }
115         return result;
116     }
117 
118     /**
119      * Runs device side tests from the com.android.server.cts.device.statsdatom package.
120      */
runDeviceTestsOnStatsdApp(ITestDevice device, @Nullable String testClassName, @Nullable String testMethodName)121     public static @Nonnull TestRunResult runDeviceTestsOnStatsdApp(ITestDevice device,
122             @Nullable String testClassName, @Nullable String testMethodName)
123             throws DeviceNotAvailableException {
124         return runDeviceTests(device, STATSD_ATOM_TEST_PKG, testClassName, testMethodName);
125     }
126 
127     /**
128      * Install the statsdatom CTS app to the device.
129      */
installStatsdTestApp(ITestDevice device, IBuildInfo ctsBuildInfo)130     public static void installStatsdTestApp(ITestDevice device, IBuildInfo ctsBuildInfo)
131             throws FileNotFoundException, DeviceNotAvailableException {
132         installTestApp(device, STATSD_ATOM_TEST_APK, STATSD_ATOM_TEST_PKG, ctsBuildInfo);
133     }
134 
135     /**
136      * Install a test app to the device.
137      */
installTestApp(ITestDevice device, String apkName, String pkgName, IBuildInfo ctsBuildInfo)138     public static void installTestApp(ITestDevice device, String apkName, String pkgName,
139             IBuildInfo ctsBuildInfo) throws FileNotFoundException, DeviceNotAvailableException {
140         CLog.d("Installing app " + apkName);
141         CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(ctsBuildInfo);
142         final String result = device.installPackage(
143                 buildHelper.getTestFile(apkName), /*reinstall=*/true, /*grantPermissions=*/true);
144         assertWithMessage("Failed to install " + apkName + ": " + result).that(result).isNull();
145         allowBackgroundServices(device, pkgName);
146     }
147 
148     /**
149      * Required to successfully start a background service from adb, starting in O.
150      */
allowBackgroundServices(ITestDevice device, String pkgName)151     private static void allowBackgroundServices(ITestDevice device, String pkgName)
152             throws DeviceNotAvailableException {
153         String cmd = "cmd deviceidle tempwhitelist " + pkgName;
154         device.executeShellCommand(cmd);
155     }
156 
157     /**
158      * Uninstall the statsdatom CTS app from the device.
159      */
uninstallStatsdTestApp(ITestDevice device)160     public static void uninstallStatsdTestApp(ITestDevice device) throws Exception {
161         uninstallTestApp(device, STATSD_ATOM_TEST_PKG);
162     }
163 
164     /**
165      * Uninstall the test app from the device.
166      */
uninstallTestApp(ITestDevice device, String pkgName)167     public static void uninstallTestApp(ITestDevice device, String pkgName) throws Exception {
168         device.uninstallPackage(pkgName);
169     }
170 
171     /**
172      * Run an adb shell command on device and parse the results as a proto of a given type.
173      *
174      * @param device Device to run cmd on
175      * @param parser Protobuf parser object, which can be retrieved by running MyProto.parser()
176      * @param cmd The adb shell command to run (e.g. "cmd stats update config")
177      *
178      * @throws DeviceNotAvailableException
179      * @throws InvalidProtocolBufferException Occurs if there was an error parsing the proto. Note
180      *     that a 0 length buffer is not necessarily an error.
181      * @return Proto of specified type
182      */
getShellCommandOutput(@onnull ITestDevice device, Parser<T> parser, String cmd)183     public static <T extends MessageLite> T getShellCommandOutput(@Nonnull ITestDevice device,
184             Parser<T> parser, String cmd)
185             throws DeviceNotAvailableException, InvalidProtocolBufferException {
186         final CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
187         device.executeShellCommand(cmd, receiver);
188         try {
189             return parser.parseFrom(receiver.getOutput());
190         } catch (Exception ex) {
191             CLog.d("Error parsing " + parser.getClass().getCanonicalName() + " for cmd " + cmd);
192             throw ex;
193         }
194     }
195 
196     /**
197      * Returns the UID of the host, which should always either be AID_SHELL (2000) or AID_ROOT (0).
198      */
getHostUid(ITestDevice device)199     public static int getHostUid(ITestDevice device) throws DeviceNotAvailableException {
200         String uidString = "";
201         try {
202             uidString = device.executeShellCommand("id -u");
203             return Integer.parseInt(uidString.trim());
204         } catch (NumberFormatException ex) {
205             CLog.e("Failed to get host's uid via shell command. Found " + uidString);
206             // Fall back to alternative method...
207             if (device.isAdbRoot()) {
208                 return 0;
209             } else {
210                 return 2000; // SHELL
211             }
212         }
213     }
214 
215     /**
216      * Returns the UID of the statsdatom CTS test app.
217      */
getStatsdTestAppUid(ITestDevice device)218     public static int getStatsdTestAppUid(ITestDevice device) throws DeviceNotAvailableException {
219         return getAppUid(device, STATSD_ATOM_TEST_PKG);
220     }
221 
222     /**
223      * Returns the UID of the test app.
224      */
getAppUid(ITestDevice device, String pkgName)225     public static int getAppUid(ITestDevice device, String pkgName)
226             throws DeviceNotAvailableException {
227         int currentUser = device.getCurrentUser();
228         String uidLine = device.executeShellCommand("cmd package list packages -U --user "
229                 + currentUser + " " + pkgName);
230         String[] uidLineArr = uidLine.split(":");
231 
232         // Package uid is located at index 2.
233         assertThat(uidLineArr.length).isGreaterThan(2);
234         int appUid = Integer.parseInt(uidLineArr[2].trim());
235         assertThat(appUid).isGreaterThan(10000);
236         return appUid;
237     }
238 
239     /**
240      * Determines if the device has the given features.
241      *
242      * @param feature name of the feature (e.g. "android.hardware.bluetooth")
243      */
hasFeature(ITestDevice device, String feature)244     public static boolean hasFeature(ITestDevice device, String feature) throws Exception {
245         final String features = device.executeShellCommand("pm list features");
246         return features.contains(feature);
247     }
248 
249     /**
250      * Runs an activity in a particular app.
251      */
runActivity(ITestDevice device, String pkgName, String activity, @Nullable String actionKey, @Nullable String actionValue)252     public static void runActivity(ITestDevice device, String pkgName, String activity,
253             @Nullable String actionKey, @Nullable String actionValue) throws Exception {
254         runActivity(device, pkgName, activity, actionKey, actionValue,
255                 AtomTestUtils.WAIT_TIME_LONG);
256     }
257 
258     /**
259      * Runs an activity in a particular app for a certain period of time.
260      *
261      * @param pkgName name of package that contains the Activity
262      * @param activity name of the Activity class
263      * @param actionKey key of extra data that is passed to the Activity via an Intent
264      * @param actionValue value of extra data that is passed to the Activity via an Intent
265      * @param waitTimeMs duration that the activity runs for
266      */
runActivity(ITestDevice device, String pkgName, String activity, @Nullable String actionKey, @Nullable String actionValue, long waitTimeMs)267     public static void runActivity(ITestDevice device, String pkgName, String activity,
268             @Nullable String actionKey, @Nullable String actionValue, long waitTimeMs)
269             throws Exception {
270         try (AutoCloseable a = withActivity(device, pkgName, activity, actionKey, actionValue)) {
271             Thread.sleep(waitTimeMs);
272         }
273     }
274 
275     /**
276      * Starts the specified activity and returns an {@link AutoCloseable} that stops the activity
277      * when closed.
278      *
279      * <p>Example usage:
280      * <pre>
281      *     try (AutoClosable a = withActivity("activity", "action", "action-value")) {
282      *         doStuff();
283      *     }
284      * </pre>
285      */
withActivity(ITestDevice device, String pkgName, String activity, @Nullable String actionKey, @Nullable String actionValue)286     public static AutoCloseable withActivity(ITestDevice device, String pkgName, String activity,
287             @Nullable String actionKey, @Nullable String actionValue) throws Exception {
288         String intentString;
289         if (actionKey != null && actionValue != null) {
290             intentString = actionKey + " " + actionValue;
291         } else {
292             intentString = null;
293         }
294 
295         String cmd = "am start -n " + pkgName + "/." + activity;
296         if (intentString != null) {
297             cmd += " -e " + intentString;
298         }
299         device.executeShellCommand(cmd);
300 
301         return () -> {
302             device.executeShellCommand("am force-stop " + pkgName);
303             Thread.sleep(AtomTestUtils.WAIT_TIME_SHORT);
304         };
305     }
306 
setChargingState(ITestDevice device, int state)307     public static void setChargingState(ITestDevice device, int state) throws Exception {
308         device.executeShellCommand("cmd battery set status " + state);
309     }
310 
unplugDevice(ITestDevice device)311     public static void unplugDevice(ITestDevice device) throws Exception {
312         // On batteryless devices on Android P or above, the 'unplug' command
313         // alone does not simulate the really unplugged state.
314         //
315         // This is because charging state is left as "unknown". Unless a valid
316         // state like 3 = BatteryManager.BATTERY_STATUS_DISCHARGING is set,
317         // framework does not consider the device as running on battery.
318         setChargingState(device, 3);
319         device.executeShellCommand("cmd battery unplug");
320     }
321 
plugInAc(ITestDevice device)322     public static void plugInAc(ITestDevice device) throws Exception {
323         device.executeShellCommand("cmd battery set ac 1");
324     }
325 
turnScreenOn(ITestDevice device)326     public static void turnScreenOn(ITestDevice device) throws Exception {
327         device.executeShellCommand("input keyevent KEYCODE_WAKEUP");
328         device.executeShellCommand("wm dismiss-keyguard");
329     }
330 
turnScreenOff(ITestDevice device)331     public static void turnScreenOff(ITestDevice device) throws Exception {
332         device.executeShellCommand("input keyevent KEYCODE_SLEEP");
333     }
334 
turnBatteryStatsAutoResetOn(ITestDevice device)335     public static void turnBatteryStatsAutoResetOn(ITestDevice device) throws Exception {
336         device.executeShellCommand("dumpsys batterystats enable no-auto-reset");
337     }
338 
turnBatteryStatsAutoResetOff(ITestDevice device)339     public static void turnBatteryStatsAutoResetOff(ITestDevice device) throws Exception {
340         device.executeShellCommand("dumpsys batterystats enable no-auto-reset");
341     }
342 
flushBatteryStatsHandlers(ITestDevice device)343     public static void flushBatteryStatsHandlers(ITestDevice device) throws Exception {
344         // Dumping batterystats will flush everything in the batterystats handler threads.
345         device.executeShellCommand("dumpsys batterystats");
346     }
347 
hasBattery(ITestDevice device)348     public static boolean hasBattery(ITestDevice device) throws Exception {
349         try {
350             BatteryServiceDumpProto batteryProto = getShellCommandOutput(device, BatteryServiceDumpProto.parser(),
351                     String.join(" ", DUMP_BATTERY_CMD, "--proto"));
352             LogUtil.CLog.d("Got battery service dump:\n " + batteryProto.toString());
353             return batteryProto.getIsPresent();
354         } catch (com.google.protobuf.InvalidProtocolBufferException e) {
355             LogUtil.CLog.e("Failed to dump batteryservice proto");
356             throw (e);
357         }
358     }
359 
resetBatteryStatus(ITestDevice device)360     public static void resetBatteryStatus(ITestDevice device) throws Exception {
361         device.executeShellCommand("cmd battery reset");
362     }
363 
getProperty(ITestDevice device, String prop)364     public static String getProperty(ITestDevice device, String prop) throws Exception {
365         return device.executeShellCommand("getprop " + prop).replace("\n", "");
366     }
367 
isDebuggable(ITestDevice device)368     public static boolean isDebuggable(ITestDevice device) throws Exception {
369         return Integer.parseInt(getProperty(device, "ro.debuggable")) == 1;
370     }
371 
checkDeviceFor(ITestDevice device, String methodName)372     public static boolean checkDeviceFor(ITestDevice device, String methodName) throws Exception {
373         try {
374             runDeviceTestsOnStatsdApp(device, ".Checkers", methodName);
375             // Test passes, meaning that the answer is true.
376             LogUtil.CLog.d(methodName + "() indicates true.");
377             return true;
378         } catch (AssertionError e) {
379             // Method is designed to fail if the answer is false.
380             LogUtil.CLog.d(methodName + "() indicates false.");
381             return false;
382         }
383     }
384 
385     /** Make the test app standby-active so it can run syncs and jobs immediately. */
allowImmediateSyncs(ITestDevice device)386     public static void allowImmediateSyncs(ITestDevice device) throws Exception {
387         device.executeShellCommand("am set-standby-bucket "
388                 + DeviceUtils.STATSD_ATOM_TEST_PKG + " active");
389     }
390 
391     /**
392      * Runs a (background) service to perform the given action.
393      * @param actionValue the action code constants indicating the desired action to perform.
394      */
executeBackgroundService(ITestDevice device, String actionValue)395     public static void executeBackgroundService(ITestDevice device, String actionValue)
396             throws Exception {
397         executeServiceAction(device, "StatsdCtsBackgroundService", actionValue);
398     }
399 
400     /**
401      * Runs the specified statsd package service to perform the given action.
402      * @param actionValue the action code constants indicating the desired action to perform.
403      */
executeServiceAction(ITestDevice device, String service, String actionValue)404     public static void executeServiceAction(ITestDevice device, String service, String actionValue)
405             throws Exception {
406         allowBackgroundServices(device);
407         device.executeShellCommand(String.format(
408                 "am startservice -n '%s/.%s' -e %s %s",
409                 STATSD_ATOM_TEST_PKG, service,
410                 KEY_ACTION, actionValue));
411     }
412 
413     /**
414      * Required to successfully start a background service from adb in Android O.
415      */
allowBackgroundServices(ITestDevice device)416     private static void allowBackgroundServices(ITestDevice device) throws Exception {
417         device.executeShellCommand(String.format(
418                 "cmd deviceidle tempwhitelist %s", STATSD_ATOM_TEST_PKG));
419     }
420 
421     /**
422      * Returns the kernel major version as a pair of ints.
423      */
getKernelVersion(ITestDevice device)424     public static Pair<Integer, Integer> getKernelVersion(ITestDevice device)
425             throws Exception {
426         String[] version = device.executeShellCommand("uname -r").split("\\.");
427         if (version.length < 2) {
428               throw new RuntimeException("Could not parse kernel version");
429         }
430         return Pair.create(Integer.parseInt(version[0]), Integer.parseInt(version[1]));
431     }
432 
433     /** Returns if the device kernel version >= input kernel version. */
isKernelGreaterEqual(ITestDevice device, Pair<Integer, Integer> version)434     public static boolean isKernelGreaterEqual(ITestDevice device, Pair<Integer, Integer> version)
435             throws Exception {
436         Pair<Integer, Integer> kernelVersion = getKernelVersion(device);
437         return kernelVersion.first > version.first
438                 || (kernelVersion.first == version.first && kernelVersion.second >= version.second);
439     }
440 
DeviceUtils()441     private DeviceUtils() {}
442 }
443