blob: bbc59d4827c9ae41ec1dcc3c9de2581fd14e1457 [file] [log] [blame]
Brett Chabot74121d82010-01-28 20:14:27 -08001/*
2 * Copyright (C) 2010 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 */
Brett Chabot74121d82010-01-28 20:14:27 -080016package com.android.tradefed.device;
17
Julien Desprezc8474552016-02-17 10:59:27 +000018import com.android.ddmlib.AdbCommandRejectedException;
Brett Chabot74121d82010-01-28 20:14:27 -080019import com.android.ddmlib.IDevice;
Julien Desprezc8474552016-02-17 10:59:27 +000020import com.android.ddmlib.InstallException;
jdesprezb1469112018-02-15 09:57:25 -080021import com.android.ddmlib.InstallReceiver;
Julien Desprezc8474552016-02-17 10:59:27 +000022import com.android.ddmlib.RawImage;
23import com.android.ddmlib.ShellCommandUnresponsiveException;
24import com.android.ddmlib.SyncException;
25import com.android.ddmlib.TimeoutException;
26import com.android.tradefed.log.LogUtil.CLog;
27import com.android.tradefed.result.ByteArrayInputStreamSource;
28import com.android.tradefed.result.InputStreamSource;
Julien Desprez14e96692017-01-12 12:31:29 +000029import com.android.tradefed.util.KeyguardControllerState;
Julien Desprez4c8aabc2016-03-14 15:53:48 +000030import com.android.tradefed.util.RunUtil;
Julien Desprezc8474552016-02-17 10:59:27 +000031import com.android.tradefed.util.StreamUtil;
32
Julien Desprez1fadf1a2016-10-20 15:50:46 +010033import com.google.common.annotations.VisibleForTesting;
jdesprezd7670322017-08-30 14:12:28 -070034import com.google.common.base.Strings;
Julien Desprez1fadf1a2016-10-20 15:50:46 +010035
Julien Desprezc8474552016-02-17 10:59:27 +000036import java.awt.Image;
37import java.awt.image.BufferedImage;
38import java.io.ByteArrayOutputStream;
39import java.io.File;
40import java.io.IOException;
41import java.util.ArrayList;
42import java.util.Arrays;
Julien Desprez4c8aabc2016-03-14 15:53:48 +000043import java.util.HashMap;
Julien Desprezc8474552016-02-17 10:59:27 +000044import java.util.HashSet;
45import java.util.List;
46import java.util.Map;
47import java.util.Set;
jdesprezbc580f92017-06-02 11:41:40 -070048import java.util.concurrent.TimeUnit;
Julien Desprezc8474552016-02-17 10:59:27 +000049import java.util.regex.Matcher;
50import java.util.regex.Pattern;
51
52import javax.imageio.ImageIO;
Brett Chabotbbea34d2011-06-10 13:48:41 -070053
Brett Chabot74121d82010-01-28 20:14:27 -080054/**
Julien Desprez6961b272016-02-01 09:58:23 +000055 * Implementation of a {@link ITestDevice} for a full stack android device
Brett Chabot74121d82010-01-28 20:14:27 -080056 */
Julien Desprez2f34e382016-06-21 12:30:39 +010057public class TestDevice extends NativeDevice {
Guang Zhubd90e4d2015-09-08 17:44:12 -070058
Julien Desprezc8474552016-02-17 10:59:27 +000059 /** number of attempts made to clear dialogs */
60 private static final int NUM_CLEAR_ATTEMPTS = 5;
61 /** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */
62 static final String DISMISS_DIALOG_CMD = "input keyevent 23";
Julien Desprez0e849332017-01-18 12:22:30 +000063 /** Commands that can be used to dismiss the keyguard. */
64 static final String DISMISS_KEYGUARD_CMD = "input keyevent 82";
65
66 /**
67 * Alternative command to dismiss the keyguard by requesting the Window Manager service to do
68 * it. Api 23 and after.
69 */
70 static final String DISMISS_KEYGUARD_WM_CMD = "wm dismiss-keyguard";
71
Julien Desprezc8474552016-02-17 10:59:27 +000072 /** Timeout to wait for input dispatch to become ready **/
73 private static final long INPUT_DISPATCH_READY_TIMEOUT = 5 * 1000;
74 /** command to test input dispatch readiness **/
75 private static final String TEST_INPUT_CMD = "dumpsys input";
76
Julien Desprez4c8aabc2016-03-14 15:53:48 +000077 private static final long AM_COMMAND_TIMEOUT = 10 * 1000;
78 private static final long CHECK_NEW_USER = 1000;
79
Julien Desprezc8474552016-02-17 10:59:27 +000080 static final String LIST_PACKAGES_CMD = "pm list packages -f";
81 private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)=(.*)");
82
Jiyong Park3396a842018-12-17 14:01:54 +090083 static final String LIST_APEXES_CMD = "pm list packages --apex-only --show-versioncode";
84 private static final Pattern APEXES_REGEX = Pattern.compile("package:(.*) versionCode:(.*)");
85
Julien Desprezc8474552016-02-17 10:59:27 +000086 private static final int FLAG_PRIMARY = 1; // From the UserInfo class
87
Julien Desprez4c8aabc2016-03-14 15:53:48 +000088 private static final String[] SETTINGS_NAMESPACE = {"system", "secure", "global"};
89
Julien Desprez0f0a02a2017-03-01 16:59:10 +000090 /** user pattern in the output of "pm list users" = TEXT{<id>:<name>:<flags>} TEXT * */
91 private static final String USER_PATTERN = "(.*?\\{)(\\d+)(:)(.*)(:)(\\d+)(\\}.*)";
Julien Desprez4c8aabc2016-03-14 15:53:48 +000092
Julien Desprez3faaefd2016-05-24 19:25:12 +010093 private static final int API_LEVEL_GET_CURRENT_USER = 24;
jdesprezbc580f92017-06-02 11:41:40 -070094 /** Timeout to wait for a screenshot before giving up to avoid hanging forever */
95 private static final long MAX_SCREENSHOT_TIMEOUT = 5 * 60 * 1000; // 5 min
Julien Desprez3faaefd2016-05-24 19:25:12 +010096
jdesprezd7670322017-08-30 14:12:28 -070097 /** adb shell am dumpheap <service pid> <dump file path> */
98 private static final String DUMPHEAP_CMD = "am dumpheap %s %s";
99 /** Time given to a file to be dumped on device side */
100 private static final long DUMPHEAP_TIME = 5000l;
101
jdesprezb1469112018-02-15 09:57:25 -0800102 /** Timeout in minutes for the package installation */
103 static final long INSTALL_TIMEOUT_MINUTES = 4;
104 /** Max timeout to output for package installation */
105 static final long INSTALL_TIMEOUT_TO_OUTPUT_MINUTES = 3;
106
Julien Desprezb1301842018-04-16 10:12:52 -0700107 private boolean mWasWifiHelperInstalled = false;
108
Jiyong Park30475a82018-12-03 22:25:00 +0900109 private static final String APEX_SUFFIX = ".apex";
110 private static final String APEX_ARG = "--apex";
111
Julien Desprezc8474552016-02-17 10:59:27 +0000112 /**
Julien Desprez6961b272016-02-01 09:58:23 +0000113 * @param device
114 * @param stateMonitor
115 * @param allocationMonitor
Omari Stephens718c9622011-02-02 18:17:32 -0800116 */
Julien Desprez6961b272016-02-01 09:58:23 +0000117 public TestDevice(IDevice device, IDeviceStateMonitor stateMonitor,
118 IDeviceMonitor allocationMonitor) {
119 super(device, stateMonitor, allocationMonitor);
Kevin Lau Fang3435aff2016-01-29 01:47:12 +0000120 }
Julien Desprezc8474552016-02-17 10:59:27 +0000121
122 /**
123 * Core implementation of package installation, with retries around
124 * {@link IDevice#installPackage(String, boolean, String...)}
125 * @param packageFile
126 * @param reinstall
127 * @param extraArgs
128 * @return the response from the installation
129 * @throws DeviceNotAvailableException
130 */
131 private String internalInstallPackage(
132 final File packageFile, final boolean reinstall, final List<String> extraArgs)
133 throws DeviceNotAvailableException {
Jiyong Park30475a82018-12-03 22:25:00 +0900134 List<String> args = new ArrayList<>(extraArgs);
135 if (packageFile.getName().endsWith(APEX_SUFFIX)) {
136 args.add(APEX_ARG);
137 }
Julien Desprezc8474552016-02-17 10:59:27 +0000138 // use array to store response, so it can be returned to caller
139 final String[] response = new String[1];
jdesprezb1469112018-02-15 09:57:25 -0800140 DeviceAction installAction =
141 new DeviceAction() {
142 @Override
143 public boolean run() throws InstallException {
144 try {
145 InstallReceiver receiver = createInstallReceiver();
146 getIDevice()
147 .installPackage(
148 packageFile.getAbsolutePath(),
149 reinstall,
150 receiver,
151 INSTALL_TIMEOUT_MINUTES,
152 INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
153 TimeUnit.MINUTES,
Jiyong Park30475a82018-12-03 22:25:00 +0900154 args.toArray(new String[] {}));
jdesprezb1469112018-02-15 09:57:25 -0800155 if (receiver.isSuccessfullyCompleted()) {
156 response[0] = null;
157 } else if (receiver.getErrorMessage() == null) {
158 response[0] =
159 String.format(
160 "Installation of %s timed out",
161 packageFile.getAbsolutePath());
162 } else {
163 response[0] = receiver.getErrorMessage();
164 }
165 } catch (InstallException e) {
Julien Desprez1307c5e2018-10-16 08:59:10 -0700166 String message = e.getMessage();
167 if (message == null) {
168 message =
169 String.format(
170 "InstallException during package installation. "
171 + "cause: %s",
172 StreamUtil.getStackTrace(e));
173 }
174 response[0] = message;
jdesprezb1469112018-02-15 09:57:25 -0800175 }
176 return response[0] == null;
177 }
178 };
Julien Desprezc8474552016-02-17 10:59:27 +0000179 performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
180 installAction, MAX_RETRY_ATTEMPTS);
181 return response[0];
182 }
183
184 /**
jdesprezb1469112018-02-15 09:57:25 -0800185 * Creates and return an {@link InstallReceiver} for {@link #internalInstallPackage(File,
186 * boolean, List)} and {@link #installPackage(File, File, boolean, String...)} testing.
187 */
188 @VisibleForTesting
189 InstallReceiver createInstallReceiver() {
190 return new InstallReceiver();
191 }
192
193 /**
Julien Desprezc8474552016-02-17 10:59:27 +0000194 * {@inheritDoc}
195 */
196 @Override
197 public String installPackage(final File packageFile, final boolean reinstall,
198 final String... extraArgs) throws DeviceNotAvailableException {
199 boolean runtimePermissionSupported = isRuntimePermissionSupported();
200 List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
201 // grant all permissions by default if feature is supported
202 if (runtimePermissionSupported) {
203 args.add("-g");
204 }
205 return internalInstallPackage(packageFile, reinstall, args);
206 }
207
208 /**
209 * {@inheritDoc}
210 */
211 @Override
212 public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
213 String... extraArgs) throws DeviceNotAvailableException {
214 ensureRuntimePermissionSupported();
215 List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
216 if (grantPermissions) {
217 args.add("-g");
218 }
219 return internalInstallPackage(packageFile, reinstall, args);
220 }
221
222 /**
223 * {@inheritDoc}
224 */
225 @Override
226 public String installPackageForUser(File packageFile, boolean reinstall, int userId,
227 String... extraArgs) throws DeviceNotAvailableException {
228 boolean runtimePermissionSupported = isRuntimePermissionSupported();
229 List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
230 // grant all permissions by default if feature is supported
231 if (runtimePermissionSupported) {
232 args.add("-g");
233 }
234 args.add("--user");
235 args.add(Integer.toString(userId));
236 return internalInstallPackage(packageFile, reinstall, args);
237 }
238
239 /**
240 * {@inheritDoc}
241 */
242 @Override
243 public String installPackageForUser(File packageFile, boolean reinstall,
244 boolean grantPermissions, int userId, String... extraArgs)
245 throws DeviceNotAvailableException {
246 ensureRuntimePermissionSupported();
247 List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
248 if (grantPermissions) {
249 args.add("-g");
250 }
251 args.add("--user");
252 args.add(Integer.toString(userId));
253 return internalInstallPackage(packageFile, reinstall, args);
254 }
255
256 public String installPackage(final File packageFile, final File certFile,
257 final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException {
258 // use array to store response, so it can be returned to caller
259 final String[] response = new String[1];
jdesprezb1469112018-02-15 09:57:25 -0800260 DeviceAction installAction =
261 new DeviceAction() {
262 @Override
263 public boolean run()
264 throws InstallException, SyncException, IOException, TimeoutException,
265 AdbCommandRejectedException {
266 // TODO: create a getIDevice().installPackage(File, File...) method when the
267 // dist cert functionality is ready to be open sourced
268 String remotePackagePath =
269 getIDevice().syncPackageToDevice(packageFile.getAbsolutePath());
270 String remoteCertPath =
271 getIDevice().syncPackageToDevice(certFile.getAbsolutePath());
272 // trick installRemotePackage into issuing a 'pm install <apk> <cert>'
273 // command, by adding apk path to extraArgs, and using cert as the
274 // 'apk file'.
275 String[] newExtraArgs = new String[extraArgs.length + 1];
276 System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length);
277 newExtraArgs[newExtraArgs.length - 1] =
278 String.format("\"%s\"", remotePackagePath);
279 try {
280 InstallReceiver receiver = createInstallReceiver();
281 getIDevice()
282 .installRemotePackage(
283 remoteCertPath,
284 reinstall,
285 receiver,
286 INSTALL_TIMEOUT_MINUTES,
287 INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
288 TimeUnit.MINUTES,
289 newExtraArgs);
290 if (receiver.isSuccessfullyCompleted()) {
291 response[0] = null;
292 } else if (receiver.getErrorMessage() == null) {
293 response[0] =
294 String.format(
295 "Installation of %s timed out.",
296 packageFile.getAbsolutePath());
297 } else {
298 response[0] = receiver.getErrorMessage();
299 }
300 } catch (InstallException e) {
Julien Desprez1307c5e2018-10-16 08:59:10 -0700301 String message = e.getMessage();
302 if (message == null) {
303 message =
304 String.format(
305 "InstallException during package installation. "
306 + "cause: %s",
307 StreamUtil.getStackTrace(e));
308 }
309 response[0] = message;
jdesprezb1469112018-02-15 09:57:25 -0800310 } finally {
311 getIDevice().removeRemotePackage(remotePackagePath);
312 getIDevice().removeRemotePackage(remoteCertPath);
313 }
314 return true;
315 }
316 };
Julien Desprezc8474552016-02-17 10:59:27 +0000317 performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
318 installAction, MAX_RETRY_ATTEMPTS);
319 return response[0];
320 }
321
322 /**
323 * {@inheritDoc}
324 */
325 @Override
326 public String uninstallPackage(final String packageName) throws DeviceNotAvailableException {
327 // use array to store response, so it can be returned to caller
328 final String[] response = new String[1];
329 DeviceAction uninstallAction = new DeviceAction() {
330 @Override
331 public boolean run() throws InstallException {
332 CLog.d("Uninstalling %s", packageName);
333 String result = getIDevice().uninstallPackage(packageName);
334 response[0] = result;
335 return result == null;
336 }
337 };
338 performDeviceAction(String.format("uninstall %s", packageName), uninstallAction,
339 MAX_RETRY_ATTEMPTS);
340 return response[0];
341 }
342
Betty Zhoucd2abe12018-10-22 15:18:55 -0700343 /**
344 * Core implementation for installing application with split apk files {@link
345 * IDevice#installPackages(String, boolean, String...)}
346 * See "https://developer.android.com/studio/build/configure-apk-splits" on how to split
347 * apk to several files.
348 *
349 * @param packageFiles the local apk files
350 * @param reinstall <code>true</code> if a reinstall should be performed
351 * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
352 * available options.
353 * @return the response from the installation <code>null</code> if installation succeeds.
354 * @throws DeviceNotAvailableException
355 */
356 private String internalInstallPackages(
357 final List<File> packageFiles, final boolean reinstall, final List<String> extraArgs)
358 throws DeviceNotAvailableException {
359 // use array to store response, so it can be returned to caller
360 final String[] response = new String[1];
361 DeviceAction installAction =
362 new DeviceAction() {
363 @Override
364 public boolean run() throws InstallException {
365 try {
366 getIDevice()
367 .installPackages(
368 packageFiles,
369 reinstall,
Betty Zhouf8c46a22019-01-16 12:08:16 -0800370 extraArgs,
Betty Zhoucd2abe12018-10-22 15:18:55 -0700371 INSTALL_TIMEOUT_MINUTES,
372 TimeUnit.MINUTES);
373 response[0] = null;
374 return true;
375 } catch (InstallException e) {
376 response[0] = e.getMessage();
377 if (response[0] == null) {
378 response[0] =
379 String.format(
380 "InstallException: %s",
381 StreamUtil.getStackTrace(e));
382 }
383 return false;
384 }
385 }
386 };
387 performDeviceAction(
388 String.format("install %s", packageFiles.toString()),
389 installAction,
390 MAX_RETRY_ATTEMPTS);
391 return response[0];
392 }
393
394 /** {@inheritDoc} */
395 @Override
396 public String installPackages(
397 final List<File> packageFiles, final boolean reinstall, final String... extraArgs)
398 throws DeviceNotAvailableException {
399 // Grant all permissions by default if feature is supported
400 return installPackages(packageFiles, reinstall, isRuntimePermissionSupported(), extraArgs);
401 }
402
403 /** {@inheritDoc} */
404 @Override
405 public String installPackages(
406 List<File> packageFiles,
407 boolean reinstall,
408 boolean grantPermissions,
409 String... extraArgs)
410 throws DeviceNotAvailableException {
411 List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
412 if (grantPermissions) {
413 ensureRuntimePermissionSupported();
414 args.add("-g");
415 }
416 return internalInstallPackages(packageFiles, reinstall, args);
417 }
418
Betty Zhouf8c46a22019-01-16 12:08:16 -0800419 /** {@inheritDoc} */
420 @Override
421 public String installPackagesForUser(
422 List<File> packageFiles, boolean reinstall, int userId, String... extraArgs)
423 throws DeviceNotAvailableException {
424 // Grant all permissions by default if feature is supported
425 return installPackagesForUser(
426 packageFiles, reinstall, isRuntimePermissionSupported(), userId, extraArgs);
427 }
428
429 /** {@inheritDoc} */
430 @Override
431 public String installPackagesForUser(
432 List<File> packageFiles,
433 boolean reinstall,
434 boolean grantPermissions,
435 int userId,
436 String... extraArgs)
437 throws DeviceNotAvailableException {
438 List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
439 if (grantPermissions) {
440 ensureRuntimePermissionSupported();
441 args.add("-g");
442 }
443 args.add("--user");
444 args.add(Integer.toString(userId));
445 return internalInstallPackages(packageFiles, reinstall, args);
446 }
447
Betty Zhoucd2abe12018-10-22 15:18:55 -0700448 /**
449 * Core implementation for split apk remote installation {@link IDevice#installPackage(String,
450 * boolean, String...)}
451 * See "https://developer.android.com/studio/build/configure-apk-splits" on how to split
452 * apk to several files.
453 *
454 * @param packageFiles the remote apk file paths
455 * @param reinstall <code>true</code> if a reinstall should be performed
456 * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
457 * available options.
458 * @return the response from the installation <code>null</code> if installation succeeds.
459 * @throws DeviceNotAvailableException
460 */
461 private String internalInstallRemotePackages(
462 final List<String> remoteApkPaths,
463 final boolean reinstall,
464 final List<String> extraArgs)
465 throws DeviceNotAvailableException {
466 // use array to store response, so it can be returned to caller
467 final String[] response = new String[1];
468 DeviceAction installAction =
469 new DeviceAction() {
470 @Override
471 public boolean run() throws InstallException {
472 try {
473 getIDevice()
474 .installRemotePackages(
475 remoteApkPaths,
476 reinstall,
Betty Zhouf8c46a22019-01-16 12:08:16 -0800477 extraArgs,
Betty Zhoucd2abe12018-10-22 15:18:55 -0700478 INSTALL_TIMEOUT_MINUTES,
479 TimeUnit.MINUTES);
480 response[0] = null;
481 return true;
482 } catch (InstallException e) {
483 response[0] = e.getMessage();
484 if (response[0] == null) {
485 response[0] = String.format(
486 "InstallException during package installation. cause: %s",
487 StreamUtil.getStackTrace(e));
488 }
489 return false;
490 }
491 }
492 };
493 performDeviceAction(
494 String.format("install %s", remoteApkPaths.toString()),
495 installAction,
496 MAX_RETRY_ATTEMPTS);
497 return response[0];
498 }
499
500 /** {@inheritDoc} */
501 @Override
502 public String installRemotePackages(
503 final List<String> remoteApkPaths, final boolean reinstall, final String... extraArgs)
504 throws DeviceNotAvailableException {
505 // Grant all permissions by default if feature is supported
506 return installRemotePackages(
507 remoteApkPaths, reinstall, isRuntimePermissionSupported(), extraArgs);
508 }
509
510 /** {@inheritDoc} */
511 @Override
512 public String installRemotePackages(
513 List<String> remoteApkPaths,
514 boolean reinstall,
515 boolean grantPermissions,
516 String... extraArgs)
517 throws DeviceNotAvailableException {
518 List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
519 if (grantPermissions) {
520 ensureRuntimePermissionSupported();
521 args.add("-g");
522 }
523 return internalInstallRemotePackages(remoteApkPaths, reinstall, args);
524 }
525
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000526 /** {@inheritDoc} */
Julien Desprezc8474552016-02-17 10:59:27 +0000527 @Override
528 public InputStreamSource getScreenshot() throws DeviceNotAvailableException {
529 return getScreenshot("PNG");
530 }
531
532 /**
533 * {@inheritDoc}
534 */
535 @Override
536 public InputStreamSource getScreenshot(String format) throws DeviceNotAvailableException {
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000537 return getScreenshot(format, true);
538 }
539
540 /** {@inheritDoc} */
541 @Override
542 public InputStreamSource getScreenshot(String format, boolean rescale)
543 throws DeviceNotAvailableException {
Julien Desprezc8474552016-02-17 10:59:27 +0000544 if (!format.equalsIgnoreCase("PNG") && !format.equalsIgnoreCase("JPEG")){
545 CLog.e("Screenshot: Format %s is not supported, defaulting to PNG.", format);
546 format = "PNG";
547 }
548 ScreenshotAction action = new ScreenshotAction();
549 if (performDeviceAction("screenshot", action, MAX_RETRY_ATTEMPTS)) {
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000550 byte[] imageData =
551 compressRawImage(action.mRawScreenshot, format.toUpperCase(), rescale);
Julien Desprezc8474552016-02-17 10:59:27 +0000552 if (imageData != null) {
553 return new ByteArrayInputStreamSource(imageData);
554 }
555 }
jdesprez8694ce92017-06-01 16:56:14 -0700556 // Return an error in the buffer
557 return new ByteArrayInputStreamSource(
558 "Error: device reported null for screenshot.".getBytes());
Julien Desprezc8474552016-02-17 10:59:27 +0000559 }
560
561 private class ScreenshotAction implements DeviceAction {
562
563 RawImage mRawScreenshot;
564
565 /**
566 * {@inheritDoc}
567 */
568 @Override
569 public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
570 ShellCommandUnresponsiveException, InstallException, SyncException {
jdesprezbc580f92017-06-02 11:41:40 -0700571 mRawScreenshot =
572 getIDevice().getScreenshot(MAX_SCREENSHOT_TIMEOUT, TimeUnit.MILLISECONDS);
Julien Desprezc8474552016-02-17 10:59:27 +0000573 return mRawScreenshot != null;
574 }
575 }
576
Julien Desprez1fadf1a2016-10-20 15:50:46 +0100577 /**
578 * Helper to compress a rawImage obtained from the screen.
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000579 *
580 * @param rawImage {@link RawImage} to compress.
581 * @param format resulting format of compressed image. PNG and JPEG are supported.
582 * @param rescale if rescaling should be done to further reduce size of compressed image.
583 * @return compressed image.
Julien Desprez1fadf1a2016-10-20 15:50:46 +0100584 */
585 @VisibleForTesting
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000586 byte[] compressRawImage(RawImage rawImage, String format, boolean rescale) {
587 BufferedImage image = rawImageToBufferedImage(rawImage, format);
588
589 // Rescale to reduce size if needed
590 // Screenshot default format is 1080 x 1920, 8-bit/color RGBA
591 // By cutting in half we can easily keep good quality and smaller size
592 if (rescale) {
593 image = rescaleImage(image);
594 }
595
596 return getImageData(image, format);
597 }
598
599 /**
600 * Converts {@link RawImage} to {@link BufferedImage} in specified format.
601 *
602 * @param rawImage {@link RawImage} to convert.
603 * @param format resulting format of image. PNG and JPEG are supported.
604 * @return converted image.
605 */
606 @VisibleForTesting
607 BufferedImage rawImageToBufferedImage(RawImage rawImage, String format) {
Julien Desprezc8474552016-02-17 10:59:27 +0000608 BufferedImage image = null;
609
610 if ("JPEG".equalsIgnoreCase(format)) {
611 //JPEG does not support ARGB without a special encoder
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000612 image =
613 new BufferedImage(
614 rawImage.width, rawImage.height, BufferedImage.TYPE_3BYTE_BGR);
Julien Desprezc8474552016-02-17 10:59:27 +0000615 }
616 else {
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000617 image = new BufferedImage(rawImage.width, rawImage.height, BufferedImage.TYPE_INT_ARGB);
Julien Desprezc8474552016-02-17 10:59:27 +0000618 }
619
620 // borrowed conversion logic from platform/sdk/screenshot/.../Screenshot.java
621 int index = 0;
622 int IndexInc = rawImage.bpp >> 3;
623 for (int y = 0 ; y < rawImage.height ; y++) {
624 for (int x = 0 ; x < rawImage.width ; x++) {
625 int value = rawImage.getARGB(index);
626 index += IndexInc;
627 image.setRGB(x, y, value);
628 }
629 }
630
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000631 return image;
632 }
633
634 /**
635 * Rescales image cutting it in half.
636 *
637 * @param image source {@link BufferedImage}.
638 * @return resulting scaled image.
639 */
640 @VisibleForTesting
641 BufferedImage rescaleImage(BufferedImage image) {
Julien Desprezc8474552016-02-17 10:59:27 +0000642 int shortEdge = Math.min(image.getHeight(), image.getWidth());
643 if (shortEdge > 720) {
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000644 Image resized =
645 image.getScaledInstance(
646 image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_SMOOTH);
647 image =
648 new BufferedImage(
649 image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_REPLICATE);
Julien Desprezc8474552016-02-17 10:59:27 +0000650 image.getGraphics().drawImage(resized, 0, 0, null);
651 }
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000652 return image;
653 }
Julien Desprezc8474552016-02-17 10:59:27 +0000654
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000655 /**
656 * Gets byte array representation of {@link BufferedImage}.
657 *
658 * @param image source {@link BufferedImage}.
659 * @param format resulting format of image. PNG and JPEG are supported.
660 * @return byte array representation of the image.
661 */
662 @VisibleForTesting
663 byte[] getImageData(BufferedImage image, String format) {
Julien Desprezc8474552016-02-17 10:59:27 +0000664 // store compressed image in memory, and let callers write to persistent storage
665 // use initial buffer size of 128K
666 byte[] imageData = null;
667 ByteArrayOutputStream imageOut = new ByteArrayOutputStream(128*1024);
668 try {
669 if (ImageIO.write(image, format, imageOut)) {
670 imageData = imageOut.toByteArray();
671 } else {
672 CLog.e("Failed to compress screenshot to png");
673 }
674 } catch (IOException e) {
675 CLog.e("Failed to compress screenshot to png");
676 CLog.e(e);
677 }
678 StreamUtil.close(imageOut);
679 return imageData;
680 }
681
682 /**
683 * {@inheritDoc}
684 */
685 @Override
686 public boolean clearErrorDialogs() throws DeviceNotAvailableException {
687 // attempt to clear error dialogs multiple times
688 for (int i = 0; i < NUM_CLEAR_ATTEMPTS; i++) {
689 int numErrorDialogs = getErrorDialogCount();
690 if (numErrorDialogs == 0) {
691 return true;
692 }
693 doClearDialogs(numErrorDialogs);
694 }
695 if (getErrorDialogCount() > 0) {
696 // at this point, all attempts to clear error dialogs completely have failed
697 // it might be the case that the process keeps showing new dialogs immediately after
698 // clearing. There's really no workaround, but to dump an error
699 CLog.e("error dialogs still exist on %s.", getSerialNumber());
700 return false;
701 }
702 return true;
703 }
704
705 /**
706 * Detects the number of crash or ANR dialogs currently displayed.
707 * <p/>
708 * Parses output of 'dump activity processes'
709 *
710 * @return count of dialogs displayed
711 * @throws DeviceNotAvailableException
712 */
713 private int getErrorDialogCount() throws DeviceNotAvailableException {
714 int errorDialogCount = 0;
715 Pattern crashPattern = Pattern.compile(".*crashing=true.*AppErrorDialog.*");
716 Pattern anrPattern = Pattern.compile(".*notResponding=true.*AppNotRespondingDialog.*");
717 String systemStatusOutput = executeShellCommand("dumpsys activity processes");
718 Matcher crashMatcher = crashPattern.matcher(systemStatusOutput);
719 while (crashMatcher.find()) {
720 errorDialogCount++;
721 }
722 Matcher anrMatcher = anrPattern.matcher(systemStatusOutput);
723 while (anrMatcher.find()) {
724 errorDialogCount++;
725 }
726
727 return errorDialogCount;
728 }
729
730 private void doClearDialogs(int numDialogs) throws DeviceNotAvailableException {
731 CLog.i("Attempted to clear %d dialogs on %s", numDialogs, getSerialNumber());
732 for (int i=0; i < numDialogs; i++) {
733 // send DPAD_CENTER
734 executeShellCommand(DISMISS_DIALOG_CMD);
735 }
736 }
737
738 /**
Guang Zhu03c985e2017-04-28 14:53:55 -0700739 * {@inheritDoc}
Julien Desprezc8474552016-02-17 10:59:27 +0000740 */
Guang Zhu03c985e2017-04-28 14:53:55 -0700741 @Override
742 public void disableKeyguard() throws DeviceNotAvailableException {
Julien Desprezc8474552016-02-17 10:59:27 +0000743 long start = System.currentTimeMillis();
744 while (true) {
745 Boolean ready = isDeviceInputReady();
746 if (ready == null) {
747 // unsupported API level, bail
748 break;
749 }
750 if (ready) {
751 // input dispatch is ready, bail
752 break;
753 }
754 long timeSpent = System.currentTimeMillis() - start;
755 if (timeSpent > INPUT_DISPATCH_READY_TIMEOUT) {
756 CLog.w("Timeout after waiting %dms on enabling of input dispatch", timeSpent);
757 // break & proceed anyway
758 break;
759 } else {
760 getRunUtil().sleep(1000);
761 }
762 }
Julien Desprez0e849332017-01-18 12:22:30 +0000763 if (getApiLevel() >= 23) {
764 CLog.i(
765 "Attempting to disable keyguard on %s using %s",
766 getSerialNumber(), DISMISS_KEYGUARD_WM_CMD);
767 String output = executeShellCommand(DISMISS_KEYGUARD_WM_CMD);
768 CLog.i("output of %s: %s", DISMISS_KEYGUARD_WM_CMD, output);
769 } else {
Tsu Chiang Chuangbfee7362017-01-25 14:22:54 -0800770 CLog.i("Command: %s, is not supported, falling back to %s", DISMISS_KEYGUARD_WM_CMD,
771 DISMISS_KEYGUARD_CMD);
Julien Desprez0e849332017-01-18 12:22:30 +0000772 executeShellCommand(DISMISS_KEYGUARD_CMD);
773 }
774 // TODO: check that keyguard was actually dismissed.
Julien Desprezc8474552016-02-17 10:59:27 +0000775 }
776
Julien Desprez14e96692017-01-12 12:31:29 +0000777 /** {@inheritDoc} */
778 @Override
779 public KeyguardControllerState getKeyguardState() throws DeviceNotAvailableException {
780 String output =
781 executeShellCommand("dumpsys activity activities | grep -A3 KeyguardController:");
Julien Desprez8a7bff32017-01-24 13:53:48 +0000782 CLog.d("Output from KeyguardController: %s", output);
Julien Desprez510a4832017-01-27 11:58:17 +0000783 KeyguardControllerState state =
784 KeyguardControllerState.create(Arrays.asList(output.trim().split("\n")));
785 return state;
Julien Desprez14e96692017-01-12 12:31:29 +0000786 }
787
Julien Desprezc8474552016-02-17 10:59:27 +0000788 /**
789 * Tests the device to see if input dispatcher is ready
Julien Desprez14e96692017-01-12 12:31:29 +0000790 *
Julien Desprezc8474552016-02-17 10:59:27 +0000791 * @return <code>null</code> if not supported by platform, or the actual readiness state
792 * @throws DeviceNotAvailableException
793 */
794 Boolean isDeviceInputReady() throws DeviceNotAvailableException {
795 CollectingOutputReceiver receiver = new CollectingOutputReceiver();
796 executeShellCommand(TEST_INPUT_CMD, receiver);
797 String output = receiver.getOutput();
798 Matcher m = INPUT_DISPATCH_STATE_REGEX.matcher(output);
799 if (!m.find()) {
800 // output does not contain the line at all, implying unsupported API level, bail
801 return null;
802 }
803 return "1".equals(m.group(1));
804 }
805
806 /**
807 * {@inheritDoc}
808 */
809 @Override
810 protected void prePostBootSetup() throws DeviceNotAvailableException {
811 if (mOptions.isDisableKeyguard()) {
812 disableKeyguard();
813 }
814 }
815
816 /**
817 * Performs an reboot via framework power manager
818 *
819 * Must have root access, device must be API Level 18 or above
820 *
821 * @param into the mode to reboot into, currently supported: bootloader, recovery, leave it
822 * null for a plain reboot
823 * @return <code>true</code> if the device rebooted, <code>false</code> if not successful or
824 * unsupported
825 * @throws DeviceNotAvailableException
826 */
827 private boolean doAdbFrameworkReboot(final String into) throws DeviceNotAvailableException {
828 // use framework reboot when:
829 // 1. device API level >= 18
830 // 2. has adb root
831 // 3. framework is running
832 if (!isEnableAdbRoot()) {
833 CLog.i("framework reboot is not supported; when enable root is disabled");
834 return false;
835 }
836 enableAdbRoot();
837 if (getApiLevel() >= 18 && isAdbRoot()) {
838 try {
839 // check framework running
840 String output = executeShellCommand("pm path android");
841 if (output == null || !output.contains("package:")) {
842 CLog.v("framework reboot: can't detect framework running");
843 return false;
844 }
845 String command = "svc power reboot";
846 if (into != null && !into.isEmpty()) {
847 command = String.format("%s %s", command, into);
848 }
849 executeShellCommand(command);
850 } catch (DeviceUnresponsiveException due) {
851 CLog.v("framework reboot: device unresponsive to shell command, using fallback");
852 return false;
853 }
Julien Desprezba60f9d2018-08-20 20:59:10 +0000854 boolean notAvailable = waitForDeviceNotAvailable(30 * 1000);
855 if (notAvailable) {
Julien Desprez78344aa2018-09-04 16:06:05 -0700856 postAdbReboot();
Julien Desprezba60f9d2018-08-20 20:59:10 +0000857 }
858 return notAvailable;
Julien Desprezc8474552016-02-17 10:59:27 +0000859 } else {
860 CLog.v("framework reboot: not supported");
861 return false;
862 }
863 }
864
865 /**
866 * Perform a adb reboot.
867 *
868 * @param into the bootloader name to reboot into, or <code>null</code> to just reboot the
869 * device.
870 * @throws DeviceNotAvailableException
871 */
872 @Override
873 protected void doAdbReboot(final String into) throws DeviceNotAvailableException {
Julien Desprezc8474552016-02-17 10:59:27 +0000874 if (!doAdbFrameworkReboot(into)) {
Julien Desprez78344aa2018-09-04 16:06:05 -0700875 super.doAdbReboot(into);
Julien Desprezc8474552016-02-17 10:59:27 +0000876 }
877 }
878
879 /**
880 * {@inheritDoc}
881 */
882 @Override
883 public Set<String> getInstalledPackageNames() throws DeviceNotAvailableException {
884 return getInstalledPackageNames(new PkgFilter() {
885 @Override
886 public boolean accept(String pkgName, String apkPath) {
887 return true;
888 }
889 });
890 }
891
Jiyong Park3396a842018-12-17 14:01:54 +0900892 /** {@inheritDoc} */
893 @Override
894 public Set<ApexInfo> getActiveApexes() throws DeviceNotAvailableException {
895 Set<ApexInfo> ret = new HashSet<>();
896 String output = executeShellCommand(LIST_APEXES_CMD);
897 if (output != null) {
898 Matcher m = APEXES_REGEX.matcher(output);
899 while (m.find()) {
900 String name = m.group(1);
901 long version = Long.valueOf(m.group(2));
902 ret.add(new ApexInfo(name, version));
903 }
904 }
905 return ret;
906 }
907
Julien Desprezc8474552016-02-17 10:59:27 +0000908 /**
Julien Desprez2f34e382016-06-21 12:30:39 +0100909 * A {@link com.android.tradefed.device.NativeDevice.DeviceAction}
Julien Desprezc8474552016-02-17 10:59:27 +0000910 * for retrieving package system service info, and do retries on
911 * failures.
912 */
913 private class DumpPkgAction implements DeviceAction {
914
915 Map<String, PackageInfo> mPkgInfoMap;
916
917 DumpPkgAction() {
918 }
919
920 @Override
921 public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
922 ShellCommandUnresponsiveException, InstallException, SyncException {
923 DumpsysPackageReceiver receiver = new DumpsysPackageReceiver();
924 getIDevice().executeShellCommand("dumpsys package p", receiver);
925 mPkgInfoMap = receiver.getPackages();
926 if (mPkgInfoMap.size() == 0) {
927 // Package parsing can fail if package manager is currently down. throw exception
928 // to retry
929 CLog.w("no packages found from dumpsys package p.");
930 throw new IOException();
931 }
932 return true;
933 }
934 }
935
936 /**
937 * {@inheritDoc}
938 */
939 @Override
940 public Set<String> getUninstallablePackageNames() throws DeviceNotAvailableException {
941 DumpPkgAction action = new DumpPkgAction();
942 performDeviceAction("dumpsys package p", action, MAX_RETRY_ATTEMPTS);
943
944 Set<String> pkgs = new HashSet<String>();
945 for (PackageInfo pkgInfo : action.mPkgInfoMap.values()) {
946 if (!pkgInfo.isSystemApp() || pkgInfo.isUpdatedSystemApp()) {
947 CLog.d("Found uninstallable package %s", pkgInfo.getPackageName());
948 pkgs.add(pkgInfo.getPackageName());
949 }
950 }
951 return pkgs;
952 }
953
954 /**
955 * {@inheritDoc}
956 */
957 @Override
958 public PackageInfo getAppPackageInfo(String packageName) throws DeviceNotAvailableException {
959 DumpPkgAction action = new DumpPkgAction();
960 performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS);
961 return action.mPkgInfoMap.get(packageName);
962 }
963
964 private static interface PkgFilter {
965 boolean accept(String pkgName, String apkPath);
966 }
967
968 // TODO: convert this to use DumpPkgAction
969 private Set<String> getInstalledPackageNames(PkgFilter filter)
970 throws DeviceNotAvailableException {
971 Set<String> packages= new HashSet<String>();
972 String output = executeShellCommand(LIST_PACKAGES_CMD);
973 if (output != null) {
974 Matcher m = PACKAGE_REGEX.matcher(output);
975 while (m.find()) {
976 String packagePath = m.group(1);
977 String packageName = m.group(2);
978 if (filter.accept(packageName, packagePath)) {
979 packages.add(packageName);
980 }
981 }
982 }
983 return packages;
984 }
985
986 /**
987 * {@inheritDoc}
988 */
989 @Override
990 public ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
991 ArrayList<String[]> users = tokenizeListUsers();
Julien Desprezc8474552016-02-17 10:59:27 +0000992 ArrayList<Integer> userIds = new ArrayList<Integer>(users.size());
993 for (String[] user : users) {
994 userIds.add(Integer.parseInt(user[1]));
995 }
996 return userIds;
997 }
998
999 /**
1000 * Tokenizes the output of 'pm list users'.
1001 * The returned tokens for each user have the form: {"\tUserInfo", Integer.toString(id), name,
1002 * Integer.toHexString(flag), "[running]"}; (the last one being optional)
1003 * @return a list of arrays of strings, each element of the list representing the tokens
1004 * for a user, or {@code null} if there was an error while tokenizing the adb command output.
1005 */
1006 private ArrayList<String[]> tokenizeListUsers() throws DeviceNotAvailableException {
1007 String command = "pm list users";
1008 String commandOutput = executeShellCommand(command);
1009 // Extract the id of all existing users.
1010 String[] lines = commandOutput.split("\\r?\\n");
Julien Desprezc8474552016-02-17 10:59:27 +00001011 if (!lines[0].equals("Users:")) {
jdesprez6b295b12017-11-02 16:00:48 -07001012 throw new DeviceRuntimeException(
1013 String.format("'%s' in not a valid output for 'pm list users'", commandOutput));
Julien Desprezc8474552016-02-17 10:59:27 +00001014 }
1015 ArrayList<String[]> users = new ArrayList<String[]>(lines.length - 1);
1016 for (int i = 1; i < lines.length; i++) {
1017 // Individual user is printed out like this:
1018 // \tUserInfo{$id$:$name$:$Integer.toHexString(flags)$} [running]
1019 String[] tokens = lines[i].split("\\{|\\}|:");
1020 if (tokens.length != 4 && tokens.length != 5) {
jdesprez6b295b12017-11-02 16:00:48 -07001021 throw new DeviceRuntimeException(
1022 String.format(
1023 "device output: '%s' \nline: '%s' was not in the expected "
1024 + "format for user info.",
1025 commandOutput, lines[i]));
Julien Desprezc8474552016-02-17 10:59:27 +00001026 }
1027 users.add(tokens);
1028 }
1029 return users;
1030 }
1031
1032 /**
1033 * {@inheritDoc}
1034 */
1035 @Override
1036 public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
1037 String command = "pm get-max-users";
1038 String commandOutput = executeShellCommand(command);
1039 try {
1040 return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
1041 } catch (NumberFormatException e) {
1042 CLog.e("Failed to parse result: %s", commandOutput);
1043 }
1044 return 0;
1045 }
1046
Alex Chau93617352018-01-16 15:22:25 +00001047 /** {@inheritDoc} */
1048 @Override
1049 public int getMaxNumberOfRunningUsersSupported() throws DeviceNotAvailableException {
1050 checkApiLevelAgainstNextRelease("get-max-running-users", 28);
1051 String command = "pm get-max-running-users";
1052 String commandOutput = executeShellCommand(command);
1053 try {
1054 return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
1055 } catch (NumberFormatException e) {
1056 CLog.e("Failed to parse result: %s", commandOutput);
1057 }
1058 return 0;
1059 }
1060
Julien Desprezc8474552016-02-17 10:59:27 +00001061 /**
1062 * {@inheritDoc}
1063 */
1064 @Override
1065 public boolean isMultiUserSupported() throws DeviceNotAvailableException {
1066 return getMaxNumberOfUsersSupported() > 1;
1067 }
1068
1069 /**
1070 * {@inheritDoc}
1071 */
1072 @Override
1073 public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException {
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001074 return createUser(name, false, false);
1075 }
1076
1077 /**
1078 * {@inheritDoc}
1079 */
1080 @Override
1081 public int createUser(String name, boolean guest, boolean ephemeral)
1082 throws DeviceNotAvailableException, IllegalStateException {
1083 String command ="pm create-user " + (guest ? "--guest " : "")
1084 + (ephemeral ? "--ephemeral " : "") + name;
1085 final String output = executeShellCommand(command);
Julien Desprezc8474552016-02-17 10:59:27 +00001086 if (output.startsWith("Success")) {
1087 try {
1088 return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim());
1089 } catch (NumberFormatException e) {
1090 CLog.e("Failed to parse result: %s", output);
1091 }
Julien Desprezc8474552016-02-17 10:59:27 +00001092 }
jdesprez86eb7742017-08-10 09:16:41 -07001093 throw new IllegalStateException(String.format("Failed to create user: %s", output));
Julien Desprezc8474552016-02-17 10:59:27 +00001094 }
1095
1096 /**
1097 * {@inheritDoc}
1098 */
1099 @Override
1100 public boolean removeUser(int userId) throws DeviceNotAvailableException {
1101 final String output = executeShellCommand(String.format("pm remove-user %s", userId));
1102 if (output.startsWith("Error")) {
1103 CLog.e("Failed to remove user: %s", output);
1104 return false;
1105 }
1106 return true;
1107 }
1108
1109 /**
1110 * {@inheritDoc}
1111 */
1112 @Override
1113 public boolean startUser(int userId) throws DeviceNotAvailableException {
1114 final String output = executeShellCommand(String.format("am start-user %s", userId));
1115 if (output.startsWith("Error")) {
1116 CLog.e("Failed to start user: %s", output);
1117 return false;
1118 }
1119 return true;
1120 }
1121
1122 /**
1123 * {@inheritDoc}
1124 */
1125 @Override
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001126 public boolean stopUser(int userId) throws DeviceNotAvailableException {
Julien Desprezc8474552016-02-17 10:59:27 +00001127 // No error or status code is returned.
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001128 return stopUser(userId, false, false);
1129 }
1130
1131 /**
1132 * {@inheritDoc}
1133 */
1134 @Override
1135 public boolean stopUser(int userId, boolean waitFlag, boolean forceFlag)
1136 throws DeviceNotAvailableException {
Tony Mak2ec98fa2017-05-31 14:43:39 +01001137 final int apiLevel = getApiLevel();
1138 if (waitFlag && apiLevel < 23) {
1139 throw new IllegalArgumentException("stop-user -w requires API level >= 23");
1140 }
1141 if (forceFlag && apiLevel < 24) {
1142 throw new IllegalArgumentException("stop-user -f requires API level >= 24");
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001143 }
Julien Desprezf06ab1e2016-11-18 11:52:37 +00001144 StringBuilder cmd = new StringBuilder("am stop-user ");
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001145 if (waitFlag) {
Julien Desprezf06ab1e2016-11-18 11:52:37 +00001146 cmd.append("-w ");
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001147 }
1148 if (forceFlag) {
Julien Desprezf06ab1e2016-11-18 11:52:37 +00001149 cmd.append("-f ");
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001150 }
Julien Desprezf06ab1e2016-11-18 11:52:37 +00001151 cmd.append(userId);
1152
1153 CLog.d("stopping user with command: %s", cmd.toString());
1154 final String output = executeShellCommand(cmd.toString());
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001155 if (output.contains("Error: Can't stop system user")) {
1156 CLog.e("Cannot stop System user.");
1157 return false;
1158 }
Tony Mak2ec98fa2017-05-31 14:43:39 +01001159 if (output.contains("Can't stop current user")) {
1160 CLog.e("Cannot stop current user.");
1161 return false;
1162 }
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001163 if (isUserRunning(userId)) {
1164 CLog.w("User Id: %s is still running after the stop-user command.", userId);
1165 return false;
1166 }
1167 return true;
Julien Desprezc8474552016-02-17 10:59:27 +00001168 }
1169
1170 /**
1171 * {@inheritDoc}
1172 */
1173 @Override
1174 public Integer getPrimaryUserId() throws DeviceNotAvailableException {
1175 ArrayList<String[]> users = tokenizeListUsers();
Julien Desprezc8474552016-02-17 10:59:27 +00001176 for (String[] user : users) {
1177 int flag = Integer.parseInt(user[3], 16);
1178 if ((flag & FLAG_PRIMARY) != 0) {
1179 return Integer.parseInt(user[1]);
1180 }
1181 }
1182 return null;
1183 }
1184
1185 /**
1186 * {@inheritDoc}
1187 */
1188 @Override
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001189 public int getCurrentUser() throws DeviceNotAvailableException {
Julien Desprez3faaefd2016-05-24 19:25:12 +01001190 checkApiLevelAgainstNextRelease("get-current-user", API_LEVEL_GET_CURRENT_USER);
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001191 final String output = executeShellCommand("am get-current-user");
1192 try {
Julien Desprez34ce18b2016-04-21 22:17:18 +01001193 int userId = Integer.parseInt(output.trim());
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001194 return userId;
1195 } catch (NumberFormatException e) {
1196 CLog.e(e);
1197 }
1198 return INVALID_USER_ID;
1199 }
1200
1201 private Matcher findUserInfo(String pmListUsersOutput) {
1202 Pattern pattern = Pattern.compile(USER_PATTERN);
1203 Matcher matcher = pattern.matcher(pmListUsersOutput);
1204 return matcher;
1205 }
1206
1207 /**
1208 * {@inheritDoc}
1209 */
1210 @Override
1211 public int getUserFlags(int userId) throws DeviceNotAvailableException {
1212 checkApiLevelAgainst("getUserFlags", 22);
1213 final String commandOutput = executeShellCommand("pm list users");
1214 Matcher matcher = findUserInfo(commandOutput);
1215 while(matcher.find()) {
1216 if (Integer.parseInt(matcher.group(2)) == userId) {
1217 return Integer.parseInt(matcher.group(6), 16);
1218 }
1219 }
1220 CLog.w("Could not find any flags for userId: %d in output: %s", userId, commandOutput);
1221 return INVALID_USER_ID;
1222 }
1223
1224 /**
1225 * {@inheritDoc}
1226 */
1227 @Override
1228 public boolean isUserRunning(int userId) throws DeviceNotAvailableException {
1229 checkApiLevelAgainst("isUserIdRunning", 22);
1230 final String commandOutput = executeShellCommand("pm list users");
1231 Matcher matcher = findUserInfo(commandOutput);
1232 while(matcher.find()) {
1233 if (Integer.parseInt(matcher.group(2)) == userId) {
1234 if (matcher.group(7).contains("running")) {
1235 return true;
1236 }
1237 }
1238 }
1239 return false;
1240 }
1241
1242 /**
1243 * {@inheritDoc}
1244 */
1245 @Override
1246 public int getUserSerialNumber(int userId) throws DeviceNotAvailableException {
1247 checkApiLevelAgainst("getUserSerialNumber", 22);
1248 final String commandOutput = executeShellCommand("dumpsys user");
1249 // example: UserInfo{0:Test:13} serialNo=0
1250 String userSerialPatter = "(.*\\{)(\\d+)(.*\\})(.*=)(\\d+)";
1251 Pattern pattern = Pattern.compile(userSerialPatter);
1252 Matcher matcher = pattern.matcher(commandOutput);
1253 while(matcher.find()) {
1254 if (Integer.parseInt(matcher.group(2)) == userId) {
1255 return Integer.parseInt(matcher.group(5));
1256 }
1257 }
1258 CLog.w("Could not find user serial number for userId: %d, in output: %s",
1259 userId, commandOutput);
1260 return INVALID_USER_ID;
1261 }
1262
1263 /**
1264 * {@inheritDoc}
1265 */
1266 @Override
1267 public boolean switchUser(int userId) throws DeviceNotAvailableException {
1268 return switchUser(userId, AM_COMMAND_TIMEOUT);
1269 }
1270
1271 /**
1272 * {@inheritDoc}
1273 */
1274 @Override
1275 public boolean switchUser(int userId, long timeout) throws DeviceNotAvailableException {
Julien Desprez3faaefd2016-05-24 19:25:12 +01001276 checkApiLevelAgainstNextRelease("switchUser", API_LEVEL_GET_CURRENT_USER);
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001277 if (userId == getCurrentUser()) {
1278 CLog.w("Already running as user id: %s. Nothing to be done.", userId);
1279 return true;
1280 }
1281 executeShellCommand(String.format("am switch-user %d", userId));
1282 long initialTime = getHostCurrentTime();
1283 while (getHostCurrentTime() - initialTime <= timeout) {
1284 if (userId == getCurrentUser()) {
1285 // disable keyguard if option is true
1286 prePostBootSetup();
1287 return true;
1288 } else {
1289 RunUtil.getDefault().sleep(getCheckNewUserSleep());
1290 }
1291 }
1292 CLog.e("User did not switch in the given %d timeout", timeout);
1293 return false;
1294 }
1295
1296 /**
1297 * Exposed for testing.
1298 */
1299 protected long getCheckNewUserSleep() {
1300 return CHECK_NEW_USER;
1301 }
1302
1303 /**
1304 * Exposed for testing
1305 */
1306 protected long getHostCurrentTime() {
1307 return System.currentTimeMillis();
1308 }
1309
1310 /**
1311 * {@inheritDoc}
1312 */
1313 @Override
1314 public boolean hasFeature(String feature) throws DeviceNotAvailableException {
1315 final String output = executeShellCommand("pm list features");
1316 if (output.contains(feature)) {
1317 return true;
1318 }
1319 CLog.w("Feature: %s is not available on %s", feature, getSerialNumber());
1320 return false;
1321 }
1322
1323 /**
1324 * {@inheritDoc}
1325 */
1326 @Override
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001327 public String getSetting(String namespace, String key) throws DeviceNotAvailableException {
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001328 return getSettingInternal("", namespace.trim(), key.trim());
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001329 }
1330
1331 /**
1332 * {@inheritDoc}
1333 */
1334 @Override
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001335 public String getSetting(int userId, String namespace, String key)
1336 throws DeviceNotAvailableException {
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001337 return getSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim());
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001338 }
1339
1340 /**
1341 * Internal Helper to get setting with or without a userId provided.
1342 */
1343 private String getSettingInternal(String userFlag, String namespace, String key)
1344 throws DeviceNotAvailableException {
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001345 namespace = namespace.toLowerCase();
1346 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) {
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001347 String cmd = String.format("settings %s get %s %s", userFlag, namespace, key);
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001348 String output = executeShellCommand(cmd);
1349 if ("null".equals(output)) {
1350 CLog.w("settings returned null for command: %s. "
1351 + "please check if the namespace:key exists", cmd);
1352 return null;
1353 }
1354 return output.trim();
1355 }
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001356 CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace);
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001357 return null;
1358 }
1359
Yichun Lib1900022018-05-11 09:54:48 -07001360 /** {@inheritDoc} */
1361 @Override
1362 public Map<String, String> getAllSettings(String namespace) throws DeviceNotAvailableException {
1363 return getAllSettingsInternal(namespace.trim());
1364 }
1365
1366 /** Internal helper to get all settings */
1367 private Map<String, String> getAllSettingsInternal(String namespace)
1368 throws DeviceNotAvailableException {
1369 namespace = namespace.toLowerCase();
1370 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) {
1371 Map<String, String> map = new HashMap<>();
1372 String cmd = String.format("settings list %s", namespace);
1373 String output = executeShellCommand(cmd);
1374 for (String line : output.split("\\n")) {
1375 // Setting's value could be empty
1376 String[] pair = line.trim().split("=", -1);
1377 if (pair.length > 1) {
1378 map.putIfAbsent(pair[0], pair[1]);
1379 } else {
1380 CLog.e("Unable to get setting from string: %s", line);
1381 }
1382 }
1383 return map;
1384 }
1385 CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace);
1386 return null;
1387 }
1388
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001389 /**
1390 * {@inheritDoc}
1391 */
1392 @Override
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001393 public void setSetting(String namespace, String key, String value)
1394 throws DeviceNotAvailableException {
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001395 setSettingInternal("", namespace.trim(), key.trim(), value.trim());
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001396 }
1397
1398 /**
1399 * {@inheritDoc}
1400 */
1401 @Override
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001402 public void setSetting(int userId, String namespace, String key, String value)
1403 throws DeviceNotAvailableException {
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001404 setSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim(),
1405 value.trim());
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001406 }
1407
1408 /**
1409 * Internal helper to set a setting with or without a userId provided.
1410 */
1411 private void setSettingInternal(String userFlag, String namespace, String key, String value)
1412 throws DeviceNotAvailableException {
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001413 checkApiLevelAgainst("Changing settings", 22);
1414 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace.toLowerCase())) {
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001415 executeShellCommand(String.format("settings %s put %s %s %s",
1416 userFlag, namespace, key, value));
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001417 } else {
1418 throw new IllegalArgumentException("Namespace must be one of system, secure, global."
1419 + " You provided: " + namespace);
1420 }
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001421 }
1422
1423 /**
1424 * {@inheritDoc}
1425 */
1426 @Override
1427 public String getAndroidId(int userId) throws DeviceNotAvailableException {
1428 if (isAdbRoot()) {
1429 String cmd = String.format(
1430 "sqlite3 /data/user/%d/com.google.android.gsf/databases/gservices.db "
1431 + "'select value from main where name = \"android_id\"'", userId);
1432 String output = executeShellCommand(cmd).trim();
1433 if (!output.contains("unable to open database")) {
1434 return output;
1435 }
1436 CLog.w("Couldn't find android-id, output: %s", output);
1437 } else {
1438 CLog.w("adb root is required.");
1439 }
1440 return null;
1441 }
1442
1443 /**
1444 * {@inheritDoc}
1445 */
1446 @Override
1447 public Map<Integer, String> getAndroidIds() throws DeviceNotAvailableException {
1448 ArrayList<Integer> userIds = listUsers();
1449 if (userIds == null) {
1450 return null;
1451 }
1452 Map<Integer, String> androidIds = new HashMap<Integer, String>();
1453 for (Integer id : userIds) {
1454 String androidId = getAndroidId(id);
1455 androidIds.put(id, androidId);
1456 }
1457 return androidIds;
1458 }
1459
1460 /**
1461 * {@inheritDoc}
1462 */
1463 @Override
Julien Desprezc8474552016-02-17 10:59:27 +00001464 IWifiHelper createWifiHelper() throws DeviceNotAvailableException {
Julien Desprezb1301842018-04-16 10:12:52 -07001465 mWasWifiHelperInstalled = true;
Avinankumar Vellore Suriyakumarc321ecd2017-08-08 16:44:40 -07001466 return new WifiHelper(this, mOptions.getWifiUtilAPKPath());
Julien Desprezc8474552016-02-17 10:59:27 +00001467 }
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001468
Julien Desprezb1301842018-04-16 10:12:52 -07001469 /**
1470 * Alternative to {@link #createWifiHelper()} where we can choose whether to do the wifi helper
1471 * setup or not.
1472 */
1473 @VisibleForTesting
1474 IWifiHelper createWifiHelper(boolean doSetup) throws DeviceNotAvailableException {
1475 if (doSetup) {
1476 mWasWifiHelperInstalled = true;
1477 }
1478 return new WifiHelper(this, mOptions.getWifiUtilAPKPath(), doSetup);
1479 }
1480
1481 /** {@inheritDoc} */
1482 @Override
1483 public void postInvocationTearDown() {
1484 super.postInvocationTearDown();
1485 // If wifi was installed and it's a real device, attempt to clean it.
1486 if (mWasWifiHelperInstalled) {
1487 mWasWifiHelperInstalled = false;
1488 if (getIDevice() instanceof StubDevice) {
1489 return;
1490 }
1491 if (!TestDeviceState.ONLINE.equals(getDeviceState())) {
1492 return;
1493 }
1494 try {
1495 // Uninstall the wifi utility if it was installed.
1496 IWifiHelper wifi = createWifiHelper(false);
1497 wifi.cleanUp();
1498 } catch (DeviceNotAvailableException e) {
1499 CLog.e("Device became unavailable while uninstalling wifi util.");
1500 CLog.e(e);
1501 }
1502 }
1503 }
1504
Tony Mak14e4dee2017-04-12 14:37:34 +01001505 /** {@inheritDoc} */
1506 @Override
1507 public boolean setDeviceOwner(String componentName, int userId)
1508 throws DeviceNotAvailableException {
1509 final String command = "dpm set-device-owner --user " + userId + " '" + componentName + "'";
1510 final String commandOutput = executeShellCommand(command);
1511 return commandOutput.startsWith("Success:");
1512 }
1513
1514 /** {@inheritDoc} */
1515 @Override
1516 public boolean removeAdmin(String componentName, int userId)
1517 throws DeviceNotAvailableException {
1518 final String command =
1519 "dpm remove-active-admin --user " + userId + " '" + componentName + "'";
1520 final String commandOutput = executeShellCommand(command);
1521 return commandOutput.startsWith("Success:");
1522 }
1523
1524 /** {@inheritDoc} */
1525 @Override
1526 public void removeOwners() throws DeviceNotAvailableException {
1527 String command = "dumpsys device_policy";
1528 String commandOutput = executeShellCommand(command);
1529 String[] lines = commandOutput.split("\\r?\\n");
1530 for (int i = 0; i < lines.length; ++i) {
1531 String line = lines[i].trim();
1532 if (line.contains("Profile Owner")) {
1533 // Line is "Profile owner (User <id>):
1534 String[] tokens = line.split("\\(|\\)| ");
1535 int userId = Integer.parseInt(tokens[4]);
Jonathan Scott8c033db2018-12-13 17:08:30 +00001536
1537 i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i);
Tony Mak14e4dee2017-04-12 14:37:34 +01001538 line = lines[i].trim();
1539 // Line is admin=ComponentInfo{<component>}
1540 tokens = line.split("\\{|\\}");
1541 String componentName = tokens[1];
1542 CLog.d("Cleaning up profile owner " + userId + " " + componentName);
1543 removeAdmin(componentName, userId);
1544 } else if (line.contains("Device Owner:")) {
Jonathan Scott8c033db2018-12-13 17:08:30 +00001545 i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i);
Tony Mak14e4dee2017-04-12 14:37:34 +01001546 line = lines[i].trim();
1547 // Line is admin=ComponentInfo{<component>}
1548 String[] tokens = line.split("\\{|\\}");
1549 String componentName = tokens[1];
Jonathan Scott8c033db2018-12-13 17:08:30 +00001550
Tony Mak14e4dee2017-04-12 14:37:34 +01001551 // Skip to user id line.
Jonathan Scott8c033db2018-12-13 17:08:30 +00001552 i = moveToNextIndexMatchingRegex(".*User ID:.*", lines, i);
Tony Mak14e4dee2017-04-12 14:37:34 +01001553 line = lines[i].trim();
1554 // Line is User ID: <N>
1555 tokens = line.split(":");
1556 int userId = Integer.parseInt(tokens[1].trim());
1557 CLog.d("Cleaning up device owner " + userId + " " + componentName);
1558 removeAdmin(componentName, userId);
1559 }
1560 }
1561 }
1562
Julien Desprez3faaefd2016-05-24 19:25:12 +01001563 /**
Jonathan Scott8c033db2018-12-13 17:08:30 +00001564 * Search forward from the current index to find a string matching the given regex.
1565 *
1566 * @param regex The regex to match each line against.
1567 * @param lines An array of strings to be searched.
1568 * @param currentIndex the index to start searching from.
1569 * @return The index of a string beginning with the regex.
1570 * @throws IllegalStateException if the line cannot be found.
1571 */
1572 private int moveToNextIndexMatchingRegex(String regex, String[] lines, int currentIndex) {
1573 while (currentIndex < lines.length && !lines[currentIndex].matches(regex)) {
1574 currentIndex++;
1575 }
1576
1577 if (currentIndex >= lines.length) {
1578 throw new IllegalStateException(
1579 "The output of 'dumpsys device_policy' was not as expected. Owners have not "
1580 + "been removed. This will leave the device in an unstable state and "
1581 + "will lead to further test failures.");
1582 }
1583
1584 return currentIndex;
1585 }
1586
1587 /**
Julien Desprez3faaefd2016-05-24 19:25:12 +01001588 * Helper for Api level checking of features in the new release before we incremented the api
1589 * number.
1590 */
1591 private void checkApiLevelAgainstNextRelease(String feature, int strictMinLevel)
1592 throws DeviceNotAvailableException {
1593 String codeName = getProperty(BUILD_CODENAME_PROP).trim();
Julien Desprez71489f32017-03-03 12:46:34 +00001594 int apiLevel = getApiLevel() + ("REL".equals(codeName) ? 0 : 1);
Julien Desprez3faaefd2016-05-24 19:25:12 +01001595 if (apiLevel < strictMinLevel){
1596 throw new IllegalArgumentException(String.format("%s not supported on %s. "
1597 + "Must be API %d.", feature, getSerialNumber(), strictMinLevel));
1598 }
1599 }
jdesprezd7670322017-08-30 14:12:28 -07001600
1601 @Override
1602 public File dumpHeap(String process, String devicePath) throws DeviceNotAvailableException {
1603 if (Strings.isNullOrEmpty(devicePath) || Strings.isNullOrEmpty(process)) {
1604 throw new IllegalArgumentException("devicePath or process cannot be null or empty.");
1605 }
1606 String pid = getProcessPid(process);
1607 if (pid == null) {
1608 return null;
1609 }
1610 File dump = dumpAndPullHeap(pid, devicePath);
1611 // Clean the device.
1612 executeShellCommand(String.format("rm %s", devicePath));
1613 return dump;
1614 }
1615
1616 /** Dump the heap file and pull it from the device. */
1617 private File dumpAndPullHeap(String pid, String devicePath) throws DeviceNotAvailableException {
1618 executeShellCommand(String.format(DUMPHEAP_CMD, pid, devicePath));
1619 // Allow a little bit of time for the file to populate on device side.
1620 int attempt = 0;
1621 // TODO: add an API to check device file size
1622 while (!doesFileExist(devicePath) && attempt < 3) {
1623 getRunUtil().sleep(DUMPHEAP_TIME);
1624 attempt++;
1625 }
1626 File dumpFile = pullFile(devicePath);
1627 return dumpFile;
1628 }
Brett Chabot74121d82010-01-28 20:14:27 -08001629}