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.config.GlobalConfiguration;
27 import com.android.tradefed.device.IDeviceSelection.BaseDeviceType;
28 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
29 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
30 import com.android.tradefed.invoker.tracing.CloseableTraceScope;
31 import com.android.tradefed.log.ITestLogger;
32 import com.android.tradefed.log.LogUtil.CLog;
33 import com.android.tradefed.result.ByteArrayInputStreamSource;
34 import com.android.tradefed.result.FileInputStreamSource;
35 import com.android.tradefed.result.InputStreamSource;
36 import com.android.tradefed.result.LogDataType;
37 import com.android.tradefed.result.error.DeviceErrorIdentifier;
38 import com.android.tradefed.result.error.InfraErrorIdentifier;
39 import com.android.tradefed.targetprep.TargetSetupError;
40 import com.android.tradefed.util.AaptParser;
41 import com.android.tradefed.util.Bugreport;
42 import com.android.tradefed.util.CommandResult;
43 import com.android.tradefed.util.CommandStatus;
44 import com.android.tradefed.util.FileUtil;
45 import com.android.tradefed.util.KeyguardControllerState;
46 import com.android.tradefed.util.RunUtil;
47 import com.android.tradefed.util.StreamUtil;
48 import com.android.tradefed.util.TimeUtil;
49 import com.android.tradefed.util.ZipUtil2;
50 
51 import com.google.common.annotations.VisibleForTesting;
52 import com.google.common.base.Strings;
53 
54 import org.apache.commons.compress.archivers.zip.ZipFile;
55 
56 import java.awt.Image;
57 import java.awt.image.BufferedImage;
58 import java.io.BufferedReader;
59 import java.io.ByteArrayOutputStream;
60 import java.io.File;
61 import java.io.IOException;
62 import java.io.InputStreamReader;
63 import java.io.PipedInputStream;
64 import java.io.PipedOutputStream;
65 import java.io.Reader;
66 import java.net.ServerSocket;
67 import java.util.ArrayList;
68 import java.util.Arrays;
69 import java.util.Collections;
70 import java.util.HashMap;
71 import java.util.HashSet;
72 import java.util.LinkedHashMap;
73 import java.util.LinkedHashSet;
74 import java.util.List;
75 import java.util.Map;
76 import java.util.Set;
77 import java.util.concurrent.ExecutorService;
78 import java.util.concurrent.Executors;
79 import java.util.concurrent.TimeUnit;
80 import java.util.regex.Matcher;
81 import java.util.regex.Pattern;
82 import java.util.stream.Collectors;
83 
84 import javax.annotation.Nonnull;
85 import javax.annotation.Nullable;
86 import javax.imageio.ImageIO;
87 
88 /**
89  * Implementation of a {@link ITestDevice} for a full stack android device
90  */
91 public class TestDevice extends NativeDevice {
92 
93     /** number of attempts made to clear dialogs */
94     private static final int NUM_CLEAR_ATTEMPTS = 5;
95     /** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */
96     static final String DISMISS_DIALOG_CMD = "input keyevent 23";
97 
98     static final String DISMISS_DIALOG_BROADCAST =
99             "am broadcast -a android.intent.action.CLOSE_SYSTEM_DIALOGS";
100     // Collapse notifications
101     private static final String COLLAPSE_STATUS_BAR = "cmd statusbar collapse";
102 
103     /** Commands that can be used to dismiss the keyguard. */
104     public static final String DISMISS_KEYGUARD_CMD = "input keyevent 82";
105 
106     /**
107      * Alternative command to dismiss the keyguard by requesting the Window Manager service to do
108      * it. Api 23 and after.
109      */
110     static final String DISMISS_KEYGUARD_WM_CMD = "wm dismiss-keyguard";
111 
112     /** Maximum time to wait for keyguard to be dismissed. */
113     private static final long DISMISS_KEYGUARD_TIMEOUT = 3 * 1000;
114 
115     /** Command to construct KeyguardControllerState. */
116     static final String KEYGUARD_CONTROLLER_CMD =
117             "dumpsys activity activities | grep -A3 KeyguardController:";
118 
119     /** Timeout to wait for input dispatch to become ready **/
120     private static final long INPUT_DISPATCH_READY_TIMEOUT = 5 * 1000;
121     /** command to test input dispatch readiness **/
122     private static final String TEST_INPUT_CMD = "dumpsys input";
123 
124     private static final long AM_COMMAND_TIMEOUT = 10 * 1000;
125     private static final long CHECK_NEW_USER = 1000;
126 
127     static final String LIST_PACKAGES_CMD = "pm list packages -f";
128     private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)=(.*)");
129 
130     static final String LIST_APEXES_CMD = "pm list packages --apex-only --show-versioncode -f";
131     private static final Pattern APEXES_WITH_PATH_REGEX =
132             Pattern.compile("package:(.*)=(.*) versionCode:(.*)");
133 
134     static final String GET_MODULEINFOS_CMD = "pm get-moduleinfo --all";
135     private static final Pattern MODULEINFO_REGEX =
136             Pattern.compile("ModuleInfo\\{(.*)\\} packageName: (.*)");
137 
138     /**
139      * Regexp to match on old versions of platform (before R), where {@code -f} flag for the {@code
140      * pm list packages apex-only} command wasn't supported.
141      */
142     private static final Pattern APEXES_WITHOUT_PATH_REGEX =
143             Pattern.compile("package:(.*) versionCode:(.*)");
144 
145     private static final int FLAG_PRIMARY = 1; // From the UserInfo class
146 
147     private static final int FLAG_MAIN = 0x00004000; // From the UserInfo class
148 
149     private static final String[] SETTINGS_NAMESPACE = {"system", "secure", "global"};
150 
151     /** user pattern in the output of "pm list users" = TEXT{<id>:<name>:<flags>} TEXT * */
152     private static final String USER_PATTERN = "(.*?\\{)(\\d+)(:)(.*)(:)(\\w+)(\\}.*)";
153     /** Pattern to find the display ids of "dumpsys SurfaceFlinger" */
154     private static final String DISPLAY_ID_PATTERN = "(Display )(?<id>\\d+)( color modes:)";
155 
156     private static final int API_LEVEL_GET_CURRENT_USER = 24;
157     /** Timeout to wait for a screenshot before giving up to avoid hanging forever */
158     private static final long MAX_SCREENSHOT_TIMEOUT = 5 * 60 * 1000; // 5 min
159 
160     /** adb shell am dumpheap <service pid> <dump file path> */
161     private static final String DUMPHEAP_CMD = "am dumpheap %s %s";
162     /** Time given to a file to be dumped on device side */
163     private static final long DUMPHEAP_TIME = 5000L;
164 
165     /** Timeout in minutes for the package installation */
166     static final long INSTALL_TIMEOUT_MINUTES = 4;
167     /** Max timeout to output for package installation */
168     static final long INSTALL_TIMEOUT_TO_OUTPUT_MINUTES = 3;
169 
170     private boolean mWasWifiHelperInstalled = false;
171 
172     private static final String APEX_SUFFIX = ".apex";
173     private static final String APEX_ARG = "--apex";
174 
175     /** Contains a set of Microdroid instances running in this TestDevice, and their resources. */
176     private Map<Process, MicrodroidTracker> mStartedMicrodroids = new HashMap<>();
177 
178     private static final String TEST_ROOT = "/data/local/tmp/virt/tradefed/";
179     private static final String VIRT_APEX = "/apex/com.android.virt/";
180     private static final String INSTANCE_ID_FILE = "instance_id";
181     private static final String INSTANCE_IMG = "instance.img";
182 
183     // This is really slow on GCE (2m 40s) but fast on localhost or actual Android phones (< 10s).
184     // Then there is time to run the actual task. Set the maximum timeout value big enough.
185     private static final long MICRODROID_MAX_LIFETIME_MINUTES = 20;
186 
187     private static final long MICRODROID_DEFAULT_ADB_CONNECT_TIMEOUT_MINUTES = 5;
188 
189     private static final String EARLY_REBOOT = "Too early to call shutdown() or reboot()";
190 
191     /**
192      * Allow pauses of up to 2 minutes while receiving bugreport.
193      *
194      * <p>Note that dumpsys may pause up to a minute while waiting for unresponsive components. It
195      * still should bail after that minute, if it will ever terminate on its own.
196      */
197     private static final int BUGREPORT_TIMEOUT = 2 * 60 * 1000;
198 
199     private static final String BUGREPORT_CMD = "bugreport";
200     private static final String BUGREPORTZ_CMD = "bugreportz";
201     private static final Pattern BUGREPORTZ_RESPONSE_PATTERN = Pattern.compile("(OK:)(.*)");
202 
203     /** Number of attempts made to get user info. */
204     private static final int NUM_USER_INFO_ATTEMPTS = 3;
205 
206     /** Track microdroid and its resources */
207     private class MicrodroidTracker {
208         ExecutorService executor;
209         String cid;
210     }
211 
212     private boolean mWaitForSnapuserd = false;
213     private SnapuserdWaitPhase mWaitPhase = null;
214     private long mSnapuserNotificationTimestamp = 0L;
215 
216     /**
217      * @param device
218      * @param stateMonitor
219      * @param allocationMonitor
220      */
TestDevice(IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor)221     public TestDevice(IDevice device, IDeviceStateMonitor stateMonitor,
222             IDeviceMonitor allocationMonitor) {
223         super(device, stateMonitor, allocationMonitor);
224     }
225 
226     @Override
isAppEnumerationSupported()227     public boolean isAppEnumerationSupported() throws DeviceNotAvailableException {
228         if (!checkApiLevelAgainstNextRelease(30)) {
229             return false;
230         }
231         return hasFeature("android.software.app_enumeration");
232     }
233 
234     /**
235      * Core implementation of package installation, with retries around
236      * {@link IDevice#installPackage(String, boolean, String...)}
237      * @param packageFile
238      * @param reinstall
239      * @param extraArgs
240      * @return the response from the installation
241      * @throws DeviceNotAvailableException
242      */
internalInstallPackage( final File packageFile, final boolean reinstall, final List<String> extraArgs)243     private String internalInstallPackage(
244             final File packageFile, final boolean reinstall, final List<String> extraArgs)
245                     throws DeviceNotAvailableException {
246         long startTime = System.currentTimeMillis();
247         try {
248             List<String> args = new ArrayList<>(extraArgs);
249             if (packageFile.getName().endsWith(APEX_SUFFIX)) {
250                 args.add(APEX_ARG);
251             }
252             // use array to store response, so it can be returned to caller
253             final String[] response = new String[1];
254             DeviceAction installAction =
255                     new DeviceAction() {
256                         @Override
257                         public boolean run() throws InstallException {
258                             try {
259                                 InstallReceiver receiver = createInstallReceiver();
260                                 getIDevice()
261                                         .installPackage(
262                                                 packageFile.getAbsolutePath(),
263                                                 reinstall,
264                                                 receiver,
265                                                 INSTALL_TIMEOUT_MINUTES,
266                                                 INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
267                                                 TimeUnit.MINUTES,
268                                                 args.toArray(new String[] {}));
269                                 response[0] = handleInstallReceiver(receiver, packageFile);
270                             } catch (InstallException e) {
271                                 response[0] = handleInstallationError(e);
272                             }
273                             return response[0] == null;
274                         }
275                     };
276             CLog.v(
277                     "Installing package file %s with args %s on %s",
278                     packageFile.getAbsolutePath(), extraArgs.toString(), getSerialNumber());
279             performDeviceAction(
280                     String.format("install %s", packageFile.getAbsolutePath()),
281                     installAction,
282                     MAX_RETRY_ATTEMPTS);
283             List<File> packageFiles = new ArrayList<>();
284             packageFiles.add(packageFile);
285             allowLegacyStorageForApps(packageFiles);
286             return response[0];
287         } finally {
288             InvocationMetricLogger.addInvocationMetrics(
289                     InvocationMetricKey.PACKAGE_INSTALL_COUNT, 1);
290             InvocationMetricLogger.addInvocationMetrics(
291                     InvocationMetricKey.PACKAGE_INSTALL_TIME,
292                     System.currentTimeMillis() - startTime);
293         }
294     }
295 
296     /**
297      * Creates and return an {@link InstallReceiver} for {@link #internalInstallPackage(File,
298      * boolean, List)} and {@link #installPackage(File, File, boolean, String...)} testing.
299      */
300     @VisibleForTesting
createInstallReceiver()301     InstallReceiver createInstallReceiver() {
302         return new InstallReceiver();
303     }
304 
305     /** {@inheritDoc} */
306     @Override
getBugreport()307     public InputStreamSource getBugreport() {
308         if (getApiLevelSafe() < 24) {
309             InputStreamSource bugreport = getBugreportInternal();
310             if (bugreport == null) {
311                 // Safe call so we don't return null but an empty resource.
312                 return new ByteArrayInputStreamSource("".getBytes());
313             }
314             return bugreport;
315         }
316         CLog.d("Api level above 24, using bugreportz instead.");
317         File mainEntry = null;
318         File bugreportzFile = null;
319         long startTime = System.currentTimeMillis();
320         try {
321             bugreportzFile = getBugreportzInternal();
322             if (bugreportzFile == null) {
323                 // return empty buffer
324                 return new ByteArrayInputStreamSource("".getBytes());
325             }
326             try (ZipFile zip = new ZipFile(bugreportzFile)) {
327                 // We get the main_entry.txt that contains the bugreport name.
328                 mainEntry = ZipUtil2.extractFileFromZip(zip, "main_entry.txt");
329                 String bugreportName = FileUtil.readStringFromFile(mainEntry).trim();
330                 CLog.d("bugreport name: '%s'", bugreportName);
331                 File bugreport = ZipUtil2.extractFileFromZip(zip, bugreportName);
332                 return new FileInputStreamSource(bugreport, true);
333             }
334         } catch (IOException e) {
335             CLog.e("Error while unzipping bugreportz");
336             CLog.e(e);
337             return new ByteArrayInputStreamSource("corrupted bugreport.".getBytes());
338         } finally {
339             InvocationMetricLogger.addInvocationMetrics(
340                     InvocationMetricKey.BUGREPORT_TIME, System.currentTimeMillis() - startTime);
341             InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.BUGREPORT_COUNT, 1);
342             FileUtil.deleteFile(bugreportzFile);
343             FileUtil.deleteFile(mainEntry);
344         }
345     }
346 
347     /** {@inheritDoc} */
348     @Override
logBugreport(String dataName, ITestLogger listener)349     public boolean logBugreport(String dataName, ITestLogger listener) {
350         InputStreamSource bugreport = null;
351         LogDataType type = null;
352         try {
353             bugreport = getBugreportz();
354             type = LogDataType.BUGREPORTZ;
355             // log what we managed to capture.
356             if (bugreport != null && bugreport.size() > 0L) {
357                 listener.testLog(dataName, type, bugreport);
358                 return true;
359             }
360         } finally {
361             StreamUtil.cancel(bugreport);
362         }
363         CLog.d(
364                 "logBugreport() was not successful in collecting and logging the bugreport "
365                         + "for device %s",
366                 getSerialNumber());
367         return false;
368     }
369 
370     /** {@inheritDoc} */
371     @Override
takeBugreport()372     public Bugreport takeBugreport() {
373         File bugreportFile = null;
374         int apiLevel = getApiLevelSafe();
375         if (apiLevel == UNKNOWN_API_LEVEL) {
376             return null;
377         }
378         long startTime = System.currentTimeMillis();
379         try {
380             if (apiLevel >= 24) {
381                 CLog.d("Api level above 24, using bugreportz.");
382                 bugreportFile = getBugreportzInternal();
383                 if (bugreportFile != null) {
384                     return new Bugreport(bugreportFile, true);
385                 }
386                 return null;
387             }
388             // fall back to regular bugreport
389             InputStreamSource bugreport = getBugreportInternal();
390             if (bugreport == null) {
391                 CLog.e("Error when collecting the bugreport.");
392                 return null;
393             }
394             try {
395                 bugreportFile = FileUtil.createTempFile("bugreport", ".txt");
396                 FileUtil.writeToFile(bugreport.createInputStream(), bugreportFile);
397                 return new Bugreport(bugreportFile, false);
398             } catch (IOException e) {
399                 CLog.e("Error when writing the bugreport file");
400                 CLog.e(e);
401             }
402             return null;
403         } finally {
404             InvocationMetricLogger.addInvocationMetrics(
405                     InvocationMetricKey.BUGREPORT_TIME, System.currentTimeMillis() - startTime);
406             InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.BUGREPORT_COUNT, 1);
407         }
408     }
409 
410     /** {@inheritDoc} */
411     @Override
getBugreportz()412     public InputStreamSource getBugreportz() {
413         if (getApiLevelSafe() < 24) {
414             return null;
415         }
416         CLog.d("Start getBugreportz()");
417         long startTime = System.currentTimeMillis();
418         try {
419             File bugreportZip = getBugreportzInternal();
420             if (bugreportZip != null) {
421                 return new FileInputStreamSource(bugreportZip, true);
422             }
423             return null;
424         } finally {
425             CLog.d("Done with getBugreportz()");
426             InvocationMetricLogger.addInvocationMetrics(
427                     InvocationMetricKey.BUGREPORT_TIME, System.currentTimeMillis() - startTime);
428             InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.BUGREPORT_COUNT, 1);
429         }
430     }
431 
432     /** Internal Helper method to get the bugreportz zip file as a {@link File}. */
433     @VisibleForTesting
getBugreportzInternal()434     protected File getBugreportzInternal() {
435         CollectingOutputReceiver receiver = new CollectingOutputReceiver();
436         // Does not rely on {@link ITestDevice#executeAdbCommand(String...)} because it does not
437         // provide a timeout.
438         try {
439             executeShellCommand(
440                     BUGREPORTZ_CMD,
441                     receiver,
442                     getOptions().getBugreportzTimeout(),
443                     TimeUnit.MILLISECONDS,
444                     0 /* don't retry */);
445             String output = receiver.getOutput().trim();
446             Matcher match = BUGREPORTZ_RESPONSE_PATTERN.matcher(output);
447             if (!match.find()) {
448                 CLog.e("Something went went wrong during bugreportz collection: '%s'", output);
449                 return null;
450             } else {
451                 String remoteFilePath = match.group(2);
452                 if (Strings.isNullOrEmpty(remoteFilePath)) {
453                     CLog.e("Invalid bugreportz path found from output: %s", output);
454                     return null;
455                 }
456                 File zipFile = null;
457                 try {
458                     if (!doesFileExist(remoteFilePath)) {
459                         CLog.e("Did not find bugreportz at: '%s'", remoteFilePath);
460                         return null;
461                     }
462                     // Create a placeholder to replace the file
463                     zipFile = FileUtil.createTempFile("bugreportz", ".zip");
464                     // pull
465                     pullFile(remoteFilePath, zipFile);
466                     String bugreportDir =
467                             remoteFilePath.substring(0, remoteFilePath.lastIndexOf('/'));
468                     if (!bugreportDir.isEmpty()) {
469                         // clean bugreport files directory on device
470                         deleteFile(String.format("%s/*", bugreportDir));
471                     }
472 
473                     return zipFile;
474                 } catch (IOException e) {
475                     CLog.e("Failed to create the temporary file.");
476                     return null;
477                 }
478             }
479         } catch (DeviceNotAvailableException e) {
480             CLog.e("Device %s became unresponsive while retrieving bugreportz", getSerialNumber());
481             CLog.e(e);
482         }
483         return null;
484     }
485 
getBugreportInternal()486     protected InputStreamSource getBugreportInternal() {
487         CollectingByteOutputReceiver receiver = new CollectingByteOutputReceiver();
488         try {
489             executeShellCommand(
490                     BUGREPORT_CMD,
491                     receiver,
492                     BUGREPORT_TIMEOUT,
493                     TimeUnit.MILLISECONDS,
494                     0 /* don't retry */);
495         } catch (DeviceNotAvailableException e) {
496             // Log, but don't throw, so the caller can get the bugreport contents even
497             // if the device goes away
498             CLog.e("Device %s became unresponsive while retrieving bugreport", getSerialNumber());
499             return null;
500         }
501         return new ByteArrayInputStreamSource(receiver.getOutput());
502     }
503 
504     /**
505      * {@inheritDoc}
506      */
507     @Override
installPackage(final File packageFile, final boolean reinstall, final String... extraArgs)508     public String installPackage(final File packageFile, final boolean reinstall,
509             final String... extraArgs) throws DeviceNotAvailableException {
510         boolean runtimePermissionSupported = isRuntimePermissionSupported();
511         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
512         // grant all permissions by default if feature is supported
513         if (runtimePermissionSupported) {
514             args.add("-g");
515         }
516         return internalInstallPackage(packageFile, reinstall, args);
517     }
518 
519     /**
520      * {@inheritDoc}
521      */
522     @Override
installPackage(File packageFile, boolean reinstall, boolean grantPermissions, String... extraArgs)523     public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
524             String... extraArgs) throws DeviceNotAvailableException {
525         ensureRuntimePermissionSupported();
526         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
527         if (grantPermissions) {
528             args.add("-g");
529         }
530         return internalInstallPackage(packageFile, reinstall, args);
531     }
532 
installPackage(final File packageFile, final File certFile, final boolean reinstall, final String... extraArgs)533     public String installPackage(final File packageFile, final File certFile,
534             final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException {
535         long startTime = System.currentTimeMillis();
536         try {
537             // use array to store response, so it can be returned to caller
538             final String[] response = new String[1];
539             DeviceAction installAction =
540                     new DeviceAction() {
541                         @Override
542                         public boolean run()
543                                 throws InstallException, SyncException, IOException,
544                                         TimeoutException, AdbCommandRejectedException {
545                             // TODO: create a getIDevice().installPackage(File, File...) method when
546                             // the
547                             // dist cert functionality is ready to be open sourced
548                             String remotePackagePath =
549                                     getIDevice().syncPackageToDevice(packageFile.getAbsolutePath());
550                             String remoteCertPath =
551                                     getIDevice().syncPackageToDevice(certFile.getAbsolutePath());
552                             // trick installRemotePackage into issuing a 'pm install <apk> <cert>'
553                             // command, by adding apk path to extraArgs, and using cert as the
554                             // 'apk file'.
555                             String[] newExtraArgs = new String[extraArgs.length + 1];
556                             System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length);
557                             newExtraArgs[newExtraArgs.length - 1] =
558                                     String.format("\"%s\"", remotePackagePath);
559                             try {
560                                 InstallReceiver receiver = createInstallReceiver();
561                                 getIDevice()
562                                         .installRemotePackage(
563                                                 remoteCertPath,
564                                                 reinstall,
565                                                 receiver,
566                                                 INSTALL_TIMEOUT_MINUTES,
567                                                 INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
568                                                 TimeUnit.MINUTES,
569                                                 newExtraArgs);
570                                 response[0] = handleInstallReceiver(receiver, packageFile);
571                             } catch (InstallException e) {
572                                 response[0] = handleInstallationError(e);
573                             } finally {
574                                 getIDevice().removeRemotePackage(remotePackagePath);
575                                 getIDevice().removeRemotePackage(remoteCertPath);
576                             }
577                             return true;
578                         }
579                     };
580             performDeviceAction(
581                     String.format("install %s", packageFile.getAbsolutePath()),
582                     installAction,
583                     MAX_RETRY_ATTEMPTS);
584             List<File> packageFiles = new ArrayList<>();
585             packageFiles.add(packageFile);
586             allowLegacyStorageForApps(packageFiles);
587             return response[0];
588         } finally {
589             InvocationMetricLogger.addInvocationMetrics(
590                     InvocationMetricKey.PACKAGE_INSTALL_COUNT, 1);
591             InvocationMetricLogger.addInvocationMetrics(
592                     InvocationMetricKey.PACKAGE_INSTALL_TIME,
593                     System.currentTimeMillis() - startTime);
594         }
595     }
596 
597     /**
598      * {@inheritDoc}
599      */
600     @Override
installPackageForUser(File packageFile, boolean reinstall, int userId, String... extraArgs)601     public String installPackageForUser(File packageFile, boolean reinstall, int userId,
602             String... extraArgs) throws DeviceNotAvailableException {
603         boolean runtimePermissionSupported = isRuntimePermissionSupported();
604         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
605         // grant all permissions by default if feature is supported
606         if (runtimePermissionSupported) {
607             args.add("-g");
608         }
609         args.add("--user");
610         args.add(Integer.toString(userId));
611         return internalInstallPackage(packageFile, reinstall, args);
612     }
613 
614     /**
615      * {@inheritDoc}
616      */
617     @Override
installPackageForUser(File packageFile, boolean reinstall, boolean grantPermissions, int userId, String... extraArgs)618     public String installPackageForUser(File packageFile, boolean reinstall,
619             boolean grantPermissions, int userId, String... extraArgs)
620                     throws DeviceNotAvailableException {
621         ensureRuntimePermissionSupported();
622         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
623         if (grantPermissions) {
624             args.add("-g");
625         }
626         args.add("--user");
627         args.add(Integer.toString(userId));
628         return internalInstallPackage(packageFile, reinstall, args);
629     }
630 
631     /**
632      * {@inheritDoc}
633      */
634     @Override
uninstallPackage(final String packageName)635     public String uninstallPackage(final String packageName) throws DeviceNotAvailableException {
636         // use array to store response, so it can be returned to caller
637         return uninstallPackage(packageName, /* extraArgs= */ null);
638     }
639 
uninstallPackage(String packageName, @Nullable String extraArgs)640     private String uninstallPackage(String packageName, @Nullable String extraArgs)
641             throws DeviceNotAvailableException {
642         final String finalExtraArgs = (extraArgs == null) ? "" : extraArgs;
643 
644         // use array to store response, so it can be returned to caller
645         final String[] response = new String[1];
646         DeviceAction uninstallAction =
647                 () -> {
648                     CLog.d("Uninstalling %s with extra args %s", packageName, finalExtraArgs);
649 
650                     String result = getIDevice().uninstallApp(packageName, finalExtraArgs);
651                     response[0] = result;
652                     return result == null;
653                 };
654 
655         performDeviceAction(
656                 String.format("uninstall %s with extra args %s", packageName, finalExtraArgs),
657                 uninstallAction,
658                 MAX_RETRY_ATTEMPTS);
659         return response[0];
660     }
661 
662     /** {@inheritDoc} */
663     @Override
uninstallPackageForUser(final String packageName, int userId)664     public String uninstallPackageForUser(final String packageName, int userId)
665             throws DeviceNotAvailableException {
666         return uninstallPackage(packageName, "--user " + userId);
667     }
668 
669     /**
670      * Core implementation for installing application with split apk files {@link
671      * IDevice#installPackages(List, boolean, List)} See
672      * "https://developer.android.com/studio/build/configure-apk-splits" on how to split apk to
673      * several files.
674      *
675      * @param packageFiles the local apk files
676      * @param reinstall <code>true</code> if a reinstall should be performed
677      * @param extraArgs optional extra arguments to pass. See 'adb shell pm -h' for available
678      *     options.
679      * @return the response from the installation <code>null</code> if installation succeeds.
680      * @throws DeviceNotAvailableException
681      */
internalInstallPackages( final List<File> packageFiles, final boolean reinstall, final List<String> extraArgs)682     private String internalInstallPackages(
683             final List<File> packageFiles, final boolean reinstall, final List<String> extraArgs)
684             throws DeviceNotAvailableException {
685         long startTime = System.currentTimeMillis();
686         try {
687             // use array to store response, so it can be returned to caller
688             final String[] response = new String[1];
689             DeviceAction installAction =
690                     new DeviceAction() {
691                         @Override
692                         public boolean run() throws InstallException {
693                             try {
694                                 getIDevice()
695                                         .installPackages(
696                                                 packageFiles,
697                                                 reinstall,
698                                                 extraArgs,
699                                                 INSTALL_TIMEOUT_MINUTES,
700                                                 TimeUnit.MINUTES);
701                                 response[0] = null;
702                                 return true;
703                             } catch (InstallException e) {
704                                 response[0] = handleInstallationError(e);
705                                 return false;
706                             }
707                         }
708                     };
709             performDeviceAction(
710                     String.format("install %s", packageFiles.toString()),
711                     installAction,
712                     MAX_RETRY_ATTEMPTS);
713             allowLegacyStorageForApps(packageFiles);
714             return response[0];
715         } finally {
716             InvocationMetricLogger.addInvocationMetrics(
717                     InvocationMetricKey.PACKAGE_INSTALL_COUNT, 1);
718             InvocationMetricLogger.addInvocationMetrics(
719                     InvocationMetricKey.PACKAGE_INSTALL_TIME,
720                     System.currentTimeMillis() - startTime);
721         }
722     }
723 
724     /**
725      * Allows Legacy External Storage access for apps that request for it.
726      *
727      * <p>Apps that request for legacy external storage access are granted the access by setting
728      * MANAGE_EXTERNAL_STORAGE App Op. This gives the app File manager privileges, File managers
729      * have legacy external storage access.
730      *
731      * @param appFiles List of Files. Apk Files of the apps that are installed.
732      */
allowLegacyStorageForApps(List<File> appFiles)733     private void allowLegacyStorageForApps(List<File> appFiles) throws DeviceNotAvailableException {
734         for (File appFile : appFiles) {
735             AaptParser aaptParser = createParser(appFile);
736             if (aaptParser != null
737                     && aaptParser.getTargetSdkVersion() > 29
738                     && aaptParser.isRequestingLegacyStorage()) {
739                 if (!aaptParser.isUsingPermissionManageExternalStorage()) {
740                     CLog.w(
741                             "App is requesting legacy storage and targets R or above, but didn't"
742                                     + " request the MANAGE_EXTERNAL_STORAGE permission so the"
743                                     + " associated app op cannot be automatically granted and the"
744                                     + " app won't have legacy external storage access: "
745                                     + aaptParser.getPackageName());
746                     continue;
747                 }
748                 // Set the MANAGE_EXTERNAL_STORAGE App Op to MODE_ALLOWED (Code = 0)
749                 // for all users.
750                 ArrayList<Integer> userIds = listUsers();
751                 for (int userId : userIds) {
752                     CommandResult setFileManagerAppOpResult =
753                             executeShellV2Command(
754                                     "appops set --user "
755                                             + userId
756                                             + " --uid "
757                                             + aaptParser.getPackageName()
758                                             + " MANAGE_EXTERNAL_STORAGE 0");
759                     if (!CommandStatus.SUCCESS.equals(setFileManagerAppOpResult.getStatus())) {
760                         CLog.e(
761                                 "Failed to set MANAGE_EXTERNAL_STORAGE App Op to"
762                                         + " allow legacy external storage for: %s ; stderr: %s",
763                                 aaptParser.getPackageName(), setFileManagerAppOpResult.getStderr());
764                     }
765                 }
766             }
767         }
768         CommandResult persistFileManagerAppOpResult =
769                 executeShellV2Command("appops write-settings");
770         if (!CommandStatus.SUCCESS.equals(persistFileManagerAppOpResult.getStatus())) {
771             CLog.e(
772                     "Failed to persist MANAGE_EXTERNAL_STORAGE App Op over `adb reboot`: %s",
773                     persistFileManagerAppOpResult.getStderr());
774         }
775     }
776 
777     @VisibleForTesting
createParser(File appFile)778     protected AaptParser createParser(File appFile) {
779         return AaptParser.parse(appFile);
780     }
781 
782     /** {@inheritDoc} */
783     @Override
installPackages( final List<File> packageFiles, final boolean reinstall, final String... extraArgs)784     public String installPackages(
785             final List<File> packageFiles, final boolean reinstall, final String... extraArgs)
786             throws DeviceNotAvailableException {
787         // Grant all permissions by default if feature is supported
788         return installPackages(packageFiles, reinstall, isRuntimePermissionSupported(), extraArgs);
789     }
790 
791     /** {@inheritDoc} */
792     @Override
installPackages( List<File> packageFiles, boolean reinstall, boolean grantPermissions, String... extraArgs)793     public String installPackages(
794             List<File> packageFiles,
795             boolean reinstall,
796             boolean grantPermissions,
797             String... extraArgs)
798             throws DeviceNotAvailableException {
799         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
800         if (grantPermissions) {
801             ensureRuntimePermissionSupported();
802             args.add("-g");
803         }
804         return internalInstallPackages(packageFiles, reinstall, args);
805     }
806 
807     /** {@inheritDoc} */
808     @Override
installPackagesForUser( List<File> packageFiles, boolean reinstall, int userId, String... extraArgs)809     public String installPackagesForUser(
810             List<File> packageFiles, boolean reinstall, int userId, String... extraArgs)
811             throws DeviceNotAvailableException {
812         // Grant all permissions by default if feature is supported
813         return installPackagesForUser(
814                 packageFiles, reinstall, isRuntimePermissionSupported(), userId, extraArgs);
815     }
816 
817     /** {@inheritDoc} */
818     @Override
installPackagesForUser( List<File> packageFiles, boolean reinstall, boolean grantPermissions, int userId, String... extraArgs)819     public String installPackagesForUser(
820             List<File> packageFiles,
821             boolean reinstall,
822             boolean grantPermissions,
823             int userId,
824             String... extraArgs)
825             throws DeviceNotAvailableException {
826         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
827         if (grantPermissions) {
828             ensureRuntimePermissionSupported();
829             args.add("-g");
830         }
831         args.add("--user");
832         args.add(Integer.toString(userId));
833         return internalInstallPackages(packageFiles, reinstall, args);
834     }
835 
836     /**
837      * Core implementation for split apk remote installation {@link IDevice#installPackage(String,
838      * boolean, String...)} See "https://developer.android.com/studio/build/configure-apk-splits" on
839      * how to split apk to several files.
840      *
841      * @param remoteApkPaths the remote apk file paths
842      * @param reinstall <code>true</code> if a reinstall should be performed
843      * @param extraArgs optional extra arguments to pass. See 'adb shell pm -h' for available
844      *     options.
845      * @return the response from the installation <code>null</code> if installation succeeds.
846      * @throws DeviceNotAvailableException
847      */
internalInstallRemotePackages( final List<String> remoteApkPaths, final boolean reinstall, final List<String> extraArgs)848     private String internalInstallRemotePackages(
849             final List<String> remoteApkPaths,
850             final boolean reinstall,
851             final List<String> extraArgs)
852             throws DeviceNotAvailableException {
853         // use array to store response, so it can be returned to caller
854         final String[] response = new String[1];
855         DeviceAction installAction =
856                 new DeviceAction() {
857                     @Override
858                     public boolean run() throws InstallException {
859                         try {
860                             getIDevice()
861                                     .installRemotePackages(
862                                             remoteApkPaths,
863                                             reinstall,
864                                             extraArgs,
865                                             INSTALL_TIMEOUT_MINUTES,
866                                             TimeUnit.MINUTES);
867                             response[0] = null;
868                             return true;
869                         } catch (InstallException e) {
870                             response[0] = handleInstallationError(e);
871                             return false;
872                         }
873                     }
874                 };
875         performDeviceAction(
876                 String.format("install %s", remoteApkPaths.toString()),
877                 installAction,
878                 MAX_RETRY_ATTEMPTS);
879         return response[0];
880     }
881 
882     /** {@inheritDoc} */
883     @Override
installRemotePackages( final List<String> remoteApkPaths, final boolean reinstall, final String... extraArgs)884     public String installRemotePackages(
885             final List<String> remoteApkPaths, final boolean reinstall, final String... extraArgs)
886             throws DeviceNotAvailableException {
887         // Grant all permissions by default if feature is supported
888         return installRemotePackages(
889                 remoteApkPaths, reinstall, isRuntimePermissionSupported(), extraArgs);
890     }
891 
892     /** {@inheritDoc} */
893     @Override
installRemotePackages( List<String> remoteApkPaths, boolean reinstall, boolean grantPermissions, String... extraArgs)894     public String installRemotePackages(
895             List<String> remoteApkPaths,
896             boolean reinstall,
897             boolean grantPermissions,
898             String... extraArgs)
899             throws DeviceNotAvailableException {
900         List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
901         if (grantPermissions) {
902             ensureRuntimePermissionSupported();
903             args.add("-g");
904         }
905         return internalInstallRemotePackages(remoteApkPaths, reinstall, args);
906     }
907 
908     /** {@inheritDoc} */
909     @Override
getScreenshot()910     public InputStreamSource getScreenshot() throws DeviceNotAvailableException {
911         return getScreenshot("PNG");
912     }
913 
914     /**
915      * {@inheritDoc}
916      */
917     @Override
getScreenshot(String format)918     public InputStreamSource getScreenshot(String format) throws DeviceNotAvailableException {
919         return getScreenshot(format, true);
920     }
921 
922     /** {@inheritDoc} */
923     @Override
getScreenshot(String format, boolean rescale)924     public InputStreamSource getScreenshot(String format, boolean rescale)
925             throws DeviceNotAvailableException {
926         if (!format.equalsIgnoreCase("PNG") && !format.equalsIgnoreCase("JPEG")){
927             CLog.e("Screenshot: Format %s is not supported, defaulting to PNG.", format);
928             format = "PNG";
929         }
930         ScreenshotAction action = new ScreenshotAction();
931         if (performDeviceAction("screenshot", action, MAX_RETRY_ATTEMPTS)) {
932             byte[] imageData =
933                     compressRawImage(action.mRawScreenshot, format.toUpperCase(), rescale);
934             if (imageData != null) {
935                 return new ByteArrayInputStreamSource(imageData);
936             }
937         }
938         // Return an error in the buffer
939         return new ByteArrayInputStreamSource(
940                 "Error: device reported null for screenshot.".getBytes());
941     }
942 
943     /** {@inheritDoc} */
944     @Override
getScreenshot(long displayId)945     public InputStreamSource getScreenshot(long displayId) throws DeviceNotAvailableException {
946         final String tmpDevicePath = String.format("/data/local/tmp/display_%s.png", displayId);
947         CommandResult result =
948                 executeShellV2Command(
949                         String.format("screencap -p -d %s %s", displayId, tmpDevicePath));
950         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
951             // Return an error in the buffer
952             CLog.e("Error: device reported error for screenshot:");
953             CLog.e("stdout: %s\nstderr: %s", result.getStdout(), result.getStderr());
954             return null;
955         }
956         try {
957             File tmpScreenshot = pullFile(tmpDevicePath);
958             if (tmpScreenshot == null) {
959                 return null;
960             }
961             return new FileInputStreamSource(tmpScreenshot, true);
962         } finally {
963             deleteFile(tmpDevicePath);
964         }
965     }
966 
967     private class ScreenshotAction implements DeviceAction {
968 
969         RawImage mRawScreenshot;
970 
971         /**
972          * {@inheritDoc}
973          */
974         @Override
run()975         public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
976                 ShellCommandUnresponsiveException, InstallException, SyncException {
977             mRawScreenshot =
978                     getIDevice().getScreenshot(MAX_SCREENSHOT_TIMEOUT, TimeUnit.MILLISECONDS);
979             return mRawScreenshot != null;
980         }
981     }
982 
983     /**
984      * Helper to compress a rawImage obtained from the screen.
985      *
986      * @param rawImage {@link RawImage} to compress.
987      * @param format resulting format of compressed image. PNG and JPEG are supported.
988      * @param rescale if rescaling should be done to further reduce size of compressed image.
989      * @return compressed image.
990      */
991     @VisibleForTesting
compressRawImage(RawImage rawImage, String format, boolean rescale)992     byte[] compressRawImage(RawImage rawImage, String format, boolean rescale) {
993         BufferedImage image = rawImageToBufferedImage(rawImage, format);
994 
995         // Rescale to reduce size if needed
996         // Screenshot default format is 1080 x 1920, 8-bit/color RGBA
997         // By cutting in half we can easily keep good quality and smaller size
998         if (rescale) {
999             image = rescaleImage(image);
1000         }
1001 
1002         return getImageData(image, format);
1003     }
1004 
1005     /**
1006      * Converts {@link RawImage} to {@link BufferedImage} in specified format.
1007      *
1008      * @param rawImage {@link RawImage} to convert.
1009      * @param format resulting format of image. PNG and JPEG are supported.
1010      * @return converted image.
1011      */
1012     @VisibleForTesting
rawImageToBufferedImage(RawImage rawImage, String format)1013     BufferedImage rawImageToBufferedImage(RawImage rawImage, String format) {
1014         BufferedImage image = null;
1015 
1016         if ("JPEG".equalsIgnoreCase(format)) {
1017             //JPEG does not support ARGB without a special encoder
1018             image =
1019                     new BufferedImage(
1020                             rawImage.width, rawImage.height, BufferedImage.TYPE_3BYTE_BGR);
1021         }
1022         else {
1023             image = new BufferedImage(rawImage.width, rawImage.height, BufferedImage.TYPE_INT_ARGB);
1024         }
1025 
1026         // borrowed conversion logic from platform/sdk/screenshot/.../Screenshot.java
1027         int index = 0;
1028         int IndexInc = rawImage.bpp >> 3;
1029         for (int y = 0 ; y < rawImage.height ; y++) {
1030             for (int x = 0 ; x < rawImage.width ; x++) {
1031                 int value = rawImage.getARGB(index);
1032                 index += IndexInc;
1033                 image.setRGB(x, y, value);
1034             }
1035         }
1036 
1037         return image;
1038     }
1039 
1040     /**
1041      * Rescales image cutting it in half.
1042      *
1043      * @param image source {@link BufferedImage}.
1044      * @return resulting scaled image.
1045      */
1046     @VisibleForTesting
rescaleImage(BufferedImage image)1047     BufferedImage rescaleImage(BufferedImage image) {
1048         int shortEdge = Math.min(image.getHeight(), image.getWidth());
1049         if (shortEdge > 720) {
1050             Image resized =
1051                     image.getScaledInstance(
1052                             image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_SMOOTH);
1053             image =
1054                     new BufferedImage(
1055                             image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_REPLICATE);
1056             image.getGraphics().drawImage(resized, 0, 0, null);
1057         }
1058         return image;
1059     }
1060 
1061     /**
1062      * Gets byte array representation of {@link BufferedImage}.
1063      *
1064      * @param image source {@link BufferedImage}.
1065      * @param format resulting format of image. PNG and JPEG are supported.
1066      * @return byte array representation of the image.
1067      */
1068     @VisibleForTesting
getImageData(BufferedImage image, String format)1069     byte[] getImageData(BufferedImage image, String format) {
1070         // store compressed image in memory, and let callers write to persistent storage
1071         // use initial buffer size of 128K
1072         byte[] imageData = null;
1073         ByteArrayOutputStream imageOut = new ByteArrayOutputStream(128*1024);
1074         try {
1075             if (ImageIO.write(image, format, imageOut)) {
1076                 imageData = imageOut.toByteArray();
1077             } else {
1078                 CLog.e("Failed to compress screenshot to png");
1079             }
1080         } catch (IOException e) {
1081             CLog.e("Failed to compress screenshot to png");
1082             CLog.e(e);
1083         }
1084         StreamUtil.close(imageOut);
1085         return imageData;
1086     }
1087 
1088     /**
1089      * {@inheritDoc}
1090      */
1091     @Override
clearErrorDialogs()1092     public boolean clearErrorDialogs() throws DeviceNotAvailableException {
1093         CommandResult dismissResult =
1094                 executeShellV2Command(
1095                         DISMISS_DIALOG_BROADCAST, 60000L, TimeUnit.MILLISECONDS, 0 /*retry*/);
1096         if (!CommandStatus.SUCCESS.equals(dismissResult.getStatus())) {
1097             CLog.w("Issue with dimissing dialog broadcast. %s", dismissResult);
1098         }
1099         executeShellCommand(COLLAPSE_STATUS_BAR);
1100         // attempt to clear error dialogs multiple times
1101         for (int i = 0; i < NUM_CLEAR_ATTEMPTS; i++) {
1102             int numErrorDialogs = getErrorDialogCount();
1103             if (numErrorDialogs == 0) {
1104                 return true;
1105             }
1106             doClearDialogs(numErrorDialogs);
1107         }
1108         if (getErrorDialogCount() > 0) {
1109             // at this point, all attempts to clear error dialogs completely have failed
1110             // it might be the case that the process keeps showing new dialogs immediately after
1111             // clearing. There's really no workaround, but to dump an error
1112             CLog.e("error dialogs still exist on %s.", getSerialNumber());
1113             return false;
1114         }
1115         return true;
1116     }
1117 
1118     /**
1119      * Detects the number of crash or ANR dialogs currently displayed.
1120      * <p/>
1121      * Parses output of 'dump activity processes'
1122      *
1123      * @return count of dialogs displayed
1124      * @throws DeviceNotAvailableException
1125      */
getErrorDialogCount()1126     private int getErrorDialogCount() throws DeviceNotAvailableException {
1127         int errorDialogCount = 0;
1128         Pattern crashPattern = Pattern.compile(".*crashing=true.*AppErrorDialog.*");
1129         Pattern anrPattern = Pattern.compile(".*notResponding=true.*AppNotRespondingDialog.*");
1130         String systemStatusOutput =
1131                 executeShellCommand(
1132                         "dumpsys activity processes | grep -e .*crashing=true.*AppErrorDialog.* -e"
1133                                 + " .*notResponding=true.*AppNotRespondingDialog.*");
1134         Matcher crashMatcher = crashPattern.matcher(systemStatusOutput);
1135         while (crashMatcher.find()) {
1136             errorDialogCount++;
1137         }
1138         Matcher anrMatcher = anrPattern.matcher(systemStatusOutput);
1139         while (anrMatcher.find()) {
1140             errorDialogCount++;
1141         }
1142 
1143         return errorDialogCount;
1144     }
1145 
doClearDialogs(int numDialogs)1146     private void doClearDialogs(int numDialogs) throws DeviceNotAvailableException {
1147         CLog.i("Attempted to clear %d dialogs on %s", numDialogs, getSerialNumber());
1148         for (int i=0; i < numDialogs; i++) {
1149             // send DPAD_CENTER
1150             executeShellCommand(DISMISS_DIALOG_CMD);
1151         }
1152     }
1153 
1154     /**
1155      * {@inheritDoc}
1156      */
1157     @Override
disableKeyguard()1158     public void disableKeyguard() throws DeviceNotAvailableException {
1159         long start = System.currentTimeMillis();
1160         while (true) {
1161             Boolean ready = isDeviceInputReady();
1162             if (ready == null) {
1163                 // unsupported API level, bail
1164                 break;
1165             }
1166             if (ready) {
1167                 // input dispatch is ready, bail
1168                 break;
1169             }
1170             long timeSpent = System.currentTimeMillis() - start;
1171             if (timeSpent > INPUT_DISPATCH_READY_TIMEOUT) {
1172                 CLog.w("Timeout after waiting %dms on enabling of input dispatch", timeSpent);
1173                 // break & proceed anyway
1174                 break;
1175             } else {
1176                 getRunUtil().sleep(1000);
1177             }
1178         }
1179         if (getApiLevel() >= 23) {
1180             CLog.i(
1181                     "Attempting to disable keyguard on %s using %s",
1182                     getSerialNumber(), DISMISS_KEYGUARD_WM_CMD);
1183             String output = executeShellCommand(DISMISS_KEYGUARD_WM_CMD);
1184             CLog.i("output of %s: %s", DISMISS_KEYGUARD_WM_CMD, output);
1185         } else {
1186             CLog.i("Command: %s, is not supported, falling back to %s", DISMISS_KEYGUARD_WM_CMD,
1187                     DISMISS_KEYGUARD_CMD);
1188             executeShellCommand(DISMISS_KEYGUARD_CMD);
1189         }
1190         verifyKeyguardDismissed();
1191     }
1192 
verifyKeyguardDismissed()1193     private void verifyKeyguardDismissed() throws DeviceNotAvailableException {
1194         long start = System.currentTimeMillis();
1195         while (true) {
1196             KeyguardControllerState state = getKeyguardState();
1197             if (state == null) {
1198                 return; // unsupported
1199             }
1200             if (!state.isKeyguardShowing()) {
1201                 return; // keyguard dismissed successfully
1202             }
1203             long timeSpent = System.currentTimeMillis() - start;
1204             if (timeSpent > DISMISS_KEYGUARD_TIMEOUT) {
1205                 if (state.isKeyguardGoingAway()) {
1206                     CLog.w("Keyguard still going away %dms after being dismissed", timeSpent);
1207                 } else {
1208                     CLog.w("No response from keyguard %dms after being dismissed", timeSpent);
1209                 }
1210                 return; // proceed anyway, may be dismissed in a later step
1211             }
1212             getRunUtil().sleep(500);
1213         }
1214     }
1215 
1216     /** {@inheritDoc} */
1217     @Override
getKeyguardState()1218     public KeyguardControllerState getKeyguardState() throws DeviceNotAvailableException {
1219         String output = executeShellCommand(KEYGUARD_CONTROLLER_CMD);
1220         CLog.d("Output from KeyguardController: %s", output);
1221         KeyguardControllerState state =
1222                 KeyguardControllerState.create(Arrays.asList(output.trim().split("\n")));
1223         return state;
1224     }
1225 
1226     /**
1227      * Tests the device to see if input dispatcher is ready
1228      *
1229      * @return <code>null</code> if not supported by platform, or the actual readiness state
1230      * @throws DeviceNotAvailableException
1231      */
isDeviceInputReady()1232     Boolean isDeviceInputReady() throws DeviceNotAvailableException {
1233         CollectingOutputReceiver receiver = new CollectingOutputReceiver();
1234         executeShellCommand(TEST_INPUT_CMD, receiver);
1235         String output = receiver.getOutput();
1236         Matcher m = INPUT_DISPATCH_STATE_REGEX.matcher(output);
1237         if (!m.find()) {
1238             // output does not contain the line at all, implying unsupported API level, bail
1239             return null;
1240         }
1241         return "1".equals(m.group(1));
1242     }
1243 
1244     /**
1245      * {@inheritDoc}
1246      */
1247     @Override
prePostBootSetup()1248     protected void prePostBootSetup() throws DeviceNotAvailableException {
1249         if (mOptions.isDisableKeyguard()) {
1250             disableKeyguard();
1251         }
1252     }
1253 
1254     /**
1255      * Performs an reboot via framework power manager
1256      *
1257      * <p>Must have root access, device must be API Level 18 or above
1258      *
1259      * @param rebootMode a mode of this reboot.
1260      * @param reason for this reboot.
1261      * @return <code>true</code> if the device rebooted, <code>false</code> if not successful or
1262      *     unsupported
1263      * @throws DeviceNotAvailableException
1264      */
doAdbFrameworkReboot(RebootMode rebootMode, @Nullable final String reason)1265     private boolean doAdbFrameworkReboot(RebootMode rebootMode, @Nullable final String reason)
1266             throws DeviceNotAvailableException {
1267         // use framework reboot when:
1268         // 1. device API level >= 18
1269         // 2. has adb root
1270         // 3. framework is running
1271         if (!isEnableAdbRoot()) {
1272             CLog.i("framework reboot is not supported; when enable root is disabled");
1273             return false;
1274         }
1275         boolean isRoot = enableAdbRoot();
1276         if (getApiLevel() >= 18 && isRoot) {
1277             try {
1278                 // check framework running
1279                 String output = executeShellCommand("pm path android");
1280                 if (output == null || !output.contains("package:")) {
1281                     CLog.v("framework reboot: can't detect framework running");
1282                     return false;
1283                 }
1284                 notifyRebootStarted();
1285                 String command = "svc power reboot " + rebootMode.formatRebootCommand(reason);
1286                 CommandResult result = executeShellV2Command(command);
1287                 if (result.getStdout().contains(EARLY_REBOOT)
1288                         || result.getStderr().contains(EARLY_REBOOT)) {
1289                     CLog.e(
1290                             "Reboot was called too early: stdout: %s.\nstderr: %s.",
1291                             result.getStdout(), result.getStderr());
1292                     // notify of this reboot end, since reboot will be retried again at later stage.
1293                     notifyRebootEnded();
1294                     return false;
1295                 }
1296             } catch (DeviceUnresponsiveException due) {
1297                 CLog.v("framework reboot: device unresponsive to shell command, using fallback");
1298                 return false;
1299             }
1300             postAdbReboot();
1301             return true;
1302         } else {
1303             CLog.v("framework reboot: not supported");
1304             return false;
1305         }
1306     }
1307 
1308     /**
1309      * Perform a adb reboot.
1310      *
1311      * @param rebootMode a mode of this reboot.
1312      * @param reason for this reboot.
1313      * @throws DeviceNotAvailableException
1314      */
1315     @Override
doAdbReboot(RebootMode rebootMode, @Nullable final String reason)1316     protected void doAdbReboot(RebootMode rebootMode, @Nullable final String reason)
1317             throws DeviceNotAvailableException {
1318         getConnection().notifyAdbRebootCalled();
1319         if (!TestDeviceState.ONLINE.equals(getDeviceState())
1320                 || !doAdbFrameworkReboot(rebootMode, reason)) {
1321             super.doAdbReboot(rebootMode, reason);
1322         }
1323     }
1324 
1325     /**
1326      * {@inheritDoc}
1327      */
1328     @Override
getInstalledPackageNames()1329     public Set<String> getInstalledPackageNames() throws DeviceNotAvailableException {
1330         return getInstalledPackageNames(null, null);
1331     }
1332 
1333     // TODO: convert this to use DumpPkgAction
getInstalledPackageNames(String packageNameSearched, String userId)1334     private Set<String> getInstalledPackageNames(String packageNameSearched, String userId)
1335             throws DeviceNotAvailableException {
1336         Set<String> packages= new HashSet<String>();
1337         String command = LIST_PACKAGES_CMD;
1338         if (userId != null) {
1339             command += String.format(" --user %s", userId);
1340         }
1341         if (packageNameSearched != null) {
1342             command += (" | grep " + packageNameSearched);
1343         }
1344         String output = executeShellCommand(command);
1345         if (output != null) {
1346             Matcher m = PACKAGE_REGEX.matcher(output);
1347             while (m.find()) {
1348                 String packageName = m.group(2);
1349                 if (packageNameSearched != null && packageName.equals(packageNameSearched)) {
1350                     packages.add(packageName);
1351                 } else if (packageNameSearched == null) {
1352                     packages.add(packageName);
1353                 }
1354             }
1355         }
1356         return packages;
1357     }
1358 
1359     /** {@inheritDoc} */
1360     @Override
isPackageInstalled(String packageName)1361     public boolean isPackageInstalled(String packageName) throws DeviceNotAvailableException {
1362         return getInstalledPackageNames(packageName, null).contains(packageName);
1363     }
1364 
1365     /** {@inheritDoc} */
1366     @Override
isPackageInstalled(String packageName, String userId)1367     public boolean isPackageInstalled(String packageName, String userId)
1368             throws DeviceNotAvailableException {
1369         return getInstalledPackageNames(packageName, userId).contains(packageName);
1370     }
1371 
1372     /** {@inheritDoc} */
1373     @Override
getActiveApexes()1374     public Set<ApexInfo> getActiveApexes() throws DeviceNotAvailableException {
1375         String output = executeShellCommand(LIST_APEXES_CMD);
1376         // Optimistically parse expecting platform to return paths. If it doesn't, empty set will
1377         // be returned.
1378         Set<ApexInfo> ret = parseApexesFromOutput(output, true /* withPath */);
1379         if (ret.isEmpty()) {
1380             ret = parseApexesFromOutput(output, false /* withPath */);
1381         }
1382         return ret;
1383     }
1384 
1385     /** {@inheritDoc} */
1386     @Override
getMainlineModuleInfo()1387     public Set<String> getMainlineModuleInfo() throws DeviceNotAvailableException {
1388         checkApiLevelAgainstNextRelease(GET_MODULEINFOS_CMD, 29);
1389         Set<String> ret = new HashSet<>();
1390         String output = executeShellCommand(GET_MODULEINFOS_CMD);
1391         if (output != null) {
1392             Matcher m = MODULEINFO_REGEX.matcher(output);
1393             while (m.find()) {
1394                 String packageName = m.group(2);
1395                 ret.add(packageName);
1396             }
1397         }
1398         return ret;
1399     }
1400 
parseApexesFromOutput(final String output, boolean withPath)1401     private Set<ApexInfo> parseApexesFromOutput(final String output, boolean withPath) {
1402         Set<ApexInfo> ret = new HashSet<>();
1403         Matcher matcher =
1404                 withPath
1405                         ? APEXES_WITH_PATH_REGEX.matcher(output)
1406                         : APEXES_WITHOUT_PATH_REGEX.matcher(output);
1407         while (matcher.find()) {
1408             if (withPath) {
1409                 String sourceDir = matcher.group(1);
1410                 String name = matcher.group(2);
1411                 long version = Long.valueOf(matcher.group(3));
1412                 ret.add(new ApexInfo(name, version, sourceDir));
1413             } else {
1414                 String name = matcher.group(1);
1415                 long version = Long.valueOf(matcher.group(2));
1416                 ret.add(new ApexInfo(name, version));
1417             }
1418         }
1419         return ret;
1420     }
1421 
1422     /**
1423      * A {@link com.android.tradefed.device.NativeDevice.DeviceAction}
1424      * for retrieving package system service info, and do retries on
1425      * failures.
1426      */
1427     private class DumpPkgAction implements DeviceAction {
1428 
1429         Map<String, PackageInfo> mPkgInfoMap;
1430 
DumpPkgAction()1431         DumpPkgAction() {
1432         }
1433 
1434         @Override
run()1435         public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
1436                 ShellCommandUnresponsiveException, InstallException, SyncException {
1437             DumpsysPackageReceiver receiver = new DumpsysPackageReceiver();
1438             getIDevice().executeShellCommand("dumpsys package p", receiver);
1439             mPkgInfoMap = receiver.getPackages();
1440             if (mPkgInfoMap.size() == 0) {
1441                 // Package parsing can fail if package manager is currently down. throw exception
1442                 // to retry
1443                 CLog.w("no packages found from dumpsys package p.");
1444                 throw new IOException();
1445             }
1446             return true;
1447         }
1448     }
1449 
1450     /**
1451      * {@inheritDoc}
1452      */
1453     @Override
getUninstallablePackageNames()1454     public Set<String> getUninstallablePackageNames() throws DeviceNotAvailableException {
1455         DumpPkgAction action = new DumpPkgAction();
1456         performDeviceAction("dumpsys package p", action, MAX_RETRY_ATTEMPTS);
1457 
1458         Set<String> pkgs = new HashSet<String>();
1459         for (PackageInfo pkgInfo : action.mPkgInfoMap.values()) {
1460             if (!pkgInfo.isSystemApp() || pkgInfo.isUpdatedSystemApp()) {
1461                 CLog.d("Found uninstallable package %s", pkgInfo.getPackageName());
1462                 pkgs.add(pkgInfo.getPackageName());
1463             }
1464         }
1465         return pkgs;
1466     }
1467 
1468     /**
1469      * {@inheritDoc}
1470      */
1471     @Override
getAppPackageInfo(String packageName)1472     public PackageInfo getAppPackageInfo(String packageName) throws DeviceNotAvailableException {
1473         DumpPkgAction action = new DumpPkgAction();
1474         performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS);
1475         return action.mPkgInfoMap.get(packageName);
1476     }
1477 
1478     /** {@inheritDoc} */
1479     @Override
getAppPackageInfos()1480     public List<PackageInfo> getAppPackageInfos() throws DeviceNotAvailableException {
1481         DumpPkgAction action = new DumpPkgAction();
1482         performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS);
1483         return new ArrayList<>(action.mPkgInfoMap.values());
1484     }
1485 
1486     /** {@inheritDoc} */
1487     @Override
doesFileExist(String deviceFilePath)1488     public boolean doesFileExist(String deviceFilePath) throws DeviceNotAvailableException {
1489         int currentUser = 0;
1490         if (deviceFilePath.startsWith(SD_CARD)) {
1491             if (getApiLevel() > 23) {
1492                 // Don't trigger the current logic if unsupported
1493                 currentUser = getCurrentUser();
1494             }
1495         }
1496         return doesFileExist(deviceFilePath, currentUser);
1497     }
1498 
1499     /** {@inheritDoc} */
1500     @Override
doesFileExist(String deviceFilePath, int userId)1501     public boolean doesFileExist(String deviceFilePath, int userId)
1502             throws DeviceNotAvailableException {
1503         if (deviceFilePath.startsWith(SD_CARD)) {
1504             deviceFilePath =
1505                     deviceFilePath.replaceFirst(
1506                             SD_CARD, String.format("/storage/emulated/%s/", userId));
1507         }
1508         return super.doesFileExist(deviceFilePath, userId);
1509     }
1510 
1511     /**
1512      * {@inheritDoc}
1513      */
1514     @Override
listUsers()1515     public ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
1516         ArrayList<String[]> users = tokenizeListUsers();
1517         ArrayList<Integer> userIds = new ArrayList<Integer>(users.size());
1518         for (String[] user : users) {
1519             userIds.add(Integer.parseInt(user[1]));
1520         }
1521         return userIds;
1522     }
1523 
1524     /** {@inheritDoc} */
1525     @Override
getUserInfos()1526     public Map<Integer, UserInfo> getUserInfos() throws DeviceNotAvailableException {
1527         ArrayList<String[]> lines = tokenizeListUsers();
1528         Map<Integer, UserInfo> result = new HashMap<Integer, UserInfo>(lines.size());
1529         for (String[] tokens : lines) {
1530             if (getApiLevel() < 33) {
1531                 UserInfo userInfo =
1532                         new UserInfo(
1533                                 /* userId= */ Integer.parseInt(tokens[1]),
1534                                 /* userName= */ tokens[2],
1535                                 /* flag= */ Integer.parseInt(tokens[3], 16),
1536                                 /* isRunning= */ tokens.length >= 5
1537                                         ? tokens[4].contains("running")
1538                                         : false);
1539                 result.put(userInfo.userId(), userInfo);
1540             } else {
1541                 UserInfo userInfo =
1542                         new UserInfo(
1543                                 /* userId= */ Integer.parseInt(tokens[1]),
1544                                 /* userName= */ tokens[2],
1545                                 /* flag= */ Integer.parseInt(tokens[3], 16),
1546                                 /* isRunning= */ tokens.length >= 5
1547                                         ? tokens[4].contains("running")
1548                                         : false,
1549                                 /* userType= */ tokens[5]);
1550                 result.put(userInfo.userId(), userInfo);
1551             }
1552         }
1553         return result;
1554     }
1555 
1556     /**
1557      * Tokenizes the output of 'pm list users' pre-T and 'cmd user list -v' post-T.
1558      *
1559      * <p>Pre-T: The returned tokens for each user have the form: {"\tUserInfo",
1560      * Integer.toString(id), name, Integer.toHexString(flag), "[running]"}; (the last one being
1561      * optional)
1562      *
1563      * <p>Post-T: The returned tokens for each user have the form: {"\tUserInfo", Integer
1564      * .toString(id), name, Integer.toHexString(flag), "[running]", type}; (the last two being
1565      * optional)
1566      *
1567      * @return a list of arrays of strings, each element of the list representing the tokens for a
1568      *     user, or {@code null} if there was an error while tokenizing the adb command output.
1569      */
tokenizeListUsers()1570     private ArrayList<String[]> tokenizeListUsers() throws DeviceNotAvailableException {
1571         if (getApiLevel() < 33) { // Android-T
1572             return tokenizeListUsersPreT();
1573         } else {
1574             return tokenizeListUserPostT();
1575         }
1576     }
1577 
tokenizeListUserPostT()1578     private ArrayList<String[]> tokenizeListUserPostT() throws DeviceNotAvailableException {
1579         String command = "cmd user list -v";
1580         String commandOutput = null;
1581         for (int i = 0; i < NUM_USER_INFO_ATTEMPTS; i++) {
1582             commandOutput = executeShellCommand(command);
1583             if (commandOutput == null || commandOutput.trim().isEmpty()) {
1584                 CLog.d("Command `%s` executed with no output. (attempt %d)", command, i);
1585                 // Throw exception if the last attempt failed too.
1586                 if (i == NUM_USER_INFO_ATTEMPTS - 1) {
1587                     throw new DeviceRuntimeException(
1588                             String.format(
1589                                     "Command `%s` executed with no output. Attempts made = %d.",
1590                                     command, NUM_USER_INFO_ATTEMPTS),
1591                             DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
1592                 }
1593             } else {
1594                 break;
1595             }
1596             // wait before retrying
1597             RunUtil.getDefault().sleep(1000);
1598         }
1599         // Extract the id of all existing users.
1600         List<String> lines =
1601                 Arrays.stream(commandOutput.split("\\r?\\n"))
1602                         .filter(line -> line != null && line.trim().length() != 0)
1603                         .collect(Collectors.toList());
1604 
1605         if (!lines.get(0).contains("users:")) {
1606             if (commandOutput.contains("cmd: Can't find service: package")) {
1607                 throw new DeviceNotAvailableException(
1608                         String.format(
1609                                 "'%s' in not a valid output for 'user list -v'", commandOutput),
1610                         getSerialNumber());
1611             }
1612             throw new DeviceRuntimeException(
1613                     String.format("'%s' in not a valid output for 'user list -v'", commandOutput),
1614                     DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
1615         }
1616         ArrayList<String[]> users = new ArrayList<String[]>(lines.size() - 1);
1617 
1618         String pattern = ".id=(.*), name=(.*), type=(.*), flags=(.*)";
1619         Pattern r = Pattern.compile(pattern);
1620         for (int i = 1; i < lines.size(); i++) {
1621             // Individual user is printed out like this:
1622             // idx: id=$id, name=$name, type=$type, flags=AAA|BBB|XXX (running) (current) (visible)
1623             Matcher m = r.matcher(lines.get(i));
1624             if (m.find()) {
1625                 String id = m.group(1);
1626                 String name = m.group(2);
1627                 // example: full.SYSTEM, profile.XXX
1628                 String type = m.group(3);
1629                 // AAA|BBB|XXX (running) (current) (visible)
1630                 String flags_and_status = m.group(4);
1631 
1632                 String flags = "";
1633                 String status = "";
1634                 if (flags_and_status != null) {
1635                     // Split flags and convert to hex
1636                     // output: [AAA, BBB, XXX (running) (current) (visible)]
1637                     String[] flagsArr = flags_and_status.split("\\|");
1638                     // XXX (running) (current) (visible)
1639                     String last_flag_and_status =
1640                             flagsArr.length > 0 ? flagsArr[flagsArr.length - 1] : "";
1641                     String[] arr = last_flag_and_status.split("\\s", 2);
1642                     if (arr.length > 0) {
1643                         flags = Integer.toHexString(convertToHex(flagsArr, arr[0]));
1644                     }
1645                     if (arr.length > 1) {
1646                         status = arr[1] != null ? arr[1] : "";
1647                     }
1648                 }
1649                 // Maintain same sequence as per-Q output, add type at the end.
1650                 users.add(new String[] {"", id, name, flags, status, type});
1651             }
1652         }
1653         return users;
1654     }
1655 
tokenizeListUsersPreT()1656     private ArrayList<String[]> tokenizeListUsersPreT() throws DeviceNotAvailableException {
1657         String command = "pm list users";
1658         String commandOutput = null;
1659         for (int i = 0; i < NUM_USER_INFO_ATTEMPTS; i++) {
1660             commandOutput = executeShellCommand(command);
1661             if (commandOutput == null || commandOutput.trim().isEmpty()) {
1662                 CLog.d("Command `%s` executed with no output. (attempt %d)", command, i);
1663                 // Throw exception if the last attempt failed too.
1664                 if (i == NUM_USER_INFO_ATTEMPTS - 1) {
1665                     throw new DeviceRuntimeException(
1666                             String.format(
1667                                     "Command `%s` executed with no output. Attempts made = %d.",
1668                                     command, NUM_USER_INFO_ATTEMPTS),
1669                             DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
1670                 }
1671             } else {
1672                 break;
1673             }
1674             // wait before retrying
1675             RunUtil.getDefault().sleep(1000);
1676         }
1677         // Extract the id of all existing users.
1678         String[] lines = commandOutput.split("\\r?\\n");
1679         if (!lines[0].equals("Users:")) {
1680             throw new DeviceRuntimeException(
1681                     String.format("'%s' in not a valid output for 'pm list users'", commandOutput),
1682                     DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
1683         }
1684         ArrayList<String[]> users = new ArrayList<String[]>(lines.length - 1);
1685         for (int i = 1; i < lines.length; i++) {
1686             // Individual user is printed out like this:
1687             // \tUserInfo{$id$:$name$:$Integer.toHexString(flags)$} [running]
1688             String[] tokens = lines[i].split("\\{|\\}|:");
1689             if (tokens.length != 4 && tokens.length != 5) {
1690                 throw new DeviceRuntimeException(
1691                         String.format(
1692                                 "device output: '%s' \nline: '%s' was not in the expected "
1693                                         + "format for user info.",
1694                                 commandOutput, lines[i]),
1695                         DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
1696             }
1697             users.add(tokens);
1698         }
1699         return users;
1700     }
1701 
convertToHex(String[] arr, String str)1702     private int convertToHex(String[] arr, String str) {
1703         int res = 0;
1704 
1705         for (int i = 0; i < arr.length - 1; i++) {
1706             res |= getHexaDecimalValue(arr[i]);
1707         }
1708         res |= getHexaDecimalValue(str);
1709 
1710         return res;
1711     }
1712 
getHexaDecimalValue(String flag)1713     private int getHexaDecimalValue(String flag) {
1714         switch (flag) {
1715             case "PRIMARY":
1716                 return 0x00000001;
1717             case "ADMIN":
1718                 return 0x00000002;
1719             case "GUEST":
1720                 return 0x00000004;
1721             case "RESTRICTED":
1722                 return 0x00000008;
1723             case "INITIALIZED":
1724                 return 0x00000010;
1725             case "MANAGED_PROFILE":
1726                 return 0x00000020;
1727             case "DISABLED":
1728                 return 0x00000040;
1729             case "QUIET_MODE":
1730                 return 0x00000080;
1731             case "EPHEMERAL":
1732                 return 0x00000100;
1733             case "DEMO":
1734                 return 0x00000200;
1735             case "FULL":
1736                 return 0x00000400;
1737             case "SYSTEM":
1738                 return 0x00000800;
1739             case "PROFILE":
1740                 return 0x00001000;
1741             case "EPHEMERAL_ON_CREATE":
1742                 return 0x00002000;
1743             case "MAIN":
1744                 return 0x00004000;
1745             case "FOR_TESTING":
1746                 return 0x00008000;
1747             default:
1748                 CLog.e("Flag %s not found.", flag);
1749                 return 0;
1750         }
1751     }
1752 
1753     /**
1754      * {@inheritDoc}
1755      */
1756     @Override
getMaxNumberOfUsersSupported()1757     public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
1758         String command = "pm get-max-users";
1759         String commandOutput = executeShellCommand(command);
1760         try {
1761             return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
1762         } catch (NumberFormatException e) {
1763             CLog.e("Failed to parse result: %s", commandOutput);
1764         }
1765         return 0;
1766     }
1767 
1768     /** {@inheritDoc} */
1769     @Override
getMaxNumberOfRunningUsersSupported()1770     public int getMaxNumberOfRunningUsersSupported() throws DeviceNotAvailableException {
1771         checkApiLevelAgainstNextRelease("get-max-running-users", 28);
1772         String command = "pm get-max-running-users";
1773         String commandOutput = executeShellCommand(command);
1774         try {
1775             return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
1776         } catch (NumberFormatException e) {
1777             CLog.e("Failed to parse result: %s", commandOutput);
1778         }
1779         return 0;
1780     }
1781 
1782     /**
1783      * {@inheritDoc}
1784      */
1785     @Override
isMultiUserSupported()1786     public boolean isMultiUserSupported() throws DeviceNotAvailableException {
1787         checkApiLevelAgainstNextRelease("get-max-running-users", 28);
1788         final int apiLevel = getApiLevel();
1789         if (apiLevel > 33) {
1790             String command = "pm supports-multiple-users";
1791             String commandOutput = executeShellCommand(command).trim();
1792             try {
1793                 String parsedOutput =
1794                         commandOutput.substring(commandOutput.lastIndexOf(" ")).trim();
1795                 Boolean retValue = Boolean.valueOf(parsedOutput);
1796                 return retValue;
1797             } catch (NumberFormatException e) {
1798                 CLog.e("Failed to parse result: %s", commandOutput);
1799                 return false;
1800             }
1801         }
1802         return getMaxNumberOfUsersSupported() > 1;
1803     }
1804 
1805     @Override
isHeadlessSystemUserMode()1806     public boolean isHeadlessSystemUserMode() throws DeviceNotAvailableException {
1807         checkApiLevelAgainst("isHeadlessSystemUserMode", 29);
1808         return checkApiLevelAgainstNextRelease(34)
1809                 ? executeShellV2CommandThatReturnsBooleanSafe(
1810                         "cmd user is-headless-system-user-mode")
1811                 : getBooleanProperty("ro.fw.mu.headless_system_user", false);
1812     }
1813 
1814     /** {@inheritDoc} */
1815     @Override
canSwitchToHeadlessSystemUser()1816     public boolean canSwitchToHeadlessSystemUser() throws DeviceNotAvailableException {
1817         checkApiLevelAgainst("canSwitchToHeadlessSystemUser", 34);
1818         return executeShellV2CommandThatReturnsBooleanSafe(
1819                 "cmd user can-switch-to-headless-system-user");
1820     }
1821 
1822     /** {@inheritDoc} */
1823     @Override
isMainUserPermanentAdmin()1824     public boolean isMainUserPermanentAdmin() throws DeviceNotAvailableException {
1825         checkApiLevelAgainst("isMainUserPermanentAdmin", 34);
1826         return executeShellV2CommandThatReturnsBooleanSafe("cmd user is-main-user-permanent-admin");
1827     }
1828 
1829     /**
1830      * {@inheritDoc}
1831      */
1832     @Override
createUser(String name)1833     public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException {
1834         return createUser(name, false, false);
1835     }
1836 
1837     /**
1838      * {@inheritDoc}
1839      */
1840     @Override
createUser(String name, boolean guest, boolean ephemeral)1841     public int createUser(String name, boolean guest, boolean ephemeral)
1842             throws DeviceNotAvailableException, IllegalStateException {
1843         return createUser(name, guest, ephemeral, /* forTesting= */ false);
1844     }
1845 
1846     /** {@inheritDoc} */
1847     @Override
createUser(String name, boolean guest, boolean ephemeral, boolean forTesting)1848     public int createUser(String name, boolean guest, boolean ephemeral, boolean forTesting)
1849             throws DeviceNotAvailableException, IllegalStateException {
1850         String command =
1851                 "pm create-user "
1852                         + (guest ? "--guest " : "")
1853                         + (ephemeral ? "--ephemeral " : "")
1854                         + (forTesting && getApiLevel() >= 34 ? "--for-testing " : "")
1855                         + name;
1856         final String output = executeShellCommand(command);
1857         if (output.startsWith("Success")) {
1858             try {
1859                 resetContentProviderSetup();
1860                 return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim());
1861             } catch (NumberFormatException e) {
1862                 CLog.e("Failed to parse result: %s", output);
1863             }
1864         }
1865         throw new IllegalStateException(String.format("Failed to create user: %s", output));
1866     }
1867 
1868     /** {@inheritDoc} */
1869     @Override
createUserNoThrow(String name)1870     public int createUserNoThrow(String name) throws DeviceNotAvailableException {
1871         try {
1872             return createUser(name);
1873         } catch (IllegalStateException e) {
1874             CLog.e("Error creating user: " + e.toString());
1875             return -1;
1876         }
1877     }
1878 
1879     /**
1880      * {@inheritDoc}
1881      */
1882     @Override
removeUser(int userId)1883     public boolean removeUser(int userId) throws DeviceNotAvailableException {
1884         final String output = executeShellCommand(String.format("pm remove-user %s", userId));
1885         if (output.startsWith("Error")) {
1886             CLog.e("Failed to remove user %d on device %s: %s", userId, getSerialNumber(), output);
1887             return false;
1888         }
1889         return true;
1890     }
1891 
1892     /**
1893      * {@inheritDoc}
1894      */
1895     @Override
startUser(int userId)1896     public boolean startUser(int userId) throws DeviceNotAvailableException {
1897         return startUser(userId, false);
1898     }
1899 
1900     /** {@inheritDoc} */
1901     @Override
startUser(int userId, boolean waitFlag)1902     public boolean startUser(int userId, boolean waitFlag) throws DeviceNotAvailableException {
1903         if (waitFlag) {
1904             checkApiLevelAgainstNextRelease("start-user -w", 29);
1905         }
1906         String cmd = "am start-user " + (waitFlag ? "-w " : "") + userId;
1907 
1908         CLog.d("Starting user with command: %s", cmd);
1909         final String output = executeShellCommand(cmd);
1910         if (output.startsWith("Error")) {
1911             CLog.e("Failed to start user: %s", output);
1912             return false;
1913         }
1914         if (waitFlag) {
1915             String state = executeShellCommand("am get-started-user-state " + userId);
1916             if (!state.contains("RUNNING_UNLOCKED")) {
1917                 CLog.w("User %s is not RUNNING_UNLOCKED after start-user -w. (%s).", userId, state);
1918                 return false;
1919             }
1920         }
1921         return true;
1922     }
1923 
1924     @Override
startVisibleBackgroundUser(int userId, int displayId, boolean waitFlag)1925     public boolean startVisibleBackgroundUser(int userId, int displayId, boolean waitFlag)
1926             throws DeviceNotAvailableException {
1927         checkApiLevelAgainstNextRelease("startVisibleBackgroundUser", 34);
1928 
1929         String cmd =
1930                 String.format(
1931                         "am start-user%s --display %d %d",
1932                         (waitFlag ? " -w" : ""), displayId, userId);
1933         CommandResult res = executeShellV2Command(cmd);
1934         if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
1935             throw new DeviceRuntimeException(
1936                     "Command  '" + cmd + "' failed: " + res,
1937                     DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
1938         }
1939         return res.getStdout().trim().startsWith("Success");
1940     }
1941 
1942     /**
1943      * {@inheritDoc}
1944      */
1945     @Override
stopUser(int userId)1946     public boolean stopUser(int userId) throws DeviceNotAvailableException {
1947         // No error or status code is returned.
1948         return stopUser(userId, false, false);
1949     }
1950 
1951     /**
1952      * {@inheritDoc}
1953      */
1954     @Override
stopUser(int userId, boolean waitFlag, boolean forceFlag)1955     public boolean stopUser(int userId, boolean waitFlag, boolean forceFlag)
1956             throws DeviceNotAvailableException {
1957         final int apiLevel = getApiLevel();
1958         if (waitFlag && apiLevel < 23) {
1959             throw new IllegalArgumentException("stop-user -w requires API level >= 23");
1960         }
1961         if (forceFlag && apiLevel < 24) {
1962             throw new IllegalArgumentException("stop-user -f requires API level >= 24");
1963         }
1964         StringBuilder cmd = new StringBuilder("am stop-user ");
1965         if (waitFlag) {
1966             cmd.append("-w ");
1967         }
1968         if (forceFlag) {
1969             cmd.append("-f ");
1970         }
1971         cmd.append(userId);
1972 
1973         CLog.d("stopping user with command: %s", cmd.toString());
1974         final String output = executeShellCommand(cmd.toString());
1975         if (output.contains("Error: Can't stop system user")) {
1976             CLog.e("Cannot stop System user.");
1977             return false;
1978         }
1979         if (output.contains("Can't stop current user")) {
1980             CLog.e("Cannot stop current user.");
1981             return false;
1982         }
1983         if (isUserRunning(userId)) {
1984             CLog.w("User Id: %s is still running after the stop-user command.", userId);
1985             return false;
1986         }
1987         return true;
1988     }
1989 
1990     @Override
isVisibleBackgroundUsersSupported()1991     public boolean isVisibleBackgroundUsersSupported() throws DeviceNotAvailableException {
1992         checkApiLevelAgainstNextRelease("isHeadlessSystemUserMode", 34);
1993 
1994         return executeShellV2CommandThatReturnsBoolean(
1995                 "cmd user is-visible-background-users-supported");
1996     }
1997 
1998     @Override
isVisibleBackgroundUsersOnDefaultDisplaySupported()1999     public boolean isVisibleBackgroundUsersOnDefaultDisplaySupported()
2000             throws DeviceNotAvailableException {
2001         checkApiLevelAgainstNextRelease("isVisibleBackgroundUsersOnDefaultDisplaySupported", 34);
2002 
2003         return executeShellV2CommandThatReturnsBoolean(
2004                 "cmd user is-visible-background-users-on-default-display-supported");
2005     }
2006 
2007     /**
2008      * {@inheritDoc}
2009      */
2010     @Override
getPrimaryUserId()2011     public Integer getPrimaryUserId() throws DeviceNotAvailableException {
2012         return getUserIdByFlag(FLAG_PRIMARY);
2013     }
2014 
2015     /** {@inheritDoc} */
2016     @Override
getMainUserId()2017     public Integer getMainUserId() throws DeviceNotAvailableException {
2018         return getUserIdByFlag(FLAG_MAIN);
2019     }
2020 
getUserIdByFlag(int requiredFlag)2021     private Integer getUserIdByFlag(int requiredFlag) throws DeviceNotAvailableException {
2022         ArrayList<String[]> users = tokenizeListUsers();
2023         for (String[] user : users) {
2024             int flag = Integer.parseInt(user[3], 16);
2025             if ((flag & requiredFlag) != 0) {
2026                 return Integer.parseInt(user[1]);
2027             }
2028         }
2029         return null;
2030     }
2031 
2032     /** {@inheritDoc} */
2033     @Override
getCurrentUser()2034     public int getCurrentUser() throws DeviceNotAvailableException {
2035         checkApiLevelAgainstNextRelease("get-current-user", API_LEVEL_GET_CURRENT_USER);
2036         final String output = executeShellCommand("am get-current-user");
2037         try {
2038             int userId = Integer.parseInt(output.trim());
2039             if (userId >= 0) {
2040                 return userId;
2041             }
2042             CLog.e("Invalid user id '%s' was returned for get-current-user", userId);
2043         } catch (NumberFormatException e) {
2044             CLog.e("Invalid string was returned for get-current-user: %s.", output);
2045         }
2046         return INVALID_USER_ID;
2047     }
2048 
2049     @Override
isUserVisible(int userId)2050     public boolean isUserVisible(int userId) throws DeviceNotAvailableException {
2051         checkApiLevelAgainstNextRelease("isUserVisible", 34);
2052 
2053         return executeShellV2CommandThatReturnsBoolean("cmd user is-user-visible %d", userId);
2054     }
2055 
2056     @Override
isUserVisibleOnDisplay(int userId, int displayId)2057     public boolean isUserVisibleOnDisplay(int userId, int displayId)
2058             throws DeviceNotAvailableException {
2059         checkApiLevelAgainstNextRelease("isUserVisibleOnDisplay", 34);
2060 
2061         return executeShellV2CommandThatReturnsBoolean(
2062                 "cmd user is-user-visible --display %d %d", displayId, userId);
2063     }
2064 
findUserInfo(String pmListUsersOutput)2065     private Matcher findUserInfo(String pmListUsersOutput) {
2066         Pattern pattern = Pattern.compile(USER_PATTERN);
2067         Matcher matcher = pattern.matcher(pmListUsersOutput);
2068         return matcher;
2069     }
2070 
2071     /**
2072      * {@inheritDoc}
2073      */
2074     @Override
getUserFlags(int userId)2075     public int getUserFlags(int userId) throws DeviceNotAvailableException {
2076         checkApiLevelAgainst("getUserFlags", 22);
2077         final String commandOutput = executeShellCommand("pm list users");
2078         Matcher matcher = findUserInfo(commandOutput);
2079         while(matcher.find()) {
2080             if (Integer.parseInt(matcher.group(2)) == userId) {
2081                 return Integer.parseInt(matcher.group(6), 16);
2082             }
2083         }
2084         CLog.w("Could not find any flags for userId: %d in output: %s", userId, commandOutput);
2085         return INVALID_USER_ID;
2086     }
2087 
2088     /** {@inheritDoc} */
2089     @Override
isUserSecondary(int userId)2090     public boolean isUserSecondary(int userId) throws DeviceNotAvailableException {
2091         if (userId == UserInfo.USER_SYSTEM) {
2092             return false;
2093         }
2094         int flags = getUserFlags(userId);
2095         if (flags == INVALID_USER_ID) {
2096             return false;
2097         }
2098         return (flags & UserInfo.FLAGS_NOT_SECONDARY) == 0;
2099     }
2100 
2101     /**
2102      * {@inheritDoc}
2103      */
2104     @Override
isUserRunning(int userId)2105     public boolean isUserRunning(int userId) throws DeviceNotAvailableException {
2106         checkApiLevelAgainst("isUserIdRunning", 22);
2107         final String commandOutput = executeShellCommand("pm list users");
2108         Matcher matcher = findUserInfo(commandOutput);
2109         while(matcher.find()) {
2110             if (Integer.parseInt(matcher.group(2)) == userId) {
2111                 if (matcher.group(7).contains("running")) {
2112                     return true;
2113                 }
2114             }
2115         }
2116         return false;
2117     }
2118 
2119     /**
2120      * {@inheritDoc}
2121      */
2122     @Override
getUserSerialNumber(int userId)2123     public int getUserSerialNumber(int userId) throws DeviceNotAvailableException {
2124         checkApiLevelAgainst("getUserSerialNumber", 22);
2125         final String commandOutput = executeShellCommand("dumpsys user");
2126         // example: UserInfo{0:Test:13} serialNo=0
2127         String userSerialPatter = "(.*\\{)(\\d+)(.*\\})(.*=)(\\d+)";
2128         Pattern pattern = Pattern.compile(userSerialPatter);
2129         Matcher matcher = pattern.matcher(commandOutput);
2130         while(matcher.find()) {
2131             if (Integer.parseInt(matcher.group(2)) == userId) {
2132                 return Integer.parseInt(matcher.group(5));
2133             }
2134         }
2135         CLog.w("Could not find user serial number for userId: %d, in output: %s",
2136                 userId, commandOutput);
2137         return INVALID_USER_ID;
2138     }
2139 
2140     /**
2141      * {@inheritDoc}
2142      */
2143     @Override
switchUser(int userId)2144     public boolean switchUser(int userId) throws DeviceNotAvailableException {
2145         return switchUser(userId, AM_COMMAND_TIMEOUT);
2146     }
2147 
2148     /**
2149      * {@inheritDoc}
2150      */
2151     @Override
switchUser(int userId, long timeout)2152     public boolean switchUser(int userId, long timeout) throws DeviceNotAvailableException {
2153         checkApiLevelAgainstNextRelease("switchUser", API_LEVEL_GET_CURRENT_USER);
2154         if (userId == getCurrentUser()) {
2155             CLog.w("Already running as user id: %s. Nothing to be done.", userId);
2156             return true;
2157         }
2158 
2159         String switchCommand =
2160                 checkApiLevelAgainstNextRelease(30)
2161                         ? String.format("am switch-user -w %d", userId)
2162                         : String.format("am switch-user %d", userId);
2163 
2164         resetContentProviderSetup();
2165         long initialTime = getHostCurrentTime();
2166         String output = executeShellCommand(switchCommand);
2167         boolean success = userId == getCurrentUser();
2168 
2169         while (!success && (getHostCurrentTime() - initialTime <= timeout)) {
2170             // retry
2171             RunUtil.getDefault().sleep(getCheckNewUserSleep());
2172             output = executeShellCommand(String.format(switchCommand));
2173             success = userId == getCurrentUser();
2174         }
2175 
2176         CLog.d("switchUser took %d ms", getHostCurrentTime() - initialTime);
2177         if (success) {
2178             prePostBootSetup();
2179             return true;
2180         } else {
2181             CLog.e("User did not switch in the given %d timeout: %s", timeout, output);
2182             return false;
2183         }
2184     }
2185 
2186     /**
2187      * Exposed for testing.
2188      */
getCheckNewUserSleep()2189     protected long getCheckNewUserSleep() {
2190         return CHECK_NEW_USER;
2191     }
2192 
2193     /**
2194      * Exposed for testing
2195      */
getHostCurrentTime()2196     protected long getHostCurrentTime() {
2197         return System.currentTimeMillis();
2198     }
2199 
2200     /**
2201      * {@inheritDoc}
2202      */
2203     @Override
hasFeature(String feature)2204     public boolean hasFeature(String feature) throws DeviceNotAvailableException {
2205         // Add support for directly checking a feature and match the pm output.
2206         if (!feature.startsWith("feature:")) {
2207             feature = "feature:" + feature;
2208         }
2209         final String versionedFeature = feature + "=";
2210         CommandResult commandResult = executeShellV2Command("pm list features");
2211         if (!CommandStatus.SUCCESS.equals(commandResult.getStatus())) {
2212             throw new DeviceRuntimeException(
2213                     String.format(
2214                             "Failed to list features, command returned: stdout: %s, stderr: %s",
2215                             commandResult.getStdout(), commandResult.getStderr()),
2216                     DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
2217         }
2218         String commandOutput = commandResult.getStdout();
2219         for (String line: commandOutput.split("\\s+")) {
2220             // Each line in the output of the command has the format
2221             // "feature:{FEATURE_VALUE}[={FEATURE_VERSION}]".
2222             if (line.equals(feature)) {
2223                 return true;
2224             }
2225             if (line.startsWith(versionedFeature)) {
2226                 return true;
2227             }
2228         }
2229         CLog.w("Feature: %s is not available on %s", feature, getSerialNumber());
2230         return false;
2231     }
2232 
2233     /**
2234      * {@inheritDoc}
2235      */
2236     @Override
getSetting(String namespace, String key)2237     public String getSetting(String namespace, String key) throws DeviceNotAvailableException {
2238         return getSettingInternal("", namespace.trim(), key.trim());
2239     }
2240 
2241     /**
2242      * {@inheritDoc}
2243      */
2244     @Override
getSetting(int userId, String namespace, String key)2245     public String getSetting(int userId, String namespace, String key)
2246             throws DeviceNotAvailableException {
2247         return getSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim());
2248     }
2249 
2250     /**
2251      * Internal Helper to get setting with or without a userId provided.
2252      */
getSettingInternal(String userFlag, String namespace, String key)2253     private String getSettingInternal(String userFlag, String namespace, String key)
2254             throws DeviceNotAvailableException {
2255         namespace = namespace.toLowerCase();
2256         if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) {
2257             String cmd = String.format("settings %s get %s %s", userFlag, namespace, key);
2258             String output = executeShellCommand(cmd);
2259             if ("null".equals(output)) {
2260                 CLog.w("settings returned null for command: %s. "
2261                         + "please check if the namespace:key exists", cmd);
2262                 return null;
2263             }
2264             return output.trim();
2265         }
2266         CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace);
2267         return null;
2268     }
2269 
2270     /** {@inheritDoc} */
2271     @Override
getAllSettings(String namespace)2272     public Map<String, String> getAllSettings(String namespace) throws DeviceNotAvailableException {
2273         return getAllSettingsInternal(namespace.trim());
2274     }
2275 
2276     /** Internal helper to get all settings */
getAllSettingsInternal(String namespace)2277     private Map<String, String> getAllSettingsInternal(String namespace)
2278             throws DeviceNotAvailableException {
2279         namespace = namespace.toLowerCase();
2280         if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) {
2281             Map<String, String> map = new HashMap<>();
2282             String cmd = String.format("settings list %s", namespace);
2283             String output = executeShellCommand(cmd);
2284             for (String line : output.split("\\n")) {
2285                 // Setting's value could be empty
2286                 String[] pair = line.trim().split("=", -1);
2287                 if (pair.length > 1) {
2288                     map.putIfAbsent(pair[0], pair[1]);
2289                 } else {
2290                     CLog.e("Unable to get setting from string: %s", line);
2291                 }
2292             }
2293             return map;
2294         }
2295         CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace);
2296         return null;
2297     }
2298 
2299     /**
2300      * {@inheritDoc}
2301      */
2302     @Override
setSetting(String namespace, String key, String value)2303     public void setSetting(String namespace, String key, String value)
2304             throws DeviceNotAvailableException {
2305         setSettingInternal("", namespace.trim(), key.trim(), value.trim());
2306     }
2307 
2308     /**
2309      * {@inheritDoc}
2310      */
2311     @Override
setSetting(int userId, String namespace, String key, String value)2312     public void setSetting(int userId, String namespace, String key, String value)
2313             throws DeviceNotAvailableException {
2314         setSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim(),
2315                 value.trim());
2316     }
2317 
2318     /**
2319      * Internal helper to set a setting with or without a userId provided.
2320      */
setSettingInternal(String userFlag, String namespace, String key, String value)2321     private void setSettingInternal(String userFlag, String namespace, String key, String value)
2322             throws DeviceNotAvailableException {
2323         checkApiLevelAgainst("Changing settings", 22);
2324         if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace.toLowerCase())) {
2325             executeShellCommand(String.format("settings %s put %s %s %s",
2326                     userFlag, namespace, key, value));
2327         } else {
2328             throw new IllegalArgumentException("Namespace must be one of system, secure, global."
2329                     + " You provided: " + namespace);
2330         }
2331     }
2332 
2333     /**
2334      * {@inheritDoc}
2335      */
2336     @Override
getAndroidId(int userId)2337     public String getAndroidId(int userId) throws DeviceNotAvailableException {
2338         if (isAdbRoot()) {
2339             String cmd = String.format(
2340                     "sqlite3 /data/user/%d/*/databases/gservices.db "
2341                     + "'select value from main where name = \"android_id\"'", userId);
2342             String output = executeShellCommand(cmd).trim();
2343             if (!output.contains("unable to open database")) {
2344                 return output;
2345             }
2346             CLog.w("Couldn't find android-id, output: %s", output);
2347         } else {
2348             CLog.w("adb root is required.");
2349         }
2350         return null;
2351     }
2352 
2353     /**
2354      * {@inheritDoc}
2355      */
2356     @Override
getAndroidIds()2357     public Map<Integer, String> getAndroidIds() throws DeviceNotAvailableException {
2358         ArrayList<Integer> userIds = listUsers();
2359         if (userIds == null) {
2360             return null;
2361         }
2362         Map<Integer, String> androidIds = new HashMap<Integer, String>();
2363         for (Integer id : userIds) {
2364             String androidId = getAndroidId(id);
2365             androidIds.put(id, androidId);
2366         }
2367         return androidIds;
2368     }
2369 
2370     /**
2371      * {@inheritDoc}
2372      */
2373     @Override
createWifiHelper()2374     IWifiHelper createWifiHelper() throws DeviceNotAvailableException {
2375         return createWifiHelper(false);
2376     }
2377 
2378     @Override
createWifiHelper(boolean useV2)2379     IWifiHelper createWifiHelper(boolean useV2) throws DeviceNotAvailableException {
2380         if (useV2) {
2381             CLog.d("Using WifiHelper V2. WifiUtil apk installation skipped.");
2382             InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.WIFI_HELPER_V2, "true");
2383             return createWifiHelper(useV2, false);
2384         } else {
2385             return createWifiHelper(useV2, true);
2386         }
2387     }
2388 
2389     /**
2390      * Alternative to {@link #createWifiHelper()} where we can choose whether to do the wifi helper
2391      * setup or not.
2392      */
2393     @VisibleForTesting
createWifiHelper(boolean useV2, boolean doSetup)2394     IWifiHelper createWifiHelper(boolean useV2, boolean doSetup)
2395             throws DeviceNotAvailableException {
2396         if (doSetup) {
2397             mWasWifiHelperInstalled = true;
2398             // Ensure device is ready before attempting wifi setup
2399             waitForDeviceAvailable();
2400         }
2401         return new WifiHelper(this, mOptions.getWifiUtilAPKPath(), doSetup, useV2);
2402     }
2403 
2404     /** {@inheritDoc} */
2405     @Override
postInvocationTearDown(Throwable exception)2406     public void postInvocationTearDown(Throwable exception) {
2407         super.postInvocationTearDown(exception);
2408         // If wifi was installed and it's a real device, attempt to clean it.
2409         if (mWasWifiHelperInstalled) {
2410             mWasWifiHelperInstalled = false;
2411             if (getIDevice() instanceof StubDevice) {
2412                 return;
2413             }
2414             if (!TestDeviceState.ONLINE.equals(getDeviceState())) {
2415                 return;
2416             }
2417             if (exception instanceof DeviceNotAvailableException) {
2418                 CLog.e("Skip WifiHelper teardown due to DeviceNotAvailableException.");
2419                 return;
2420             }
2421             try {
2422                 // Uninstall the wifi utility if it was installed.
2423                 IWifiHelper wifi = createWifiHelper(false, false);
2424                 wifi.cleanUp();
2425             } catch (DeviceNotAvailableException e) {
2426                 CLog.e("Device became unavailable while uninstalling wifi util.");
2427                 CLog.e(e);
2428             }
2429         }
2430     }
2431 
2432     /** {@inheritDoc} */
2433     @Override
setDeviceOwner(String componentName, int userId)2434     public boolean setDeviceOwner(String componentName, int userId)
2435             throws DeviceNotAvailableException {
2436         final String command = "dpm set-device-owner --user " + userId + " '" + componentName + "'";
2437         final String commandOutput = executeShellCommand(command);
2438         return commandOutput.startsWith("Success:");
2439     }
2440 
2441     /** {@inheritDoc} */
2442     @Override
removeAdmin(String componentName, int userId)2443     public boolean removeAdmin(String componentName, int userId)
2444             throws DeviceNotAvailableException {
2445         final String command =
2446                 "dpm remove-active-admin --user " + userId + " '" + componentName + "'";
2447         final String commandOutput = executeShellCommand(command);
2448         return commandOutput.startsWith("Success:");
2449     }
2450 
2451     /** {@inheritDoc} */
2452     @Override
removeOwners()2453     public void removeOwners() throws DeviceNotAvailableException {
2454         String command = "dumpsys device_policy";
2455         String commandOutput = executeShellCommand(command);
2456         String[] lines = commandOutput.split("\\r?\\n");
2457         for (int i = 0; i < lines.length; ++i) {
2458             String line = lines[i].trim();
2459             if (line.contains("Profile Owner")) {
2460                 // Line is "Profile owner (User <id>):
2461                 String[] tokens = line.split("\\(|\\)| ");
2462                 int userId = Integer.parseInt(tokens[4]);
2463 
2464                 i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i);
2465                 line = lines[i].trim();
2466                 // Line is admin=ComponentInfo{<component>}
2467                 tokens = line.split("\\{|\\}");
2468                 String componentName = tokens[1];
2469                 CLog.d("Cleaning up profile owner " + userId + " " + componentName);
2470                 removeAdmin(componentName, userId);
2471             } else if (line.contains("Device Owner:")) {
2472                 i = moveToNextIndexMatchingRegex(".*admin=.*", lines, i);
2473                 line = lines[i].trim();
2474                 // Line is admin=ComponentInfo{<component>}
2475                 String[] tokens = line.split("\\{|\\}");
2476                 String componentName = tokens[1];
2477 
2478                 // Skip to user id line.
2479                 i = moveToNextIndexMatchingRegex(".*User ID:.*", lines, i);
2480                 line = lines[i].trim();
2481                 // Line is User ID: <N>
2482                 tokens = line.split(":");
2483                 int userId = Integer.parseInt(tokens[1].trim());
2484                 CLog.d("Cleaning up device owner " + userId + " " + componentName);
2485                 removeAdmin(componentName, userId);
2486             }
2487         }
2488     }
2489 
2490     /**
2491      * Search forward from the current index to find a string matching the given regex.
2492      *
2493      * @param regex The regex to match each line against.
2494      * @param lines An array of strings to be searched.
2495      * @param currentIndex the index to start searching from.
2496      * @return The index of a string beginning with the regex.
2497      * @throws IllegalStateException if the line cannot be found.
2498      */
moveToNextIndexMatchingRegex(String regex, String[] lines, int currentIndex)2499     private int moveToNextIndexMatchingRegex(String regex, String[] lines, int currentIndex) {
2500         while (currentIndex < lines.length && !lines[currentIndex].matches(regex)) {
2501             currentIndex++;
2502         }
2503 
2504         if (currentIndex >= lines.length) {
2505             throw new IllegalStateException(
2506                     "The output of 'dumpsys device_policy' was not as expected. Owners have not "
2507                             + "been removed. This will leave the device in an unstable state and "
2508                             + "will lead to further test failures.");
2509         }
2510 
2511         return currentIndex;
2512     }
2513 
2514     /**
2515      * Helper for Api level checking of features in the new release before we incremented the api
2516      * number.
2517      */
checkApiLevelAgainstNextRelease(String feature, int strictMinLevel)2518     private void checkApiLevelAgainstNextRelease(String feature, int strictMinLevel)
2519             throws DeviceNotAvailableException {
2520         if (checkApiLevelAgainstNextRelease(strictMinLevel)) {
2521             return;
2522         }
2523         throw new IllegalArgumentException(
2524                 String.format(
2525                         "%s not supported on %s. Must be API %d.",
2526                         feature, getSerialNumber(), strictMinLevel));
2527     }
2528 
2529     @Override
dumpHeap(String process, String devicePath)2530     public File dumpHeap(String process, String devicePath) throws DeviceNotAvailableException {
2531         if (Strings.isNullOrEmpty(devicePath) || Strings.isNullOrEmpty(process)) {
2532             throw new IllegalArgumentException("devicePath or process cannot be null or empty.");
2533         }
2534         String pid = getProcessPid(process);
2535         if (pid == null) {
2536             return null;
2537         }
2538         File dump = dumpAndPullHeap(pid, devicePath);
2539         // Clean the device.
2540         deleteFile(devicePath);
2541         return dump;
2542     }
2543 
2544     /** Dump the heap file and pull it from the device. */
dumpAndPullHeap(String pid, String devicePath)2545     private File dumpAndPullHeap(String pid, String devicePath) throws DeviceNotAvailableException {
2546         executeShellCommand(String.format(DUMPHEAP_CMD, pid, devicePath));
2547         // Allow a little bit of time for the file to populate on device side.
2548         int attempt = 0;
2549         // TODO: add an API to check device file size
2550         while (!doesFileExist(devicePath) && attempt < 3) {
2551             getRunUtil().sleep(DUMPHEAP_TIME);
2552             attempt++;
2553         }
2554         File dumpFile = pullFile(devicePath);
2555         return dumpFile;
2556     }
2557 
2558     /** {@inheritDoc} */
2559     @Override
listDisplayIds()2560     public Set<Long> listDisplayIds() throws DeviceNotAvailableException {
2561         Set<Long> displays = new HashSet<>();
2562         CommandResult res = executeShellV2Command("dumpsys SurfaceFlinger | grep 'color modes:'");
2563         if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
2564             CLog.e("Something went wrong while listing displays: %s", res.getStderr());
2565             return displays;
2566         }
2567         String output = res.getStdout();
2568         Pattern p = Pattern.compile(DISPLAY_ID_PATTERN);
2569         for (String line : output.split("\n")) {
2570             Matcher m = p.matcher(line);
2571             if (m.matches()) {
2572                 displays.add(Long.parseLong(m.group("id")));
2573             }
2574         }
2575 
2576         // If the device is older and did not report any displays
2577         // then add the default.
2578         // Note: this assumption breaks down if the device also has multiple displays
2579         if (displays.isEmpty()) {
2580             // Zero is the default display
2581             displays.add(0L);
2582         }
2583 
2584         return displays;
2585     }
2586 
2587     @Override
listDisplayIdsForStartingVisibleBackgroundUsers()2588     public Set<Integer> listDisplayIdsForStartingVisibleBackgroundUsers()
2589             throws DeviceNotAvailableException {
2590         checkApiLevelAgainstNextRelease("getDisplayIdsForStartingVisibleBackgroundUsers", 34);
2591 
2592         String cmd = "cmd activity list-displays-for-starting-users";
2593         CommandResult res = executeShellV2Command(cmd);
2594         if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
2595             throw new DeviceRuntimeException(
2596                     "Command  '" + cmd + "' failed: " + res,
2597                     DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
2598         }
2599         String output = res.getStdout().trim();
2600 
2601         if (output.equalsIgnoreCase("none")) {
2602             return Collections.emptySet();
2603         }
2604 
2605         // TODO: reuse some helper to parse the list
2606         if (!output.startsWith("[") || !output.endsWith("]")) {
2607             throw new DeviceRuntimeException(
2608                     "Invalid output for command '" + cmd + "': " + output,
2609                     DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
2610         }
2611         String contents = output.substring(1, output.length() - 1);
2612         try {
2613             String[] ids = contents.split(",");
2614             return Arrays.asList(ids).stream()
2615                     .map(id -> Integer.parseInt(id.trim()))
2616                     .collect(Collectors.toSet());
2617         } catch (Exception e) {
2618             throw new DeviceRuntimeException(
2619                     "Invalid output for command '" + cmd + "': " + output,
2620                     DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
2621         }
2622     }
2623 
2624     @Override
getFoldableStates()2625     public Set<DeviceFoldableState> getFoldableStates() throws DeviceNotAvailableException {
2626         if (getIDevice() instanceof StubDevice) {
2627             return new HashSet<>();
2628         }
2629         try (CloseableTraceScope foldable = new CloseableTraceScope("getFoldableStates")) {
2630             CommandResult result = executeShellV2Command("cmd device_state print-states");
2631             if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
2632                 // Can't throw an exception since it would fail on non-supported version
2633                 return new HashSet<>();
2634             }
2635             Set<DeviceFoldableState> foldableStates = new LinkedHashSet<>();
2636             Pattern deviceStatePattern =
2637                     Pattern.compile(
2638                             "DeviceState\\{identifier=(\\d+), name='(\\S+)'"
2639                                     + "(?:, app_accessible=)?(\\S+)?"
2640                                     + "(?:, cancel_when_requester_not_on_top=)?(\\S+)?"
2641                                     + "\\}\\S*");
2642             for (String line : result.getStdout().split("\n")) {
2643                 Matcher m = deviceStatePattern.matcher(line.trim());
2644                 if (m.matches()) {
2645                     // Move onto the next state if the device state is not accessible by apps
2646                     if (m.groupCount() > 2
2647                             && m.group(3) != null
2648                             && !Boolean.parseBoolean(m.group(3))) {
2649                         continue;
2650                     }
2651                     // Move onto the next state if the device state is canceled when the requesting
2652                     // app
2653                     // is not on top.
2654                     if (m.groupCount() > 3
2655                             && m.group(4) != null
2656                             && Boolean.parseBoolean(m.group(4))) {
2657                         continue;
2658                     }
2659                     foldableStates.add(
2660                             new DeviceFoldableState(Integer.parseInt(m.group(1)), m.group(2)));
2661                 }
2662             }
2663             return foldableStates;
2664         }
2665     }
2666 
2667     @Override
notifySnapuserd(SnapuserdWaitPhase waitPhase)2668     public void notifySnapuserd(SnapuserdWaitPhase waitPhase) {
2669         mWaitForSnapuserd = true;
2670         mSnapuserNotificationTimestamp = System.currentTimeMillis();
2671         mWaitPhase = waitPhase;
2672         CLog.d("Notified to wait for snapuserd at %s", waitPhase);
2673     }
2674 
2675     @Override
waitForSnapuserd(SnapuserdWaitPhase currentPhase)2676     public void waitForSnapuserd(SnapuserdWaitPhase currentPhase)
2677             throws DeviceNotAvailableException {
2678         if (!mWaitForSnapuserd) {
2679             CLog.d("No snapuserd notification in progress for %s", currentPhase);
2680             return;
2681         }
2682         // At releasing or at the reported phase, block for snapuserd.
2683         if (!SnapuserdWaitPhase.BLOCK_BEFORE_RELEASING.equals(currentPhase)
2684                 && !currentPhase.equals(mWaitPhase)) {
2685             return;
2686         }
2687         long startTime = System.currentTimeMillis();
2688         try (CloseableTraceScope ignored = new CloseableTraceScope("wait_for_snapuserd")) {
2689             long maxTimeout = getOptions().getSnapuserdTimeout();
2690             while (System.currentTimeMillis() - startTime < maxTimeout) {
2691                 CommandResult psOutput = executeShellV2Command("ps -ef | grep snapuserd");
2692                 CLog.d("stdout: %s, stderr: %s", psOutput.getStdout(), psOutput.getStderr());
2693                 if (psOutput.getStdout().contains("snapuserd -")) {
2694                     RunUtil.getDefault().sleep(2500);
2695                     CLog.d("waiting for snapuserd to complete.");
2696                 } else {
2697                     return;
2698                 }
2699             }
2700             throw new DeviceRuntimeException(
2701                     String.format(
2702                             "snapuserd didn't complete in %s",
2703                             TimeUtil.formatElapsedTime(maxTimeout)),
2704                     InfraErrorIdentifier.INCREMENTAL_FLASHING_ERROR);
2705         } finally {
2706             InvocationMetricLogger.addInvocationMetrics(
2707                     InvocationMetricKey.INCREMENTAL_SNAPUSERD_WRITE_BLOCKING_TIME,
2708                     System.currentTimeMillis() - startTime);
2709             InvocationMetricLogger.addInvocationMetrics(
2710                     InvocationMetricKey.INCREMENTAL_SNAPUSERD_WRITE_TIME,
2711                     System.currentTimeMillis() - mSnapuserNotificationTimestamp);
2712             mWaitForSnapuserd = false;
2713             mSnapuserNotificationTimestamp = 0L;
2714             mWaitPhase = null;
2715         }
2716     }
2717 
2718     @Override
getCurrentFoldableState()2719     public DeviceFoldableState getCurrentFoldableState() throws DeviceNotAvailableException {
2720         if (getIDevice() instanceof StubDevice) {
2721             return null;
2722         }
2723         CommandResult result = executeShellV2Command("cmd device_state state");
2724         Pattern deviceStatePattern =
2725                 Pattern.compile(
2726                         "Committed state: DeviceState\\{identifier=(\\d+), name='(\\S+)'"
2727                                 + "(?:, app_accessible=)?(\\S+)?"
2728                                 + "(?:, cancel_when_requester_not_on_top=)?(\\S+)?"
2729                                 + "\\}\\S*");
2730         for (String line : result.getStdout().split("\n")) {
2731             Matcher m = deviceStatePattern.matcher(line.trim());
2732             if (m.matches()) {
2733                 return new DeviceFoldableState(Integer.parseInt(m.group(1)), m.group(2));
2734             }
2735         }
2736         return null;
2737     }
2738 
2739     /**
2740      * Checks the preconditions to run a microdroid.
2741      *
2742      * @param protectedVm true if microdroid is intended to run on protected VM.
2743      * @return returns true if the preconditions are satisfied, false otherwise.
2744      */
supportsMicrodroid(boolean protectedVm)2745     public boolean supportsMicrodroid(boolean protectedVm) throws Exception {
2746         CommandResult result = executeShellV2Command("getprop ro.product.cpu.abi");
2747         if (result.getStatus() != CommandStatus.SUCCESS) {
2748             return false;
2749         }
2750         String abi = result.getStdout().trim();
2751 
2752         if (abi.isEmpty() || (!abi.startsWith("arm64") && !abi.startsWith("x86_64"))) {
2753             CLog.d("Unsupported ABI: " + abi);
2754             return false;
2755         }
2756 
2757         if (protectedVm) {
2758             // check if device supports protected virtual machines.
2759             boolean pVMSupported =
2760                     getBooleanProperty("ro.boot.hypervisor.protected_vm.supported", false);
2761             if (!pVMSupported) {
2762                 CLog.i("Device does not support protected virtual machines.");
2763                 return false;
2764             }
2765         } else {
2766             // check if device supports non protected virtual machines.
2767             boolean nonProtectedVMSupported =
2768                     getBooleanProperty("ro.boot.hypervisor.vm.supported", false);
2769             if (!nonProtectedVMSupported) {
2770                 CLog.i("Device does not support non protected virtual machines.");
2771                 return false;
2772             }
2773         }
2774 
2775         if (!doesFileExist("/apex/com.android.virt")) {
2776             CLog.i(
2777                     "com.android.virt APEX was not pre-installed. Command Failed: 'ls"
2778                             + " /apex/com.android.virt/bin/crosvm'");
2779             return false;
2780         }
2781         return true;
2782     }
2783 
2784     /**
2785      * Checks the preconditions to run a microdroid.
2786      *
2787      * @return returns true if the preconditions are satisfied, false otherwise.
2788      */
supportsMicrodroid()2789     public boolean supportsMicrodroid() throws Exception {
2790         // Micrdroid can run on protected and non-protected VMs
2791         return supportsMicrodroid(false) || supportsMicrodroid(true);
2792     }
2793 
2794     /**
2795      * Forwards contents of a file to log. To be used when testing microdroid, to forward console
2796      * and log outputs to the host device's log.
2797      */
forwardFileToLog(String logPath, String tag)2798     private void forwardFileToLog(String logPath, String tag) {
2799         try (CloseableTraceScope ignored = new CloseableTraceScope("forward_to_log:" + tag)) {
2800             String logwrapperCmd =
2801                     "logwrapper "
2802                             + "sh "
2803                             + "-c "
2804                             + "\"$'tail -f -n +0 "
2805                             + logPath
2806                             + " | sed \\'s/^/"
2807                             + tag
2808                             + ": /g\\''\""; // add tags in front of lines
2809             getRunUtil().allowInterrupt(true);
2810             // Manually execute the adb action to avoid any kind of recovery
2811             // since it hard to interrupt the forwarding
2812             final String[] fullCmd = buildAdbShellCommand(logwrapperCmd, false);
2813             AdbShellAction adbActionV2 =
2814                     new AdbShellAction(
2815                             fullCmd,
2816                             null,
2817                             null,
2818                             null,
2819                             TimeUnit.MINUTES.toMillis(MICRODROID_MAX_LIFETIME_MINUTES));
2820             adbActionV2.run();
2821         } catch (Exception e) {
2822             // Consume
2823         }
2824     }
2825 
isVirtFeatureEnabled(String feature)2826     private boolean isVirtFeatureEnabled(String feature) throws DeviceNotAvailableException {
2827         CommandResult result =
2828                 executeShellV2Command(VIRT_APEX + "bin/vm check-feature-enabled " + feature);
2829         return result.getExitCode() == 0 && result.getStdout().contains("is enabled");
2830     }
2831 
2832     /**
2833      * Starts a Microdroid TestDevice.
2834      *
2835      * @param builder A {@link MicrodroidBuilder} with required properties to start a microdroid.
2836      * @return returns a ITestDevice for the microdroid, can return null.
2837      */
startMicrodroid(MicrodroidBuilder builder)2838     private ITestDevice startMicrodroid(MicrodroidBuilder builder)
2839             throws DeviceNotAvailableException {
2840         IDeviceManager deviceManager = GlobalConfiguration.getDeviceManagerInstance();
2841 
2842         if (!mStartedMicrodroids.isEmpty())
2843             throw new IllegalStateException(
2844                     String.format(
2845                             "Microdroid with cid '%s' already exists in device. Cannot create"
2846                                     + " another one.",
2847                             mStartedMicrodroids.values().iterator().next().cid));
2848 
2849         // remove any leftover files under test root
2850         executeShellV2Command("rm -rf " + TEST_ROOT + "*");
2851 
2852         CommandResult result = executeShellV2Command("mkdir -p " + TEST_ROOT);
2853         if (result.getStatus() != CommandStatus.SUCCESS) {
2854             throw new DeviceRuntimeException(
2855                     "mkdir -p " + TEST_ROOT + " has failed: " + result,
2856                     DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
2857         }
2858 
2859         for (File localFile : builder.mBootFiles.keySet()) {
2860             String remoteFileName = builder.mBootFiles.get(localFile);
2861             pushFile(localFile, TEST_ROOT + remoteFileName);
2862         }
2863 
2864         // Push the apk file to the test directory
2865         if (builder.mApkFile != null) {
2866             pushFile(builder.mApkFile, TEST_ROOT + builder.mApkFile.getName());
2867             builder.mApkPath = TEST_ROOT + builder.mApkFile.getName();
2868         } else if (builder.mApkPath == null) {
2869             // if both apkFile and apkPath is null, we can not start a microdroid device
2870             throw new IllegalArgumentException(
2871                     "apkFile and apkPath is both null. Can not start microdroid.");
2872         }
2873 
2874         // This file is not what we provide. It will be created by the vm tool.
2875         final String outApkIdsigPath =
2876                 TEST_ROOT
2877                         + (builder.mApkFile != null ? builder.mApkFile.getName() : "NULL")
2878                         + ".idsig";
2879         final String consolePath = TEST_ROOT + "console.txt";
2880         final String logPath = TEST_ROOT + "log.txt";
2881         final String debugFlag =
2882                 Strings.isNullOrEmpty(builder.mDebugLevel) ? "" : "--debug " + builder.mDebugLevel;
2883         final String cpuFlag = builder.mNumCpus == null ? "" : "--cpus " + builder.mNumCpus;
2884         final String cpuAffinityFlag =
2885                 Strings.isNullOrEmpty(builder.mCpuAffinity)
2886                         ? ""
2887                         : "--cpu-affinity " + builder.mCpuAffinity;
2888         final String cpuTopologyFlag =
2889                 Strings.isNullOrEmpty(builder.mCpuTopology)
2890                         ? ""
2891                         : "--cpu-topology " + builder.mCpuTopology;
2892         final String gkiFlag = Strings.isNullOrEmpty(builder.mGki) ? "" : "--gki " + builder.mGki;
2893         final String hugePagesFlag = builder.mHugePages ? "--hugepages" : "";
2894 
2895         List<String> args =
2896                 new ArrayList<>(
2897                         Arrays.asList(
2898                                 deviceManager.getAdbPath(),
2899                                 "-s",
2900                                 getSerialNumber(),
2901                                 "shell",
2902                                 VIRT_APEX + "bin/vm",
2903                                 "run-app",
2904                                 "--console " + consolePath,
2905                                 "--log " + logPath,
2906                                 "--mem " + builder.mMemoryMib,
2907                                 debugFlag,
2908                                 cpuFlag,
2909                                 cpuAffinityFlag,
2910                                 cpuTopologyFlag,
2911                                 gkiFlag,
2912                                 hugePagesFlag,
2913                                 builder.mApkPath,
2914                                 outApkIdsigPath,
2915                                 builder.mInstanceImg,
2916                                 "--config-path",
2917                                 builder.mConfigPath));
2918         if (isVirtFeatureEnabled("com.android.kvm.LLPVM_CHANGES")) {
2919             args.add("--instance-id-file");
2920             args.add(builder.mInstanceIdFile);
2921         }
2922         if (builder.mProtectedVm) {
2923             args.add("--protected");
2924         }
2925         for (String path : builder.mExtraIdsigPaths) {
2926             args.add("--extra-idsig");
2927             args.add(path);
2928         }
2929         for (String path : builder.mAssignedDevices) {
2930             args.add("--devices");
2931             args.add(path);
2932         }
2933 
2934         // Run the VM
2935         String cid = null;
2936         Process process = null;
2937         try {
2938             PipedInputStream pipe = new PipedInputStream();
2939             process = getRunUtil().runCmdInBackground(args, new PipedOutputStream(pipe));
2940             cid = getCidFromVmRunOutput(new InputStreamReader(pipe));
2941         } catch (IOException ex) {
2942             throw new DeviceRuntimeException(
2943                     "Exception trying to start a VM",
2944                     ex,
2945                     DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
2946         } finally {
2947             if (cid == null) {
2948                 // Don't leak the process on failure
2949                 process.destroyForcibly();
2950             }
2951         }
2952 
2953         // Redirect log.txt to logd using logwrapper
2954         ExecutorService executor = Executors.newFixedThreadPool(2);
2955         executor.execute(
2956                 () -> {
2957                     forwardFileToLog(consolePath, "MicrodroidConsole");
2958                 });
2959         executor.execute(
2960                 () -> {
2961                     forwardFileToLog(logPath, "MicrodroidLog");
2962                 });
2963 
2964         int vmAdbPort = forwardMicrodroidAdbPort(cid);
2965         String microdroidSerial = "localhost:" + vmAdbPort;
2966 
2967         DeviceSelectionOptions microSelection = new DeviceSelectionOptions();
2968         microSelection.setSerial(microdroidSerial);
2969         microSelection.setBaseDeviceTypeRequested(BaseDeviceType.NATIVE_DEVICE);
2970 
2971         NativeDevice microdroid = (NativeDevice) deviceManager.allocateDevice(microSelection);
2972         if (microdroid == null) {
2973             process.destroy();
2974             try {
2975                 process.waitFor();
2976                 executor.shutdownNow();
2977                 executor.awaitTermination(2L, TimeUnit.MINUTES);
2978             } catch (InterruptedException ex) {
2979             }
2980             throw new DeviceRuntimeException(
2981                     "Unable to force allocate the microdroid device",
2982                     InfraErrorIdentifier.RUNNER_ALLOCATION_ERROR);
2983         }
2984         // microdroid can be slow to become unavailable after root. (b/259208275)
2985         microdroid.getOptions().setAdbRootUnavailableTimeout(4 * 1000);
2986         builder.mTestDeviceOptions.put("enable-device-connection", "true");
2987         builder.mTestDeviceOptions.put(
2988                 TestDeviceOptions.INSTANCE_TYPE_OPTION, getOptions().getInstanceType().toString());
2989         microdroid.setTestDeviceOptions(builder.mTestDeviceOptions);
2990         ((IManagedTestDevice) microdroid).setIDevice(new RemoteAvdIDevice(microdroidSerial));
2991         adbConnectToMicrodroid(cid, microdroidSerial, vmAdbPort, builder.mAdbConnectTimeoutMs);
2992         microdroid.setMicrodroidProcess(process);
2993         try {
2994             // TODO: Pass the build info
2995             microdroid.initializeConnection(null, null);
2996         } catch (DeviceNotAvailableException | TargetSetupError e) {
2997             CLog.e(e);
2998         }
2999         MicrodroidTracker tracker = new MicrodroidTracker();
3000         tracker.executor = executor;
3001         tracker.cid = cid;
3002         mStartedMicrodroids.put(process, tracker);
3003         return microdroid;
3004     }
3005 
getCidFromVmRunOutput(Reader outputReader)3006     private static String getCidFromVmRunOutput(Reader outputReader) {
3007         BufferedReader stdout = new BufferedReader(outputReader);
3008 
3009         StringBuilder output = new StringBuilder();
3010 
3011         // Retrieve the CID from the vm tool output
3012         String cid = null;
3013         Pattern pattern = Pattern.compile("with CID (\\d+)");
3014         String line;
3015         try {
3016             while ((line = stdout.readLine()) != null) {
3017                 output.append(line);
3018                 output.append(' ');
3019 
3020                 Matcher matcher = pattern.matcher(line);
3021                 if (matcher.find()) {
3022                     cid = matcher.group(1);
3023                     break;
3024                 }
3025             }
3026         } catch (IOException ex) {
3027             throw new DeviceRuntimeException(
3028                     "Failed to find the CID of the VM: " + output,
3029                     ex,
3030                     DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
3031         }
3032         if (cid == null) {
3033             throw new DeviceRuntimeException(
3034                     "Failed to find the CID of the VM: " + output,
3035                     DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
3036         }
3037         return cid;
3038     }
3039 
3040     /** Find an unused port and forward microdroid's adb connection. Returns the port number. */
forwardMicrodroidAdbPort(String cid)3041     private int forwardMicrodroidAdbPort(String cid) {
3042         IDeviceManager deviceManager = GlobalConfiguration.getDeviceManagerInstance();
3043         for (int trial = 0; trial < 10; trial++) {
3044             int vmAdbPort;
3045             try (ServerSocket serverSocket = new ServerSocket(0)) {
3046                 vmAdbPort = serverSocket.getLocalPort();
3047             } catch (IOException e) {
3048                 throw new DeviceRuntimeException(
3049                         "Unable to get an unused port for Microdroid.",
3050                         e,
3051                         DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
3052             }
3053             String from = "tcp:" + vmAdbPort;
3054             String to = "vsock:" + cid + ":5555";
3055 
3056             CommandResult result =
3057                     getRunUtil()
3058                             .runTimedCmd(
3059                                     10000,
3060                                     deviceManager.getAdbPath(),
3061                                     "-s",
3062                                     getSerialNumber(),
3063                                     "forward",
3064                                     from,
3065                                     to);
3066             if (result.getStatus() == CommandStatus.SUCCESS) {
3067                 return vmAdbPort;
3068             }
3069 
3070             if (result.getStderr().contains("Address already in use")) {
3071                 // retry with other ports
3072                 continue;
3073             } else {
3074                 throw new DeviceRuntimeException(
3075                         "Unable to forward vsock:" + cid + ":5555: " + result.getStderr(),
3076                         DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
3077             }
3078         }
3079         throw new DeviceRuntimeException(
3080                 "Unable to get an unused port for Microdroid.",
3081                 DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
3082     }
3083 
3084     /**
3085      * Establish an adb connection to microdroid by letting Android forward the connection to
3086      * microdroid. Wait until the connection is established and microdroid is booted.
3087      */
adbConnectToMicrodroid( String cid, String microdroidSerial, int vmAdbPort, long adbConnectTimeoutMs)3088     private void adbConnectToMicrodroid(
3089             String cid, String microdroidSerial, int vmAdbPort, long adbConnectTimeoutMs) {
3090         MicrodroidHelper microdroidHelper = new MicrodroidHelper();
3091         IDeviceManager deviceManager = GlobalConfiguration.getDeviceManagerInstance();
3092 
3093         long start = System.currentTimeMillis();
3094         long timeoutMillis = adbConnectTimeoutMs;
3095         long elapsed = 0;
3096 
3097         final String serial = getSerialNumber();
3098         final String from = "tcp:" + vmAdbPort;
3099         final String to = "vsock:" + cid + ":5555";
3100         getRunUtil()
3101                 .runTimedCmd(10000, deviceManager.getAdbPath(), "-s", serial, "forward", from, to);
3102 
3103         boolean disconnected = true;
3104         while (disconnected && timeoutMillis >= 0) {
3105             elapsed = System.currentTimeMillis() - start;
3106             timeoutMillis -= elapsed;
3107             start = System.currentTimeMillis();
3108             CommandResult result =
3109                     getRunUtil()
3110                             .runTimedCmd(
3111                                     timeoutMillis,
3112                                     deviceManager.getAdbPath(),
3113                                     "connect",
3114                                     microdroidSerial);
3115             if (result.getStatus() != CommandStatus.SUCCESS) {
3116                 throw new DeviceRuntimeException(
3117                         deviceManager.getAdbPath()
3118                                 + " connect "
3119                                 + microdroidSerial
3120                                 + " has failed: "
3121                                 + result,
3122                         DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
3123             }
3124             disconnected =
3125                     result.getStdout().trim().equals("failed to connect to " + microdroidSerial);
3126             if (disconnected) {
3127                 // adb demands us to disconnect if the prior connection was a failure.
3128                 // b/194375443: this somtimes fails, thus 'try*'.
3129                 getRunUtil()
3130                         .runTimedCmd(
3131                                 10000, deviceManager.getAdbPath(), "disconnect", microdroidSerial);
3132             }
3133         }
3134 
3135         elapsed = System.currentTimeMillis() - start;
3136         timeoutMillis -= elapsed;
3137         if (timeoutMillis > 0) {
3138             getRunUtil()
3139                     .runTimedCmd(
3140                             timeoutMillis,
3141                             deviceManager.getAdbPath(),
3142                             "-s",
3143                             microdroidSerial,
3144                             "wait-for-device");
3145         }
3146         boolean dataAvailable = false;
3147         while (!dataAvailable && timeoutMillis >= 0) {
3148             elapsed = System.currentTimeMillis() - start;
3149             timeoutMillis -= elapsed;
3150             start = System.currentTimeMillis();
3151             final String checkCmd = "if [ -d /data/local/tmp ]; then echo 1; fi";
3152             dataAvailable =
3153                     microdroidHelper.runOnMicrodroid(microdroidSerial, checkCmd).equals("1");
3154         }
3155         // Check if it actually booted by reading a sysprop.
3156         if (!microdroidHelper
3157                 .runOnMicrodroid(microdroidSerial, "getprop", "ro.hardware")
3158                 .equals("microdroid")) {
3159             throw new DeviceRuntimeException(
3160                     String.format("Device '%s' was not booted.", microdroidSerial),
3161                     DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
3162         }
3163     }
3164 
3165     /**
3166      * Shuts down the microdroid device, if one exist.
3167      *
3168      * @throws DeviceNotAvailableException
3169      */
shutdownMicrodroid(@onnull ITestDevice microdroidDevice)3170     public void shutdownMicrodroid(@Nonnull ITestDevice microdroidDevice)
3171             throws DeviceNotAvailableException {
3172         Process process = ((NativeDevice) microdroidDevice).getMicrodroidProcess();
3173         if (process == null) {
3174             throw new IllegalArgumentException("Process is null. TestDevice is not a Microdroid. ");
3175         }
3176         if (!mStartedMicrodroids.containsKey(process)) {
3177             throw new IllegalArgumentException(
3178                     "Microdroid device was not started in this TestDevice.");
3179         }
3180 
3181         process.destroy();
3182         try {
3183             process.waitFor();
3184         } catch (InterruptedException ex) {
3185         }
3186 
3187         // disconnect from microdroid
3188         getRunUtil()
3189                 .runTimedCmd(
3190                         10000,
3191                         GlobalConfiguration.getDeviceManagerInstance().getAdbPath(),
3192                         "disconnect",
3193                         microdroidDevice.getSerialNumber());
3194 
3195         GlobalConfiguration.getDeviceManagerInstance()
3196                 .freeDevice(microdroidDevice, FreeDeviceState.AVAILABLE);
3197         MicrodroidTracker tracker = mStartedMicrodroids.remove(process);
3198         getRunUtil().allowInterrupt(true);
3199         try {
3200             tracker.executor.shutdownNow();
3201             tracker.executor.awaitTermination(1L, TimeUnit.MINUTES);
3202         } catch (InterruptedException e) {
3203             CLog.e(e);
3204         }
3205     }
3206 
3207     // TODO (b/274941025): remove when shell commands using this method are merged in AOSP
executeShellV2CommandThatReturnsBooleanSafe( String cmdFormat, Object... cmdArgs)3208     private boolean executeShellV2CommandThatReturnsBooleanSafe(
3209             String cmdFormat, Object... cmdArgs) {
3210         try {
3211             return executeShellV2CommandThatReturnsBoolean(cmdFormat, cmdArgs);
3212         } catch (Exception e) {
3213             CLog.e(e);
3214             return false;
3215         }
3216     }
3217 
executeShellV2CommandThatReturnsBoolean(String cmdFormat, Object... cmdArgs)3218     private boolean executeShellV2CommandThatReturnsBoolean(String cmdFormat, Object... cmdArgs)
3219             throws DeviceNotAvailableException {
3220         String cmd = String.format(cmdFormat, cmdArgs);
3221         CommandResult res = executeShellV2Command(cmd);
3222         if (!CommandStatus.SUCCESS.equals(res.getStatus())) {
3223             throw new DeviceRuntimeException(
3224                     "Command  '" + cmd + "' failed: " + res,
3225                     DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
3226         }
3227         String output = res.getStdout();
3228         switch (output.trim().toLowerCase()) {
3229             case "true":
3230                 return true;
3231             case "false":
3232                 return false;
3233             default:
3234                 throw new DeviceRuntimeException(
3235                         "Non-boolean result for '" + cmd + "': " + output,
3236                         DeviceErrorIdentifier.DEVICE_UNEXPECTED_RESPONSE);
3237         }
3238     }
3239 
3240     /** A builder used to create a Microdroid TestDevice. */
3241     public static class MicrodroidBuilder {
3242         private File mApkFile;
3243         private String mApkPath;
3244         private String mConfigPath;
3245         private String mDebugLevel;
3246         private int mMemoryMib;
3247         private Integer mNumCpus;
3248         private String mCpuAffinity;
3249         private String mCpuTopology;
3250         private List<String> mExtraIdsigPaths;
3251         private boolean mProtectedVm;
3252         private Map<String, String> mTestDeviceOptions;
3253         private Map<File, String> mBootFiles;
3254         private long mAdbConnectTimeoutMs;
3255         private List<String> mAssignedDevices;
3256         private String mGki;
3257         private String mInstanceIdFile; // Path to instance_id file
3258         private String mInstanceImg; // Path to instance_img file
3259         private boolean mHugePages;
3260 
3261         /** Creates a builder for the given APK/apkPath and the payload config file in APK. */
MicrodroidBuilder(File apkFile, String apkPath, @Nonnull String configPath)3262         private MicrodroidBuilder(File apkFile, String apkPath, @Nonnull String configPath) {
3263             mApkFile = apkFile;
3264             mApkPath = apkPath;
3265             mConfigPath = configPath;
3266             mDebugLevel = null;
3267             mMemoryMib = 0;
3268             mNumCpus = null;
3269             mCpuAffinity = null;
3270             mExtraIdsigPaths = new ArrayList<>();
3271             mProtectedVm = false; // Vm is unprotected by default.
3272             mTestDeviceOptions = new LinkedHashMap<>();
3273             mBootFiles = new LinkedHashMap<>();
3274             mAdbConnectTimeoutMs = MICRODROID_DEFAULT_ADB_CONNECT_TIMEOUT_MINUTES * 60 * 1000;
3275             mAssignedDevices = new ArrayList<>();
3276             mInstanceIdFile = null;
3277             mInstanceImg = null;
3278         }
3279 
3280         /** Creates a Microdroid builder for the given APK and the payload config file in APK. */
fromFile( @onnull File apkFile, @Nonnull String configPath)3281         public static MicrodroidBuilder fromFile(
3282                 @Nonnull File apkFile, @Nonnull String configPath) {
3283             return new MicrodroidBuilder(apkFile, null, configPath);
3284         }
3285 
3286         /**
3287          * Creates a Microdroid builder for the given apkPath and the payload config file in APK.
3288          */
fromDevicePath( @onnull String apkPath, @Nonnull String configPath)3289         public static MicrodroidBuilder fromDevicePath(
3290                 @Nonnull String apkPath, @Nonnull String configPath) {
3291             return new MicrodroidBuilder(null, apkPath, configPath);
3292         }
3293 
3294         /**
3295          * Sets the debug level.
3296          *
3297          * <p>Supported values: "none" and "full". Android T also supports "app_only".
3298          */
debugLevel(String debugLevel)3299         public MicrodroidBuilder debugLevel(String debugLevel) {
3300             mDebugLevel = debugLevel;
3301             return this;
3302         }
3303 
3304         /**
3305          * Sets the amount of RAM to give the VM. If this is zero or negative then the default will
3306          * be used.
3307          */
memoryMib(int memoryMib)3308         public MicrodroidBuilder memoryMib(int memoryMib) {
3309             mMemoryMib = memoryMib;
3310             return this;
3311         }
3312 
3313         /**
3314          * Sets the number of vCPUs in the VM. Defaults to 1.
3315          *
3316          * <p>Only supported in Android T.
3317          */
numCpus(int num)3318         public MicrodroidBuilder numCpus(int num) {
3319             mNumCpus = num;
3320             return this;
3321         }
3322 
3323         /**
3324          * Sets on which host CPUs the vCPUs can run. The format is a comma-separated list of CPUs
3325          * or CPU ranges to run vCPUs on. e.g. "0,1-3,5" to choose host CPUs 0, 1, 2, 3, and 5. Or
3326          * this can be a colon-separated list of assignments of vCPU to host CPU assignments. e.g.
3327          * "0=0:1=1:2=2" to map vCPU 0 to host CPU 0, and so on.
3328          *
3329          * <p>Only supported in Android T.
3330          */
cpuAffinity(String affinity)3331         public MicrodroidBuilder cpuAffinity(String affinity) {
3332             mCpuAffinity = affinity;
3333             return this;
3334         }
3335 
3336         /** Sets the CPU topology configuration. Supported values: "one_cpu" and "match_host". */
cpuTopology(String cpuTopology)3337         public MicrodroidBuilder cpuTopology(String cpuTopology) {
3338             mCpuTopology = cpuTopology;
3339             return this;
3340         }
3341 
3342         /** Sets whether the VM will be protected or not. */
protectedVm(boolean isProtectedVm)3343         public MicrodroidBuilder protectedVm(boolean isProtectedVm) {
3344             mProtectedVm = isProtectedVm;
3345             return this;
3346         }
3347 
3348         /** Adds extra idsig file to the list. */
addExtraIdsigPath(String extraIdsigPath)3349         public MicrodroidBuilder addExtraIdsigPath(String extraIdsigPath) {
3350             if (!Strings.isNullOrEmpty(extraIdsigPath)) {
3351                 mExtraIdsigPaths.add(extraIdsigPath);
3352             }
3353             return this;
3354         }
3355 
3356         /**
3357          * Sets a {@link TestDeviceOptions} for the microdroid TestDevice.
3358          *
3359          * @param optionName The name of the TestDeviceOption to set
3360          * @param valueText The value
3361          * @return the microdroid builder.
3362          */
addTestDeviceOption(String optionName, String valueText)3363         public MicrodroidBuilder addTestDeviceOption(String optionName, String valueText) {
3364             mTestDeviceOptions.put(optionName, valueText);
3365             return this;
3366         }
3367 
3368         /**
3369          * Adds a file for booting to be pushed to {@link #TEST_ROOT}.
3370          *
3371          * <p>Use this method if an file is required for booting microdroid. Otherwise use {@link
3372          * TestDevice#pushFile}.
3373          *
3374          * @param localFile The local file on the host
3375          * @param remoteFileName The remote file name on the device
3376          * @return the microdroid builder.
3377          */
addBootFile(File localFile, String remoteFileName)3378         public MicrodroidBuilder addBootFile(File localFile, String remoteFileName) {
3379             mBootFiles.put(localFile, remoteFileName);
3380             return this;
3381         }
3382 
3383         /**
3384          * Adds a device to assign to microdroid.
3385          *
3386          * @param sysfsNode The path to the sysfs node to assign
3387          * @return the microdroid builder.
3388          */
addAssignableDevice(String sysfsNode)3389         public MicrodroidBuilder addAssignableDevice(String sysfsNode) {
3390             mAssignedDevices.add(sysfsNode);
3391             return this;
3392         }
3393 
3394         /**
3395          * Sets the timeout for adb connect to microdroid TestDevice in millis.
3396          *
3397          * @param timeoutMs The timeout in millis
3398          */
setAdbConnectTimeoutMs(long timeoutMs)3399         public MicrodroidBuilder setAdbConnectTimeoutMs(long timeoutMs) {
3400             mAdbConnectTimeoutMs = timeoutMs;
3401             return this;
3402         }
3403 
3404         /**
3405          * Uses GKI kernel instead of microdroid kernel
3406          *
3407          * @param version The GKI version to use
3408          */
gki(String version)3409         public MicrodroidBuilder gki(String version) {
3410             mGki = version;
3411             return this;
3412         }
3413 
3414         /**
3415          * Sets the instance_id path.
3416          *
3417          * @param instanceIdPath: Path to the instanceId
3418          */
instanceIdFile(String instanceIdPath)3419         public MicrodroidBuilder instanceIdFile(String instanceIdPath) {
3420             mInstanceIdFile = instanceIdPath;
3421             return this;
3422         }
3423 
3424         /**
3425          * Sets instance.img file path.
3426          *
3427          * @param instanceIdPath: Path to the instanceId
3428          */
instanceImgFile(String instanceImgPath)3429         public MicrodroidBuilder instanceImgFile(String instanceImgPath) {
3430             mInstanceImg = instanceImgPath;
3431             return this;
3432         }
3433 
3434         /**
3435          * Sets whether to hint the kernel for transparent hugepages.
3436          *
3437          * @return the microdroid builder.
3438          */
hugePages(boolean hintHugePages)3439         public MicrodroidBuilder hugePages(boolean hintHugePages) {
3440             mHugePages = hintHugePages;
3441             return this;
3442         }
3443 
3444         /** Starts a Micrdroid TestDevice on the given TestDevice. */
build(@onnull TestDevice device)3445         public ITestDevice build(@Nonnull TestDevice device) throws DeviceNotAvailableException {
3446             if (mNumCpus != null) {
3447                 if (device.getApiLevel() != 33) {
3448                     throw new IllegalStateException(
3449                             "Setting number of CPUs only supported with API level 33");
3450                 }
3451                 if (mNumCpus < 1) {
3452                     throw new IllegalArgumentException("Number of vCPUs can not be less than 1.");
3453                 }
3454             }
3455 
3456             if (!Strings.isNullOrEmpty(mCpuTopology)) {
3457                 device.checkApiLevelAgainstNextRelease("vm-cpu-topology", 34);
3458             }
3459 
3460             if (mCpuAffinity != null) {
3461                 if (device.getApiLevel() != 33) {
3462                     throw new IllegalStateException(
3463                             "Setting CPU affinity only supported with API level 33");
3464                 }
3465                 if (!Pattern.matches("[\\d]+(-[\\d]+)?(,[\\d]+(-[\\d]+)?)*", mCpuAffinity)
3466                         && !Pattern.matches("[\\d]+=[\\d]+(:[\\d]+=[\\d]+)*", mCpuAffinity)) {
3467                     throw new IllegalArgumentException(
3468                             "CPU affinity [" + mCpuAffinity + "]" + " is invalid");
3469                 }
3470             }
3471             if (mInstanceIdFile == null) {
3472                 mInstanceIdFile = TEST_ROOT + INSTANCE_ID_FILE;
3473             }
3474             if (mInstanceImg == null) {
3475                 mInstanceImg = TEST_ROOT + INSTANCE_IMG;
3476             }
3477 
3478             return device.startMicrodroid(this);
3479         }
3480     }
3481 
handleInstallationError(InstallException e)3482     private String handleInstallationError(InstallException e) {
3483         String message = e.getMessage();
3484         if (message == null) {
3485             message =
3486                     String.format(
3487                             "InstallException during package installation. " + "cause: %s",
3488                             StreamUtil.getStackTrace(e));
3489         }
3490         return message;
3491     }
3492 
handleInstallReceiver(InstallReceiver receiver, File packageFile)3493     private String handleInstallReceiver(InstallReceiver receiver, File packageFile) {
3494         if (receiver.isSuccessfullyCompleted()) {
3495             return null;
3496         }
3497         if (receiver.getErrorMessage() == null) {
3498             return String.format("Installation of %s timed out", packageFile.getAbsolutePath());
3499         }
3500         String error = receiver.getErrorMessage();
3501         if (error.contains("cmd: Failure calling service package")
3502                 || error.contains("Can't find service: package")) {
3503             String message =
3504                     String.format(
3505                             "Failed to install '%s'. Device might have"
3506                                     + " crashed, it returned: %s",
3507                             packageFile.getName(), error);
3508             throw new DeviceRuntimeException(message, DeviceErrorIdentifier.DEVICE_CRASHED);
3509         }
3510         return error;
3511     }
3512 }
3513