• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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  */
16 package com.android.tradefed.device;
17 
18 import com.android.ddmlib.AdbCommandRejectedException;
19 import com.android.ddmlib.IDevice;
20 import com.android.ddmlib.InstallException;
21 import com.android.ddmlib.InstallReceiver;
22 import com.android.ddmlib.RawImage;
23 import com.android.ddmlib.ShellCommandUnresponsiveException;
24 import com.android.ddmlib.SyncException;
25 import com.android.ddmlib.TimeoutException;
26 import com.android.tradefed.log.LogUtil.CLog;
27 import com.android.tradefed.result.ByteArrayInputStreamSource;
28 import com.android.tradefed.result.FileInputStreamSource;
29 import com.android.tradefed.result.InputStreamSource;
30 import com.android.tradefed.util.CommandResult;
31 import com.android.tradefed.util.CommandStatus;
32 import com.android.tradefed.util.KeyguardControllerState;
33 import com.android.tradefed.util.RunUtil;
34 import com.android.tradefed.util.StreamUtil;
35 import com.android.tradefed.util.UserUtil;
36 
37 import com.google.common.annotations.VisibleForTesting;
38 import com.google.common.base.Strings;
39 
40 import java.awt.Image;
41 import java.awt.image.BufferedImage;
42 import java.io.ByteArrayOutputStream;
43 import java.io.File;
44 import java.io.IOException;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.HashMap;
48 import java.util.HashSet;
49 import java.util.List;
50 import java.util.Map;
51 import java.util.Set;
52 import java.util.concurrent.TimeUnit;
53 import java.util.regex.Matcher;
54 import java.util.regex.Pattern;
55 
56 import javax.imageio.ImageIO;
57 
58 /**
59  * Implementation of a {@link ITestDevice} for a full stack android device
60  */
61 public class TestDevice extends NativeDevice {
62 
63     /** number of attempts made to clear dialogs */
64     private static final int NUM_CLEAR_ATTEMPTS = 5;
65     /** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */
66     static final String DISMISS_DIALOG_CMD = "input keyevent 23";
67     /** Commands that can be used to dismiss the keyguard. */
68     public static final String DISMISS_KEYGUARD_CMD = "input keyevent 82";
69 
70     /**
71      * Alternative command to dismiss the keyguard by requesting the Window Manager service to do
72      * it. Api 23 and after.
73      */
74     static final String DISMISS_KEYGUARD_WM_CMD = "wm dismiss-keyguard";
75 
76     /** Timeout to wait for input dispatch to become ready **/
77     private static final long INPUT_DISPATCH_READY_TIMEOUT = 5 * 1000;
78     /** command to test input dispatch readiness **/
79     private static final String TEST_INPUT_CMD = "dumpsys input";
80 
81     private static final long AM_COMMAND_TIMEOUT = 10 * 1000;
82     private static final long CHECK_NEW_USER = 1000;
83 
84     static final String LIST_PACKAGES_CMD = "pm list packages -f";
85     private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)=(.*)");
86 
87     static final String LIST_APEXES_CMD = "pm list packages --apex-only --show-versioncode";
88     private static final Pattern APEXES_REGEX = Pattern.compile("package:(.*) versionCode:(.*)");
89 
90     private static final int FLAG_PRIMARY = 1; // From the UserInfo class
91 
92     private static final String[] SETTINGS_NAMESPACE = {"system", "secure", "global"};
93 
94     /** user pattern in the output of "pm list users" = TEXT{<id>:<name>:<flags>} TEXT * */
95     private static final String USER_PATTERN = "(.*?\\{)(\\d+)(:)(.*)(:)(\\d+)(\\}.*)";
96     /** Pattern to find the display ids of "dumpsys SurfaceFlinger" */
97     private static final String DISPLAY_ID_PATTERN = "(Display )(?<id>\\d+)( color modes:)";
98 
99     private static final int API_LEVEL_GET_CURRENT_USER = 24;
100     /** Timeout to wait for a screenshot before giving up to avoid hanging forever */
101     private static final long MAX_SCREENSHOT_TIMEOUT = 5 * 60 * 1000; // 5 min
102 
103     /** adb shell am dumpheap <service pid> <dump file path> */
104     private static final String DUMPHEAP_CMD = "am dumpheap %s %s";
105     /** Time given to a file to be dumped on device side */
106     private static final long DUMPHEAP_TIME = 5000l;
107 
108     /** Timeout in minutes for the package installation */
109     static final long INSTALL_TIMEOUT_MINUTES = 4;
110     /** Max timeout to output for package installation */
111     static final long INSTALL_TIMEOUT_TO_OUTPUT_MINUTES = 3;
112 
113     private boolean mWasWifiHelperInstalled = false;
114 
115     private static final String APEX_SUFFIX = ".apex";
116     private static final String APEX_ARG = "--apex";
117 
118     /**
119      * @param device
120      * @param stateMonitor
121      * @param allocationMonitor
122      */
TestDevice(IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor)123     public TestDevice(IDevice device, IDeviceStateMonitor stateMonitor,
124             IDeviceMonitor allocationMonitor) {
125         super(device, stateMonitor, allocationMonitor);
126     }
127 
128     /**
129      * Core implementation of package installation, with retries around
130      * {@link IDevice#installPackage(String, boolean, String...)}
131      * @param packageFile
132      * @param reinstall
133      * @param extraArgs
134      * @return the response from the installation
135      * @throws DeviceNotAvailableException
136      */
internalInstallPackage( final File packageFile, final boolean reinstall, final List<String> extraArgs)137     private String internalInstallPackage(
138             final File packageFile, final boolean reinstall, final List<String> extraArgs)
139                     throws DeviceNotAvailableException {
140         List<String> args = new ArrayList<>(extraArgs);
141         if (packageFile.getName().endsWith(APEX_SUFFIX)) {
142             args.add(APEX_ARG);
143         }
144         // use array to store response, so it can be returned to caller
145         final String[] response = new String[1];
146         DeviceAction installAction =
147                 new DeviceAction() {
148                     @Override
149                     public boolean run() throws InstallException {
150                         try {
151                             InstallReceiver receiver = createInstallReceiver();
152                             getIDevice()
153                                     .installPackage(
154                                             packageFile.getAbsolutePath(),
155                                             reinstall,
156                                             receiver,
157                                             INSTALL_TIMEOUT_MINUTES,
158                                             INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
159                                             TimeUnit.MINUTES,
160                                             args.toArray(new String[] {}));
161                             if (receiver.isSuccessfullyCompleted()) {
162                                 response[0] = null;
163                             } else if (receiver.getErrorMessage() == null) {
164                                 response[0] =
165                                         String.format(
166                                                 "Installation of %s timed out",
167                                                 packageFile.getAbsolutePath());
168                             } else {
169                                 response[0] = receiver.getErrorMessage();
170                             }
171                         } catch (InstallException e) {
172                             String message = e.getMessage();
173                             if (message == null) {
174                                 message =
175                                         String.format(
176                                                 "InstallException during package installation. "
177                                                         + "cause: %s",
178                                                 StreamUtil.getStackTrace(e));
179                             }
180                             response[0] = message;
181                         }
182                         return response[0] == null;
183                     }
184                 };
185         performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
186                 installAction, MAX_RETRY_ATTEMPTS);
187         return response[0];
188     }
189 
190     /**
191      * Creates and return an {@link InstallReceiver} for {@link #internalInstallPackage(File,
192      * boolean, List)} and {@link #installPackage(File, File, boolean, String...)} testing.
193      */
194     @VisibleForTesting
createInstallReceiver()195     InstallReceiver createInstallReceiver() {
196         return new InstallReceiver();
197     }
198 
199     /**
200      * {@inheritDoc}
201      */
202     @Override
installPackage(final File packageFile, final boolean reinstall, final String... extraArgs)203     public String installPackage(final File packageFile, final boolean reinstall,
204             final String... extraArgs) throws DeviceNotAvailableException {
205         boolean runtimePermissionSupported = isRuntimePermissionSupported();
206         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
207         // grant all permissions by default if feature is supported
208         if (runtimePermissionSupported) {
209             args.add("-g");
210         }
211         return internalInstallPackage(packageFile, reinstall, args);
212     }
213 
214     /**
215      * {@inheritDoc}
216      */
217     @Override
installPackage(File packageFile, boolean reinstall, boolean grantPermissions, String... extraArgs)218     public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
219             String... extraArgs) throws DeviceNotAvailableException {
220         ensureRuntimePermissionSupported();
221         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
222         if (grantPermissions) {
223             args.add("-g");
224         }
225         return internalInstallPackage(packageFile, reinstall, args);
226     }
227 
228     /**
229      * {@inheritDoc}
230      */
231     @Override
installPackageForUser(File packageFile, boolean reinstall, int userId, String... extraArgs)232     public String installPackageForUser(File packageFile, boolean reinstall, int userId,
233             String... extraArgs) throws DeviceNotAvailableException {
234         boolean runtimePermissionSupported = isRuntimePermissionSupported();
235         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
236         // grant all permissions by default if feature is supported
237         if (runtimePermissionSupported) {
238             args.add("-g");
239         }
240         args.add("--user");
241         args.add(Integer.toString(userId));
242         return internalInstallPackage(packageFile, reinstall, args);
243     }
244 
245     /**
246      * {@inheritDoc}
247      */
248     @Override
installPackageForUser(File packageFile, boolean reinstall, boolean grantPermissions, int userId, String... extraArgs)249     public String installPackageForUser(File packageFile, boolean reinstall,
250             boolean grantPermissions, int userId, String... extraArgs)
251                     throws DeviceNotAvailableException {
252         ensureRuntimePermissionSupported();
253         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
254         if (grantPermissions) {
255             args.add("-g");
256         }
257         args.add("--user");
258         args.add(Integer.toString(userId));
259         return internalInstallPackage(packageFile, reinstall, args);
260     }
261 
installPackage(final File packageFile, final File certFile, final boolean reinstall, final String... extraArgs)262     public String installPackage(final File packageFile, final File certFile,
263             final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException {
264         // use array to store response, so it can be returned to caller
265         final String[] response = new String[1];
266         DeviceAction installAction =
267                 new DeviceAction() {
268                     @Override
269                     public boolean run()
270                             throws InstallException, SyncException, IOException, TimeoutException,
271                                     AdbCommandRejectedException {
272                         // TODO: create a getIDevice().installPackage(File, File...) method when the
273                         // dist cert functionality is ready to be open sourced
274                         String remotePackagePath =
275                                 getIDevice().syncPackageToDevice(packageFile.getAbsolutePath());
276                         String remoteCertPath =
277                                 getIDevice().syncPackageToDevice(certFile.getAbsolutePath());
278                         // trick installRemotePackage into issuing a 'pm install <apk> <cert>'
279                         // command, by adding apk path to extraArgs, and using cert as the
280                         // 'apk file'.
281                         String[] newExtraArgs = new String[extraArgs.length + 1];
282                         System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length);
283                         newExtraArgs[newExtraArgs.length - 1] =
284                                 String.format("\"%s\"", remotePackagePath);
285                         try {
286                             InstallReceiver receiver = createInstallReceiver();
287                             getIDevice()
288                                     .installRemotePackage(
289                                             remoteCertPath,
290                                             reinstall,
291                                             receiver,
292                                             INSTALL_TIMEOUT_MINUTES,
293                                             INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
294                                             TimeUnit.MINUTES,
295                                             newExtraArgs);
296                             if (receiver.isSuccessfullyCompleted()) {
297                                 response[0] = null;
298                             } else if (receiver.getErrorMessage() == null) {
299                                 response[0] =
300                                         String.format(
301                                                 "Installation of %s timed out.",
302                                                 packageFile.getAbsolutePath());
303                             } else {
304                                 response[0] = receiver.getErrorMessage();
305                             }
306                         } catch (InstallException e) {
307                             String message = e.getMessage();
308                             if (message == null) {
309                                 message =
310                                         String.format(
311                                                 "InstallException during package installation. "
312                                                         + "cause: %s",
313                                                 StreamUtil.getStackTrace(e));
314                             }
315                             response[0] = message;
316                         } finally {
317                             getIDevice().removeRemotePackage(remotePackagePath);
318                             getIDevice().removeRemotePackage(remoteCertPath);
319                         }
320                         return true;
321                     }
322                 };
323         performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
324                 installAction, MAX_RETRY_ATTEMPTS);
325         return response[0];
326     }
327 
328     /**
329      * {@inheritDoc}
330      */
331     @Override
uninstallPackage(final String packageName)332     public String uninstallPackage(final String packageName) throws DeviceNotAvailableException {
333         // use array to store response, so it can be returned to caller
334         final String[] response = new String[1];
335         DeviceAction uninstallAction = new DeviceAction() {
336             @Override
337             public boolean run() throws InstallException {
338                 CLog.d("Uninstalling %s", packageName);
339                 String result = getIDevice().uninstallPackage(packageName);
340                 response[0] = result;
341                 return result == null;
342             }
343         };
344         performDeviceAction(String.format("uninstall %s", packageName), uninstallAction,
345                 MAX_RETRY_ATTEMPTS);
346         return response[0];
347     }
348 
349     /**
350      * Core implementation for installing application with split apk files {@link
351      * IDevice#installPackages(List, boolean, List)} See
352      * "https://developer.android.com/studio/build/configure-apk-splits" on how to split apk to
353      * several files.
354      *
355      * @param packageFiles the local apk files
356      * @param reinstall <code>true</code> if a reinstall should be performed
357      * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
358      *     available options.
359      * @return the response from the installation <code>null</code> if installation succeeds.
360      * @throws DeviceNotAvailableException
361      */
internalInstallPackages( final List<File> packageFiles, final boolean reinstall, final List<String> extraArgs)362     private String internalInstallPackages(
363             final List<File> packageFiles, final boolean reinstall, final List<String> extraArgs)
364             throws DeviceNotAvailableException {
365         // use array to store response, so it can be returned to caller
366         final String[] response = new String[1];
367         DeviceAction installAction =
368                 new DeviceAction() {
369                     @Override
370                     public boolean run() throws InstallException {
371                         try {
372                             getIDevice()
373                                     .installPackages(
374                                             packageFiles,
375                                             reinstall,
376                                             extraArgs,
377                                             INSTALL_TIMEOUT_MINUTES,
378                                             TimeUnit.MINUTES);
379                             response[0] = null;
380                             return true;
381                         } catch (InstallException e) {
382                             response[0] = e.getMessage();
383                             if (response[0] == null) {
384                                 response[0] =
385                                         String.format(
386                                                 "InstallException: %s",
387                                                 StreamUtil.getStackTrace(e));
388                             }
389                             return false;
390                         }
391                     }
392                 };
393         performDeviceAction(
394                 String.format("install %s", packageFiles.toString()),
395                 installAction,
396                 MAX_RETRY_ATTEMPTS);
397         return response[0];
398     }
399 
400     /** {@inheritDoc} */
401     @Override
installPackages( final List<File> packageFiles, final boolean reinstall, final String... extraArgs)402     public String installPackages(
403             final List<File> packageFiles, final boolean reinstall, final String... extraArgs)
404             throws DeviceNotAvailableException {
405         // Grant all permissions by default if feature is supported
406         return installPackages(packageFiles, reinstall, isRuntimePermissionSupported(), extraArgs);
407     }
408 
409     /** {@inheritDoc} */
410     @Override
installPackages( List<File> packageFiles, boolean reinstall, boolean grantPermissions, String... extraArgs)411     public String installPackages(
412             List<File> packageFiles,
413             boolean reinstall,
414             boolean grantPermissions,
415             String... extraArgs)
416             throws DeviceNotAvailableException {
417         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
418         if (grantPermissions) {
419             ensureRuntimePermissionSupported();
420             args.add("-g");
421         }
422         return internalInstallPackages(packageFiles, reinstall, args);
423     }
424 
425     /** {@inheritDoc} */
426     @Override
installPackagesForUser( List<File> packageFiles, boolean reinstall, int userId, String... extraArgs)427     public String installPackagesForUser(
428             List<File> packageFiles, boolean reinstall, int userId, String... extraArgs)
429             throws DeviceNotAvailableException {
430         // Grant all permissions by default if feature is supported
431         return installPackagesForUser(
432                 packageFiles, reinstall, isRuntimePermissionSupported(), userId, extraArgs);
433     }
434 
435     /** {@inheritDoc} */
436     @Override
installPackagesForUser( List<File> packageFiles, boolean reinstall, boolean grantPermissions, int userId, String... extraArgs)437     public String installPackagesForUser(
438             List<File> packageFiles,
439             boolean reinstall,
440             boolean grantPermissions,
441             int userId,
442             String... extraArgs)
443             throws DeviceNotAvailableException {
444         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
445         if (grantPermissions) {
446             ensureRuntimePermissionSupported();
447             args.add("-g");
448         }
449         args.add("--user");
450         args.add(Integer.toString(userId));
451         return internalInstallPackages(packageFiles, reinstall, args);
452     }
453 
454     /**
455      * Core implementation for split apk remote installation {@link IDevice#installPackage(String,
456      * boolean, String...)} See "https://developer.android.com/studio/build/configure-apk-splits" on
457      * how to split apk to several files.
458      *
459      * @param remoteApkPaths the remote apk file paths
460      * @param reinstall <code>true</code> if a reinstall should be performed
461      * @param extraArgs optional extra arguments to pass. See 'adb shell pm install --help' for
462      *     available options.
463      * @return the response from the installation <code>null</code> if installation succeeds.
464      * @throws DeviceNotAvailableException
465      */
internalInstallRemotePackages( final List<String> remoteApkPaths, final boolean reinstall, final List<String> extraArgs)466     private String internalInstallRemotePackages(
467             final List<String> remoteApkPaths,
468             final boolean reinstall,
469             final List<String> extraArgs)
470             throws DeviceNotAvailableException {
471         // use array to store response, so it can be returned to caller
472         final String[] response = new String[1];
473         DeviceAction installAction =
474                 new DeviceAction() {
475                     @Override
476                     public boolean run() throws InstallException {
477                         try {
478                             getIDevice()
479                                     .installRemotePackages(
480                                             remoteApkPaths,
481                                             reinstall,
482                                             extraArgs,
483                                             INSTALL_TIMEOUT_MINUTES,
484                                             TimeUnit.MINUTES);
485                             response[0] = null;
486                             return true;
487                         } catch (InstallException e) {
488                             response[0] = e.getMessage();
489                             if (response[0] == null) {
490                                 response[0] = String.format(
491                                     "InstallException during package installation. cause: %s",
492                                     StreamUtil.getStackTrace(e));
493                             }
494                             return false;
495                         }
496                     }
497                 };
498         performDeviceAction(
499                 String.format("install %s", remoteApkPaths.toString()),
500                 installAction,
501                 MAX_RETRY_ATTEMPTS);
502         return response[0];
503     }
504 
505     /** {@inheritDoc} */
506     @Override
installRemotePackages( final List<String> remoteApkPaths, final boolean reinstall, final String... extraArgs)507     public String installRemotePackages(
508             final List<String> remoteApkPaths, final boolean reinstall, final String... extraArgs)
509             throws DeviceNotAvailableException {
510         // Grant all permissions by default if feature is supported
511         return installRemotePackages(
512                 remoteApkPaths, reinstall, isRuntimePermissionSupported(), extraArgs);
513     }
514 
515     /** {@inheritDoc} */
516     @Override
installRemotePackages( List<String> remoteApkPaths, boolean reinstall, boolean grantPermissions, String... extraArgs)517     public String installRemotePackages(
518             List<String> remoteApkPaths,
519             boolean reinstall,
520             boolean grantPermissions,
521             String... extraArgs)
522             throws DeviceNotAvailableException {
523         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
524         if (grantPermissions) {
525             ensureRuntimePermissionSupported();
526             args.add("-g");
527         }
528         return internalInstallRemotePackages(remoteApkPaths, reinstall, args);
529     }
530 
531     /** {@inheritDoc} */
532     @Override
getScreenshot()533     public InputStreamSource getScreenshot() throws DeviceNotAvailableException {
534         return getScreenshot("PNG");
535     }
536 
537     /**
538      * {@inheritDoc}
539      */
540     @Override
getScreenshot(String format)541     public InputStreamSource getScreenshot(String format) throws DeviceNotAvailableException {
542         return getScreenshot(format, true);
543     }
544 
545     /** {@inheritDoc} */
546     @Override
getScreenshot(String format, boolean rescale)547     public InputStreamSource getScreenshot(String format, boolean rescale)
548             throws DeviceNotAvailableException {
549         if (!format.equalsIgnoreCase("PNG") && !format.equalsIgnoreCase("JPEG")){
550             CLog.e("Screenshot: Format %s is not supported, defaulting to PNG.", format);
551             format = "PNG";
552         }
553         ScreenshotAction action = new ScreenshotAction();
554         if (performDeviceAction("screenshot", action, MAX_RETRY_ATTEMPTS)) {
555             byte[] imageData =
556                     compressRawImage(action.mRawScreenshot, format.toUpperCase(), rescale);
557             if (imageData != null) {
558                 return new ByteArrayInputStreamSource(imageData);
559             }
560         }
561         // Return an error in the buffer
562         return new ByteArrayInputStreamSource(
563                 "Error: device reported null for screenshot.".getBytes());
564     }
565 
566     /** {@inheritDoc} */
567     @Override
getScreenshot(int displayId)568     public InputStreamSource getScreenshot(int displayId) throws DeviceNotAvailableException {
569         final String tmpDevicePath = String.format("/data/local/tmp/display_%s.png", displayId);
570         CommandResult result =
571                 executeShellV2Command(
572                         String.format("screencap -p -d %s %s", displayId, tmpDevicePath));
573         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
574             // Return an error in the buffer
575             CLog.e("Error: device reported error for screenshot: %s", result.getStderr());
576             return null;
577         }
578         try {
579             File tmpScreenshot = pullFile(tmpDevicePath);
580             if (tmpScreenshot == null) {
581                 return null;
582             }
583             return new FileInputStreamSource(tmpScreenshot, true);
584         } finally {
585             deleteFile(tmpDevicePath);
586         }
587     }
588 
589     private class ScreenshotAction implements DeviceAction {
590 
591         RawImage mRawScreenshot;
592 
593         /**
594          * {@inheritDoc}
595          */
596         @Override
run()597         public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
598                 ShellCommandUnresponsiveException, InstallException, SyncException {
599             mRawScreenshot =
600                     getIDevice().getScreenshot(MAX_SCREENSHOT_TIMEOUT, TimeUnit.MILLISECONDS);
601             return mRawScreenshot != null;
602         }
603     }
604 
605     /**
606      * Helper to compress a rawImage obtained from the screen.
607      *
608      * @param rawImage {@link RawImage} to compress.
609      * @param format resulting format of compressed image. PNG and JPEG are supported.
610      * @param rescale if rescaling should be done to further reduce size of compressed image.
611      * @return compressed image.
612      */
613     @VisibleForTesting
compressRawImage(RawImage rawImage, String format, boolean rescale)614     byte[] compressRawImage(RawImage rawImage, String format, boolean rescale) {
615         BufferedImage image = rawImageToBufferedImage(rawImage, format);
616 
617         // Rescale to reduce size if needed
618         // Screenshot default format is 1080 x 1920, 8-bit/color RGBA
619         // By cutting in half we can easily keep good quality and smaller size
620         if (rescale) {
621             image = rescaleImage(image);
622         }
623 
624         return getImageData(image, format);
625     }
626 
627     /**
628      * Converts {@link RawImage} to {@link BufferedImage} in specified format.
629      *
630      * @param rawImage {@link RawImage} to convert.
631      * @param format resulting format of image. PNG and JPEG are supported.
632      * @return converted image.
633      */
634     @VisibleForTesting
rawImageToBufferedImage(RawImage rawImage, String format)635     BufferedImage rawImageToBufferedImage(RawImage rawImage, String format) {
636         BufferedImage image = null;
637 
638         if ("JPEG".equalsIgnoreCase(format)) {
639             //JPEG does not support ARGB without a special encoder
640             image =
641                     new BufferedImage(
642                             rawImage.width, rawImage.height, BufferedImage.TYPE_3BYTE_BGR);
643         }
644         else {
645             image = new BufferedImage(rawImage.width, rawImage.height, BufferedImage.TYPE_INT_ARGB);
646         }
647 
648         // borrowed conversion logic from platform/sdk/screenshot/.../Screenshot.java
649         int index = 0;
650         int IndexInc = rawImage.bpp >> 3;
651         for (int y = 0 ; y < rawImage.height ; y++) {
652             for (int x = 0 ; x < rawImage.width ; x++) {
653                 int value = rawImage.getARGB(index);
654                 index += IndexInc;
655                 image.setRGB(x, y, value);
656             }
657         }
658 
659         return image;
660     }
661 
662     /**
663      * Rescales image cutting it in half.
664      *
665      * @param image source {@link BufferedImage}.
666      * @return resulting scaled image.
667      */
668     @VisibleForTesting
rescaleImage(BufferedImage image)669     BufferedImage rescaleImage(BufferedImage image) {
670         int shortEdge = Math.min(image.getHeight(), image.getWidth());
671         if (shortEdge > 720) {
672             Image resized =
673                     image.getScaledInstance(
674                             image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_SMOOTH);
675             image =
676                     new BufferedImage(
677                             image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_REPLICATE);
678             image.getGraphics().drawImage(resized, 0, 0, null);
679         }
680         return image;
681     }
682 
683     /**
684      * Gets byte array representation of {@link BufferedImage}.
685      *
686      * @param image source {@link BufferedImage}.
687      * @param format resulting format of image. PNG and JPEG are supported.
688      * @return byte array representation of the image.
689      */
690     @VisibleForTesting
getImageData(BufferedImage image, String format)691     byte[] getImageData(BufferedImage image, String format) {
692         // store compressed image in memory, and let callers write to persistent storage
693         // use initial buffer size of 128K
694         byte[] imageData = null;
695         ByteArrayOutputStream imageOut = new ByteArrayOutputStream(128*1024);
696         try {
697             if (ImageIO.write(image, format, imageOut)) {
698                 imageData = imageOut.toByteArray();
699             } else {
700                 CLog.e("Failed to compress screenshot to png");
701             }
702         } catch (IOException e) {
703             CLog.e("Failed to compress screenshot to png");
704             CLog.e(e);
705         }
706         StreamUtil.close(imageOut);
707         return imageData;
708     }
709 
710     /**
711      * {@inheritDoc}
712      */
713     @Override
clearErrorDialogs()714     public boolean clearErrorDialogs() throws DeviceNotAvailableException {
715         // attempt to clear error dialogs multiple times
716         for (int i = 0; i < NUM_CLEAR_ATTEMPTS; i++) {
717             int numErrorDialogs = getErrorDialogCount();
718             if (numErrorDialogs == 0) {
719                 return true;
720             }
721             doClearDialogs(numErrorDialogs);
722         }
723         if (getErrorDialogCount() > 0) {
724             // at this point, all attempts to clear error dialogs completely have failed
725             // it might be the case that the process keeps showing new dialogs immediately after
726             // clearing. There's really no workaround, but to dump an error
727             CLog.e("error dialogs still exist on %s.", getSerialNumber());
728             return false;
729         }
730         return true;
731     }
732 
733     /**
734      * Detects the number of crash or ANR dialogs currently displayed.
735      * <p/>
736      * Parses output of 'dump activity processes'
737      *
738      * @return count of dialogs displayed
739      * @throws DeviceNotAvailableException
740      */
getErrorDialogCount()741     private int getErrorDialogCount() throws DeviceNotAvailableException {
742         int errorDialogCount = 0;
743         Pattern crashPattern = Pattern.compile(".*crashing=true.*AppErrorDialog.*");
744         Pattern anrPattern = Pattern.compile(".*notResponding=true.*AppNotRespondingDialog.*");
745         String systemStatusOutput =
746                 executeShellCommand(
747                         "dumpsys activity processes | grep -e .*crashing=true.*AppErrorDialog.* -e .*notResponding=true.*AppNotRespondingDialog.*");
748         Matcher crashMatcher = crashPattern.matcher(systemStatusOutput);
749         while (crashMatcher.find()) {
750             errorDialogCount++;
751         }
752         Matcher anrMatcher = anrPattern.matcher(systemStatusOutput);
753         while (anrMatcher.find()) {
754             errorDialogCount++;
755         }
756 
757         return errorDialogCount;
758     }
759 
doClearDialogs(int numDialogs)760     private void doClearDialogs(int numDialogs) throws DeviceNotAvailableException {
761         CLog.i("Attempted to clear %d dialogs on %s", numDialogs, getSerialNumber());
762         for (int i=0; i < numDialogs; i++) {
763             // send DPAD_CENTER
764             executeShellCommand(DISMISS_DIALOG_CMD);
765         }
766     }
767 
768     /**
769      * {@inheritDoc}
770      */
771     @Override
disableKeyguard()772     public void disableKeyguard() throws DeviceNotAvailableException {
773         long start = System.currentTimeMillis();
774         while (true) {
775             Boolean ready = isDeviceInputReady();
776             if (ready == null) {
777                 // unsupported API level, bail
778                 break;
779             }
780             if (ready) {
781                 // input dispatch is ready, bail
782                 break;
783             }
784             long timeSpent = System.currentTimeMillis() - start;
785             if (timeSpent > INPUT_DISPATCH_READY_TIMEOUT) {
786                 CLog.w("Timeout after waiting %dms on enabling of input dispatch", timeSpent);
787                 // break & proceed anyway
788                 break;
789             } else {
790                 getRunUtil().sleep(1000);
791             }
792         }
793         if (getApiLevel() >= 23) {
794             CLog.i(
795                     "Attempting to disable keyguard on %s using %s",
796                     getSerialNumber(), DISMISS_KEYGUARD_WM_CMD);
797             String output = executeShellCommand(DISMISS_KEYGUARD_WM_CMD);
798             CLog.i("output of %s: %s", DISMISS_KEYGUARD_WM_CMD, output);
799         } else {
800             CLog.i("Command: %s, is not supported, falling back to %s", DISMISS_KEYGUARD_WM_CMD,
801                     DISMISS_KEYGUARD_CMD);
802             executeShellCommand(DISMISS_KEYGUARD_CMD);
803         }
804         // TODO: check that keyguard was actually dismissed.
805     }
806 
807     /** {@inheritDoc} */
808     @Override
getKeyguardState()809     public KeyguardControllerState getKeyguardState() throws DeviceNotAvailableException {
810         String output =
811                 executeShellCommand("dumpsys activity activities | grep -A3 KeyguardController:");
812         CLog.d("Output from KeyguardController: %s", output);
813         KeyguardControllerState state =
814                 KeyguardControllerState.create(Arrays.asList(output.trim().split("\n")));
815         return state;
816     }
817 
818     /**
819      * Tests the device to see if input dispatcher is ready
820      *
821      * @return <code>null</code> if not supported by platform, or the actual readiness state
822      * @throws DeviceNotAvailableException
823      */
isDeviceInputReady()824     Boolean isDeviceInputReady() throws DeviceNotAvailableException {
825         CollectingOutputReceiver receiver = new CollectingOutputReceiver();
826         executeShellCommand(TEST_INPUT_CMD, receiver);
827         String output = receiver.getOutput();
828         Matcher m = INPUT_DISPATCH_STATE_REGEX.matcher(output);
829         if (!m.find()) {
830             // output does not contain the line at all, implying unsupported API level, bail
831             return null;
832         }
833         return "1".equals(m.group(1));
834     }
835 
836     /**
837      * {@inheritDoc}
838      */
839     @Override
prePostBootSetup()840     protected void prePostBootSetup() throws DeviceNotAvailableException {
841         if (mOptions.isDisableKeyguard()) {
842             disableKeyguard();
843         }
844     }
845 
846     /**
847      * Performs an reboot via framework power manager
848      *
849      * Must have root access, device must be API Level 18 or above
850      *
851      * @param into the mode to reboot into, currently supported: bootloader, recovery, leave it
852      *         null for a plain reboot
853      * @return <code>true</code> if the device rebooted, <code>false</code> if not successful or
854      *          unsupported
855      * @throws DeviceNotAvailableException
856      */
doAdbFrameworkReboot(final String into)857     private boolean doAdbFrameworkReboot(final String into) throws DeviceNotAvailableException {
858         // use framework reboot when:
859         // 1. device API level >= 18
860         // 2. has adb root
861         // 3. framework is running
862         if (!isEnableAdbRoot()) {
863             CLog.i("framework reboot is not supported; when enable root is disabled");
864             return false;
865         }
866         enableAdbRoot();
867         if (getApiLevel() >= 18 && isAdbRoot()) {
868             try {
869                 // check framework running
870                 String output = executeShellCommand("pm path android");
871                 if (output == null || !output.contains("package:")) {
872                     CLog.v("framework reboot: can't detect framework running");
873                     return false;
874                 }
875                 String command = "svc power reboot";
876                 if (into != null && !into.isEmpty()) {
877                     command = String.format("%s %s", command, into);
878                 }
879                 executeShellCommand(command);
880             } catch (DeviceUnresponsiveException due) {
881                 CLog.v("framework reboot: device unresponsive to shell command, using fallback");
882                 return false;
883             }
884             boolean notAvailable = waitForDeviceNotAvailable(30 * 1000);
885             if (notAvailable) {
886                 postAdbReboot();
887             }
888             return notAvailable;
889         } else {
890             CLog.v("framework reboot: not supported");
891             return false;
892         }
893     }
894 
895     /**
896      * Perform a adb reboot.
897      *
898      * @param into the bootloader name to reboot into, or <code>null</code> to just reboot the
899      *            device.
900      * @throws DeviceNotAvailableException
901      */
902     @Override
doAdbReboot(final String into)903     protected void doAdbReboot(final String into) throws DeviceNotAvailableException {
904         if (!doAdbFrameworkReboot(into)) {
905             super.doAdbReboot(into);
906         }
907     }
908 
909     /**
910      * {@inheritDoc}
911      */
912     @Override
getInstalledPackageNames()913     public Set<String> getInstalledPackageNames() throws DeviceNotAvailableException {
914         return getInstalledPackageNames(null, null);
915     }
916 
917     /** {@inheritDoc} */
918     @Override
isPackageInstalled(String packageName)919     public boolean isPackageInstalled(String packageName) throws DeviceNotAvailableException {
920         return getInstalledPackageNames(packageName, null).contains(packageName);
921     }
922 
923     /** {@inheritDoc} */
924     @Override
isPackageInstalled(String packageName, String userId)925     public boolean isPackageInstalled(String packageName, String userId)
926             throws DeviceNotAvailableException {
927         return getInstalledPackageNames(packageName, userId).contains(packageName);
928     }
929 
930     /** {@inheritDoc} */
931     @Override
getActiveApexes()932     public Set<ApexInfo> getActiveApexes() throws DeviceNotAvailableException {
933         Set<ApexInfo> ret = new HashSet<>();
934         String output = executeShellCommand(LIST_APEXES_CMD);
935         if (output != null) {
936             Matcher m = APEXES_REGEX.matcher(output);
937             while (m.find()) {
938                 String name = m.group(1);
939                 long version = Long.valueOf(m.group(2));
940                 ret.add(new ApexInfo(name, version));
941             }
942         }
943         return ret;
944     }
945 
946     /**
947      * A {@link com.android.tradefed.device.NativeDevice.DeviceAction}
948      * for retrieving package system service info, and do retries on
949      * failures.
950      */
951     private class DumpPkgAction implements DeviceAction {
952 
953         Map<String, PackageInfo> mPkgInfoMap;
954 
DumpPkgAction()955         DumpPkgAction() {
956         }
957 
958         @Override
run()959         public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
960                 ShellCommandUnresponsiveException, InstallException, SyncException {
961             DumpsysPackageReceiver receiver = new DumpsysPackageReceiver();
962             getIDevice().executeShellCommand("dumpsys package p", receiver);
963             mPkgInfoMap = receiver.getPackages();
964             if (mPkgInfoMap.size() == 0) {
965                 // Package parsing can fail if package manager is currently down. throw exception
966                 // to retry
967                 CLog.w("no packages found from dumpsys package p.");
968                 throw new IOException();
969             }
970             return true;
971         }
972     }
973 
974     /**
975      * {@inheritDoc}
976      */
977     @Override
getUninstallablePackageNames()978     public Set<String> getUninstallablePackageNames() throws DeviceNotAvailableException {
979         DumpPkgAction action = new DumpPkgAction();
980         performDeviceAction("dumpsys package p", action, MAX_RETRY_ATTEMPTS);
981 
982         Set<String> pkgs = new HashSet<String>();
983         for (PackageInfo pkgInfo : action.mPkgInfoMap.values()) {
984             if (!pkgInfo.isSystemApp() || pkgInfo.isUpdatedSystemApp()) {
985                 CLog.d("Found uninstallable package %s", pkgInfo.getPackageName());
986                 pkgs.add(pkgInfo.getPackageName());
987             }
988         }
989         return pkgs;
990     }
991 
992     /**
993      * {@inheritDoc}
994      */
995     @Override
getAppPackageInfo(String packageName)996     public PackageInfo getAppPackageInfo(String packageName) throws DeviceNotAvailableException {
997         DumpPkgAction action = new DumpPkgAction();
998         performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS);
999         return action.mPkgInfoMap.get(packageName);
1000     }
1001 
1002     // TODO: convert this to use DumpPkgAction
getInstalledPackageNames(String packageNameSearched, String userId)1003     private Set<String> getInstalledPackageNames(String packageNameSearched, String userId)
1004             throws DeviceNotAvailableException {
1005         Set<String> packages= new HashSet<String>();
1006         String command = LIST_PACKAGES_CMD;
1007         if (userId != null) {
1008             command += String.format(" --user %s", userId);
1009         }
1010         if (packageNameSearched != null) {
1011             command += (" | grep " + packageNameSearched);
1012         }
1013         String output = executeShellCommand(command);
1014         if (output != null) {
1015             Matcher m = PACKAGE_REGEX.matcher(output);
1016             while (m.find()) {
1017                 String packageName = m.group(2);
1018                 if (packageNameSearched != null && packageName.equals(packageNameSearched)) {
1019                     packages.add(packageName);
1020                 } else if (packageNameSearched == null) {
1021                     packages.add(packageName);
1022                 }
1023             }
1024         }
1025         return packages;
1026     }
1027 
1028     /**
1029      * {@inheritDoc}
1030      */
1031     @Override
listUsers()1032     public ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
1033         ArrayList<String[]> users = tokenizeListUsers();
1034         ArrayList<Integer> userIds = new ArrayList<Integer>(users.size());
1035         for (String[] user : users) {
1036             userIds.add(Integer.parseInt(user[1]));
1037         }
1038         return userIds;
1039     }
1040 
1041     /**
1042      * Tokenizes the output of 'pm list users'.
1043      * The returned tokens for each user have the form: {"\tUserInfo", Integer.toString(id), name,
1044      * Integer.toHexString(flag), "[running]"}; (the last one being optional)
1045      * @return a list of arrays of strings, each element of the list representing the tokens
1046      * for a user, or {@code null} if there was an error while tokenizing the adb command output.
1047      */
tokenizeListUsers()1048     private ArrayList<String[]> tokenizeListUsers() throws DeviceNotAvailableException {
1049         String command = "pm list users";
1050         String commandOutput = executeShellCommand(command);
1051         // Extract the id of all existing users.
1052         String[] lines = commandOutput.split("\\r?\\n");
1053         if (!lines[0].equals("Users:")) {
1054             throw new DeviceRuntimeException(
1055                     String.format("'%s' in not a valid output for 'pm list users'", commandOutput));
1056         }
1057         ArrayList<String[]> users = new ArrayList<String[]>(lines.length - 1);
1058         for (int i = 1; i < lines.length; i++) {
1059             // Individual user is printed out like this:
1060             // \tUserInfo{$id$:$name$:$Integer.toHexString(flags)$} [running]
1061             String[] tokens = lines[i].split("\\{|\\}|:");
1062             if (tokens.length != 4 && tokens.length != 5) {
1063                 throw new DeviceRuntimeException(
1064                         String.format(
1065                                 "device output: '%s' \nline: '%s' was not in the expected "
1066                                         + "format for user info.",
1067                                 commandOutput, lines[i]));
1068             }
1069             users.add(tokens);
1070         }
1071         return users;
1072     }
1073 
1074     /**
1075      * {@inheritDoc}
1076      */
1077     @Override
getMaxNumberOfUsersSupported()1078     public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
1079         String command = "pm get-max-users";
1080         String commandOutput = executeShellCommand(command);
1081         try {
1082             return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
1083         } catch (NumberFormatException e) {
1084             CLog.e("Failed to parse result: %s", commandOutput);
1085         }
1086         return 0;
1087     }
1088 
1089     /** {@inheritDoc} */
1090     @Override
getMaxNumberOfRunningUsersSupported()1091     public int getMaxNumberOfRunningUsersSupported() throws DeviceNotAvailableException {
1092         checkApiLevelAgainstNextRelease("get-max-running-users", 28);
1093         String command = "pm get-max-running-users";
1094         String commandOutput = executeShellCommand(command);
1095         try {
1096             return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
1097         } catch (NumberFormatException e) {
1098             CLog.e("Failed to parse result: %s", commandOutput);
1099         }
1100         return 0;
1101     }
1102 
1103     /**
1104      * {@inheritDoc}
1105      */
1106     @Override
isMultiUserSupported()1107     public boolean isMultiUserSupported() throws DeviceNotAvailableException {
1108         return getMaxNumberOfUsersSupported() > 1;
1109     }
1110 
1111     /**
1112      * {@inheritDoc}
1113      */
1114     @Override
createUser(String name)1115     public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException {
1116         return createUser(name, false, false);
1117     }
1118 
1119     /** {@inheritDoc} */
1120     @Override
createUserNoThrow(String name)1121     public int createUserNoThrow(String name) throws DeviceNotAvailableException {
1122         try {
1123             return createUser(name);
1124         } catch (IllegalStateException e) {
1125             CLog.e("Error creating user: " + e.toString());
1126             return -1;
1127         }
1128     }
1129 
1130     /**
1131      * {@inheritDoc}
1132      */
1133     @Override
createUser(String name, boolean guest, boolean ephemeral)1134     public int createUser(String name, boolean guest, boolean ephemeral)
1135             throws DeviceNotAvailableException, IllegalStateException {
1136         String command ="pm create-user " + (guest ? "--guest " : "")
1137                 + (ephemeral ? "--ephemeral " : "") + name;
1138         final String output = executeShellCommand(command);
1139         if (output.startsWith("Success")) {
1140             try {
1141                 resetContentProviderSetup();
1142                 return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim());
1143             } catch (NumberFormatException e) {
1144                 CLog.e("Failed to parse result: %s", output);
1145             }
1146         }
1147         throw new IllegalStateException(String.format("Failed to create user: %s", output));
1148     }
1149 
1150     /**
1151      * {@inheritDoc}
1152      */
1153     @Override
removeUser(int userId)1154     public boolean removeUser(int userId) throws DeviceNotAvailableException {
1155         final String output = executeShellCommand(String.format("pm remove-user %s", userId));
1156         if (output.startsWith("Error")) {
1157             CLog.e("Failed to remove user: %s", output);
1158             return false;
1159         }
1160         return true;
1161     }
1162 
1163     /**
1164      * {@inheritDoc}
1165      */
1166     @Override
startUser(int userId)1167     public boolean startUser(int userId) throws DeviceNotAvailableException {
1168         return startUser(userId, false);
1169     }
1170 
1171     /** {@inheritDoc} */
1172     @Override
startUser(int userId, boolean waitFlag)1173     public boolean startUser(int userId, boolean waitFlag) throws DeviceNotAvailableException {
1174         if (waitFlag) {
1175             checkApiLevelAgainstNextRelease("start-user -w", 29);
1176         }
1177         String cmd = "am start-user " + (waitFlag ? "-w " : "") + userId;
1178 
1179         CLog.d("Starting user with command: %s", cmd);
1180         final String output = executeShellCommand(cmd);
1181         if (output.startsWith("Error")) {
1182             CLog.e("Failed to start user: %s", output);
1183             return false;
1184         }
1185         if (waitFlag) {
1186             String state = executeShellCommand("am get-started-user-state " + userId);
1187             if (!state.contains("RUNNING_UNLOCKED")) {
1188                 CLog.w("User %s is not RUNNING_UNLOCKED after start-user -w. (%s).", userId, state);
1189                 return false;
1190             }
1191         }
1192         return true;
1193     }
1194 
1195     /**
1196      * {@inheritDoc}
1197      */
1198     @Override
stopUser(int userId)1199     public boolean stopUser(int userId) throws DeviceNotAvailableException {
1200         // No error or status code is returned.
1201         return stopUser(userId, false, false);
1202     }
1203 
1204     /**
1205      * {@inheritDoc}
1206      */
1207     @Override
stopUser(int userId, boolean waitFlag, boolean forceFlag)1208     public boolean stopUser(int userId, boolean waitFlag, boolean forceFlag)
1209             throws DeviceNotAvailableException {
1210         final int apiLevel = getApiLevel();
1211         if (waitFlag && apiLevel < 23) {
1212             throw new IllegalArgumentException("stop-user -w requires API level >= 23");
1213         }
1214         if (forceFlag && apiLevel < 24) {
1215             throw new IllegalArgumentException("stop-user -f requires API level >= 24");
1216         }
1217         StringBuilder cmd = new StringBuilder("am stop-user ");
1218         if (waitFlag) {
1219             cmd.append("-w ");
1220         }
1221         if (forceFlag) {
1222             cmd.append("-f ");
1223         }
1224         cmd.append(userId);
1225 
1226         CLog.d("stopping user with command: %s", cmd.toString());
1227         final String output = executeShellCommand(cmd.toString());
1228         if (output.contains("Error: Can't stop system user")) {
1229             CLog.e("Cannot stop System user.");
1230             return false;
1231         }
1232         if (output.contains("Can't stop current user")) {
1233             CLog.e("Cannot stop current user.");
1234             return false;
1235         }
1236         if (isUserRunning(userId)) {
1237             CLog.w("User Id: %s is still running after the stop-user command.", userId);
1238             return false;
1239         }
1240         return true;
1241     }
1242 
1243     /**
1244      * {@inheritDoc}
1245      */
1246     @Override
getPrimaryUserId()1247     public Integer getPrimaryUserId() throws DeviceNotAvailableException {
1248         ArrayList<String[]> users = tokenizeListUsers();
1249         for (String[] user : users) {
1250             int flag = Integer.parseInt(user[3], 16);
1251             if ((flag & FLAG_PRIMARY) != 0) {
1252                 return Integer.parseInt(user[1]);
1253             }
1254         }
1255         return null;
1256     }
1257 
1258     /** {@inheritDoc} */
1259     @Override
getCurrentUser()1260     public int getCurrentUser() throws DeviceNotAvailableException, DeviceRuntimeException {
1261         checkApiLevelAgainstNextRelease("get-current-user", API_LEVEL_GET_CURRENT_USER);
1262         final String output = executeShellCommand("am get-current-user");
1263         try {
1264             int userId = Integer.parseInt(output.trim());
1265             if (userId < 0) {
1266                 throw new DeviceRuntimeException(
1267                         String.format(
1268                                 "Invalid user id '%s' was returned for get-current-user", userId));
1269             }
1270             return userId;
1271         } catch (NumberFormatException e) {
1272             throw new DeviceRuntimeException(e);
1273         }
1274     }
1275 
findUserInfo(String pmListUsersOutput)1276     private Matcher findUserInfo(String pmListUsersOutput) {
1277         Pattern pattern = Pattern.compile(USER_PATTERN);
1278         Matcher matcher = pattern.matcher(pmListUsersOutput);
1279         return matcher;
1280     }
1281 
1282     /**
1283      * {@inheritDoc}
1284      */
1285     @Override
getUserFlags(int userId)1286     public int getUserFlags(int userId) throws DeviceNotAvailableException {
1287         checkApiLevelAgainst("getUserFlags", 22);
1288         final String commandOutput = executeShellCommand("pm list users");
1289         Matcher matcher = findUserInfo(commandOutput);
1290         while(matcher.find()) {
1291             if (Integer.parseInt(matcher.group(2)) == userId) {
1292                 return Integer.parseInt(matcher.group(6), 16);
1293             }
1294         }
1295         CLog.w("Could not find any flags for userId: %d in output: %s", userId, commandOutput);
1296         return INVALID_USER_ID;
1297     }
1298 
1299     /** {@inheritDoc} */
1300     @Override
isUserSecondary(int userId)1301     public boolean isUserSecondary(int userId) throws DeviceNotAvailableException {
1302         if (userId == UserUtil.USER_SYSTEM) {
1303             return false;
1304         }
1305         int flags = getUserFlags(userId);
1306         if (flags == INVALID_USER_ID) {
1307             return false;
1308         }
1309         return (flags & UserUtil.FLAGS_NOT_SECONDARY) == 0;
1310     }
1311 
1312     /**
1313      * {@inheritDoc}
1314      */
1315     @Override
isUserRunning(int userId)1316     public boolean isUserRunning(int userId) throws DeviceNotAvailableException {
1317         checkApiLevelAgainst("isUserIdRunning", 22);
1318         final String commandOutput = executeShellCommand("pm list users");
1319         Matcher matcher = findUserInfo(commandOutput);
1320         while(matcher.find()) {
1321             if (Integer.parseInt(matcher.group(2)) == userId) {
1322                 if (matcher.group(7).contains("running")) {
1323                     return true;
1324                 }
1325             }
1326         }
1327         return false;
1328     }
1329 
1330     /**
1331      * {@inheritDoc}
1332      */
1333     @Override
getUserSerialNumber(int userId)1334     public int getUserSerialNumber(int userId) throws DeviceNotAvailableException {
1335         checkApiLevelAgainst("getUserSerialNumber", 22);
1336         final String commandOutput = executeShellCommand("dumpsys user");
1337         // example: UserInfo{0:Test:13} serialNo=0
1338         String userSerialPatter = "(.*\\{)(\\d+)(.*\\})(.*=)(\\d+)";
1339         Pattern pattern = Pattern.compile(userSerialPatter);
1340         Matcher matcher = pattern.matcher(commandOutput);
1341         while(matcher.find()) {
1342             if (Integer.parseInt(matcher.group(2)) == userId) {
1343                 return Integer.parseInt(matcher.group(5));
1344             }
1345         }
1346         CLog.w("Could not find user serial number for userId: %d, in output: %s",
1347                 userId, commandOutput);
1348         return INVALID_USER_ID;
1349     }
1350 
1351     /**
1352      * {@inheritDoc}
1353      */
1354     @Override
switchUser(int userId)1355     public boolean switchUser(int userId) throws DeviceNotAvailableException {
1356         return switchUser(userId, AM_COMMAND_TIMEOUT);
1357     }
1358 
1359     /**
1360      * {@inheritDoc}
1361      */
1362     @Override
switchUser(int userId, long timeout)1363     public boolean switchUser(int userId, long timeout) throws DeviceNotAvailableException {
1364         checkApiLevelAgainstNextRelease("switchUser", API_LEVEL_GET_CURRENT_USER);
1365         if (userId == getCurrentUser()) {
1366             CLog.w("Already running as user id: %s. Nothing to be done.", userId);
1367             return true;
1368         }
1369         resetContentProviderSetup();
1370         executeShellCommand(String.format("am switch-user %d", userId));
1371         long initialTime = getHostCurrentTime();
1372         while (getHostCurrentTime() - initialTime <= timeout) {
1373             if (userId == getCurrentUser()) {
1374                 // disable keyguard if option is true
1375                 prePostBootSetup();
1376                 return true;
1377             } else {
1378                 RunUtil.getDefault().sleep(getCheckNewUserSleep());
1379             }
1380         }
1381         CLog.e("User did not switch in the given %d timeout", timeout);
1382         return false;
1383     }
1384 
1385     /**
1386      * Exposed for testing.
1387      */
getCheckNewUserSleep()1388     protected long getCheckNewUserSleep() {
1389         return CHECK_NEW_USER;
1390     }
1391 
1392     /**
1393      * Exposed for testing
1394      */
getHostCurrentTime()1395     protected long getHostCurrentTime() {
1396         return System.currentTimeMillis();
1397     }
1398 
1399     /**
1400      * {@inheritDoc}
1401      */
1402     @Override
hasFeature(String feature)1403     public boolean hasFeature(String feature) throws DeviceNotAvailableException {
1404         final String output = executeShellCommand("pm list features");
1405         if (output.contains(feature)) {
1406             return true;
1407         }
1408         CLog.w("Feature: %s is not available on %s", feature, getSerialNumber());
1409         return false;
1410     }
1411 
1412     /**
1413      * {@inheritDoc}
1414      */
1415     @Override
getSetting(String namespace, String key)1416     public String getSetting(String namespace, String key) throws DeviceNotAvailableException {
1417         return getSettingInternal("", namespace.trim(), key.trim());
1418     }
1419 
1420     /**
1421      * {@inheritDoc}
1422      */
1423     @Override
getSetting(int userId, String namespace, String key)1424     public String getSetting(int userId, String namespace, String key)
1425             throws DeviceNotAvailableException {
1426         return getSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim());
1427     }
1428 
1429     /**
1430      * Internal Helper to get setting with or without a userId provided.
1431      */
getSettingInternal(String userFlag, String namespace, String key)1432     private String getSettingInternal(String userFlag, String namespace, String key)
1433             throws DeviceNotAvailableException {
1434         namespace = namespace.toLowerCase();
1435         if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) {
1436             String cmd = String.format("settings %s get %s %s", userFlag, namespace, key);
1437             String output = executeShellCommand(cmd);
1438             if ("null".equals(output)) {
1439                 CLog.w("settings returned null for command: %s. "
1440                         + "please check if the namespace:key exists", cmd);
1441                 return null;
1442             }
1443             return output.trim();
1444         }
1445         CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace);
1446         return null;
1447     }
1448 
1449     /** {@inheritDoc} */
1450     @Override
getAllSettings(String namespace)1451     public Map<String, String> getAllSettings(String namespace) throws DeviceNotAvailableException {
1452         return getAllSettingsInternal(namespace.trim());
1453     }
1454 
1455     /** Internal helper to get all settings */
getAllSettingsInternal(String namespace)1456     private Map<String, String> getAllSettingsInternal(String namespace)
1457             throws DeviceNotAvailableException {
1458         namespace = namespace.toLowerCase();
1459         if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) {
1460             Map<String, String> map = new HashMap<>();
1461             String cmd = String.format("settings list %s", namespace);
1462             String output = executeShellCommand(cmd);
1463             for (String line : output.split("\\n")) {
1464                 // Setting's value could be empty
1465                 String[] pair = line.trim().split("=", -1);
1466                 if (pair.length > 1) {
1467                     map.putIfAbsent(pair[0], pair[1]);
1468                 } else {
1469                     CLog.e("Unable to get setting from string: %s", line);
1470                 }
1471             }
1472             return map;
1473         }
1474         CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace);
1475         return null;
1476     }
1477 
1478     /**
1479      * {@inheritDoc}
1480      */
1481     @Override
setSetting(String namespace, String key, String value)1482     public void setSetting(String namespace, String key, String value)
1483             throws DeviceNotAvailableException {
1484         setSettingInternal("", namespace.trim(), key.trim(), value.trim());
1485     }
1486 
1487     /**
1488      * {@inheritDoc}
1489      */
1490     @Override
setSetting(int userId, String namespace, String key, String value)1491     public void setSetting(int userId, String namespace, String key, String value)
1492             throws DeviceNotAvailableException {
1493         setSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim(),
1494                 value.trim());
1495     }
1496 
1497     /**
1498      * Internal helper to set a setting with or without a userId provided.
1499      */
setSettingInternal(String userFlag, String namespace, String key, String value)1500     private void setSettingInternal(String userFlag, String namespace, String key, String value)
1501             throws DeviceNotAvailableException {
1502         checkApiLevelAgainst("Changing settings", 22);
1503         if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace.toLowerCase())) {
1504             executeShellCommand(String.format("settings %s put %s %s %s",
1505                     userFlag, namespace, key, value));
1506         } else {
1507             throw new IllegalArgumentException("Namespace must be one of system, secure, global."
1508                     + " You provided: " + namespace);
1509         }
1510     }
1511 
1512     /**
1513      * {@inheritDoc}
1514      */
1515     @Override
getAndroidId(int userId)1516     public String getAndroidId(int userId) throws DeviceNotAvailableException {
1517         if (isAdbRoot()) {
1518             String cmd = String.format(
1519                     "sqlite3 /data/user/%d/com.google.android.gsf/databases/gservices.db "
1520                     + "'select value from main where name = \"android_id\"'", userId);
1521             String output = executeShellCommand(cmd).trim();
1522             if (!output.contains("unable to open database")) {
1523                 return output;
1524             }
1525             CLog.w("Couldn't find android-id, output: %s", output);
1526         } else {
1527             CLog.w("adb root is required.");
1528         }
1529         return null;
1530     }
1531 
1532     /**
1533      * {@inheritDoc}
1534      */
1535     @Override
getAndroidIds()1536     public Map<Integer, String> getAndroidIds() throws DeviceNotAvailableException {
1537         ArrayList<Integer> userIds = listUsers();
1538         if (userIds == null) {
1539             return null;
1540         }
1541         Map<Integer, String> androidIds = new HashMap<Integer, String>();
1542         for (Integer id : userIds) {
1543             String androidId = getAndroidId(id);
1544             androidIds.put(id, androidId);
1545         }
1546         return androidIds;
1547     }
1548 
1549     /**
1550      * {@inheritDoc}
1551      */
1552     @Override
createWifiHelper()1553     IWifiHelper createWifiHelper() throws DeviceNotAvailableException {
1554         mWasWifiHelperInstalled = true;
1555         return new WifiHelper(this, mOptions.getWifiUtilAPKPath());
1556     }
1557 
1558     /**
1559      * Alternative to {@link #createWifiHelper()} where we can choose whether to do the wifi helper
1560      * setup or not.
1561      */
1562     @VisibleForTesting
createWifiHelper(boolean doSetup)1563     IWifiHelper createWifiHelper(boolean doSetup) throws DeviceNotAvailableException {
1564         if (doSetup) {
1565             mWasWifiHelperInstalled = true;
1566         }
1567         return new WifiHelper(this, mOptions.getWifiUtilAPKPath(), doSetup);
1568     }
1569 
1570     /** {@inheritDoc} */
1571     @Override
postInvocationTearDown()1572     public void postInvocationTearDown() {
1573         super.postInvocationTearDown();
1574         // If wifi was installed and it's a real device, attempt to clean it.
1575         if (mWasWifiHelperInstalled) {
1576             mWasWifiHelperInstalled = false;
1577             if (getIDevice() instanceof StubDevice) {
1578                 return;
1579             }
1580             if (!TestDeviceState.ONLINE.equals(getDeviceState())) {
1581                 return;
1582             }
1583             try {
1584                 // Uninstall the wifi utility if it was installed.
1585                 IWifiHelper wifi = createWifiHelper(false);
1586                 wifi.cleanUp();
1587             } catch (DeviceNotAvailableException e) {
1588                 CLog.e("Device became unavailable while uninstalling wifi util.");
1589                 CLog.e(e);
1590             }
1591         }
1592     }
1593 
1594     /** {@inheritDoc} */
1595     @Override
setDeviceOwner(String componentName, int userId)1596     public boolean setDeviceOwner(String componentName, int userId)
1597             throws DeviceNotAvailableException {
1598         final String command = "dpm set-device-owner --user " + userId + " '" + componentName + "'";
1599         final String commandOutput = executeShellCommand(command);
1600         return commandOutput.startsWith("Success:");
1601     }
1602 
1603     /** {@inheritDoc} */
1604     @Override
removeAdmin(String componentName, int userId)1605     public boolean removeAdmin(String componentName, int userId)
1606             throws DeviceNotAvailableException {
1607         final String command =
1608                 "dpm remove-active-admin --user " + userId + " '" + componentName + "'";
1609         final String commandOutput = executeShellCommand(command);
1610         return commandOutput.startsWith("Success:");
1611     }
1612 
1613     /** {@inheritDoc} */
1614     @Override
removeOwners()1615     public void removeOwners() throws DeviceNotAvailableException {
1616         String command = "dumpsys device_policy";
1617         String commandOutput = executeShellCommand(command);
1618         String[] lines = commandOutput.split("\\r?\\n");
1619         for (int i = 0; i < lines.length; ++i) {
1620             String line = lines[i].trim();
1621             if (line.contains("Profile Owner")) {
1622                 // Line is "Profile owner (User <id>):
1623                 String[] tokens = line.split("\\(|\\)| ");
1624                 int userId = Integer.parseInt(tokens[4]);
1625 
1626                 i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i);
1627                 line = lines[i].trim();
1628                 // Line is admin=ComponentInfo{<component>}
1629                 tokens = line.split("\\{|\\}");
1630                 String componentName = tokens[1];
1631                 CLog.d("Cleaning up profile owner " + userId + " " + componentName);
1632                 removeAdmin(componentName, userId);
1633             } else if (line.contains("Device Owner:")) {
1634                 i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i);
1635                 line = lines[i].trim();
1636                 // Line is admin=ComponentInfo{<component>}
1637                 String[] tokens = line.split("\\{|\\}");
1638                 String componentName = tokens[1];
1639 
1640                 // Skip to user id line.
1641                 i = moveToNextIndexMatchingRegex(".*User ID:.*", lines, i);
1642                 line = lines[i].trim();
1643                 // Line is User ID: <N>
1644                 tokens = line.split(":");
1645                 int userId = Integer.parseInt(tokens[1].trim());
1646                 CLog.d("Cleaning up device owner " + userId + " " + componentName);
1647                 removeAdmin(componentName, userId);
1648             }
1649         }
1650     }
1651 
1652     /**
1653      * Search forward from the current index to find a string matching the given regex.
1654      *
1655      * @param regex The regex to match each line against.
1656      * @param lines An array of strings to be searched.
1657      * @param currentIndex the index to start searching from.
1658      * @return The index of a string beginning with the regex.
1659      * @throws IllegalStateException if the line cannot be found.
1660      */
moveToNextIndexMatchingRegex(String regex, String[] lines, int currentIndex)1661     private int moveToNextIndexMatchingRegex(String regex, String[] lines, int currentIndex) {
1662         while (currentIndex < lines.length && !lines[currentIndex].matches(regex)) {
1663             currentIndex++;
1664         }
1665 
1666         if (currentIndex >= lines.length) {
1667             throw new IllegalStateException(
1668                     "The output of 'dumpsys device_policy' was not as expected. Owners have not "
1669                             + "been removed. This will leave the device in an unstable state and "
1670                             + "will lead to further test failures.");
1671         }
1672 
1673         return currentIndex;
1674     }
1675 
1676     /**
1677      * Helper for Api level checking of features in the new release before we incremented the api
1678      * number.
1679      */
checkApiLevelAgainstNextRelease(String feature, int strictMinLevel)1680     private void checkApiLevelAgainstNextRelease(String feature, int strictMinLevel)
1681             throws DeviceNotAvailableException {
1682         if (checkApiLevelAgainstNextRelease(strictMinLevel)) {
1683             return;
1684         }
1685         throw new IllegalArgumentException(
1686                 String.format(
1687                         "%s not supported on %s. Must be API %d.",
1688                         feature, getSerialNumber(), strictMinLevel));
1689     }
1690 
1691     @Override
dumpHeap(String process, String devicePath)1692     public File dumpHeap(String process, String devicePath) throws DeviceNotAvailableException {
1693         if (Strings.isNullOrEmpty(devicePath) || Strings.isNullOrEmpty(process)) {
1694             throw new IllegalArgumentException("devicePath or process cannot be null or empty.");
1695         }
1696         String pid = getProcessPid(process);
1697         if (pid == null) {
1698             return null;
1699         }
1700         File dump = dumpAndPullHeap(pid, devicePath);
1701         // Clean the device.
1702         deleteFile(devicePath);
1703         return dump;
1704     }
1705 
1706     /** Dump the heap file and pull it from the device. */
dumpAndPullHeap(String pid, String devicePath)1707     private File dumpAndPullHeap(String pid, String devicePath) throws DeviceNotAvailableException {
1708         executeShellCommand(String.format(DUMPHEAP_CMD, pid, devicePath));
1709         // Allow a little bit of time for the file to populate on device side.
1710         int attempt = 0;
1711         // TODO: add an API to check device file size
1712         while (!doesFileExist(devicePath) && attempt < 3) {
1713             getRunUtil().sleep(DUMPHEAP_TIME);
1714             attempt++;
1715         }
1716         File dumpFile = pullFile(devicePath);
1717         return dumpFile;
1718     }
1719 
1720     /** {@inheritDoc} */
1721     @Override
listDisplayIds()1722     public Set<Integer> listDisplayIds() throws DeviceNotAvailableException {
1723         Set<Integer> displays = new HashSet<>();
1724         // Zero is the default display
1725         displays.add(0);
1726         CommandResult res = executeShellV2Command("dumpsys SurfaceFlinger | grep 'color modes:'");
1727         if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
1728             CLog.e("Something went wrong while listing displays: %s", res.getStderr());
1729             return displays;
1730         }
1731         String output = res.getStdout();
1732         Pattern p = Pattern.compile(DISPLAY_ID_PATTERN);
1733         for (String line : output.split("\n")) {
1734             Matcher m = p.matcher(line);
1735             if (m.matches()) {
1736                 displays.add(Integer.parseInt(m.group("id")));
1737             }
1738         }
1739         return displays;
1740     }
1741 }
1742