blob: 2ae1ff096acab8c30074697b759c9558f454fda6 [file] [log] [blame]
Brett Chabot74121d82010-01-28 20:14:27 -08001/*
2 * Copyright (C) 2010 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Brett Chabot74121d82010-01-28 20:14:27 -080016package com.android.tradefed.device;
17
Julien Desprezc8474552016-02-17 10:59:27 +000018import com.android.ddmlib.AdbCommandRejectedException;
Brett Chabot74121d82010-01-28 20:14:27 -080019import com.android.ddmlib.IDevice;
Julien Desprezc8474552016-02-17 10:59:27 +000020import com.android.ddmlib.InstallException;
jdesprezb1469112018-02-15 09:57:25 -080021import com.android.ddmlib.InstallReceiver;
Julien Desprezc8474552016-02-17 10:59:27 +000022import com.android.ddmlib.RawImage;
23import com.android.ddmlib.ShellCommandUnresponsiveException;
24import com.android.ddmlib.SyncException;
25import com.android.ddmlib.TimeoutException;
26import com.android.tradefed.log.LogUtil.CLog;
27import com.android.tradefed.result.ByteArrayInputStreamSource;
28import com.android.tradefed.result.InputStreamSource;
Julien Desprez14e96692017-01-12 12:31:29 +000029import com.android.tradefed.util.KeyguardControllerState;
Julien Desprez4c8aabc2016-03-14 15:53:48 +000030import com.android.tradefed.util.RunUtil;
Julien Desprezc8474552016-02-17 10:59:27 +000031import com.android.tradefed.util.StreamUtil;
32
Julien Desprez1fadf1a2016-10-20 15:50:46 +010033import com.google.common.annotations.VisibleForTesting;
jdesprezd7670322017-08-30 14:12:28 -070034import com.google.common.base.Strings;
Julien Desprez1fadf1a2016-10-20 15:50:46 +010035
Julien Desprezc8474552016-02-17 10:59:27 +000036import java.awt.Image;
37import java.awt.image.BufferedImage;
38import java.io.ByteArrayOutputStream;
39import java.io.File;
40import java.io.IOException;
41import java.util.ArrayList;
42import java.util.Arrays;
Julien Desprez4c8aabc2016-03-14 15:53:48 +000043import java.util.HashMap;
Julien Desprezc8474552016-02-17 10:59:27 +000044import java.util.HashSet;
45import java.util.List;
46import java.util.Map;
47import java.util.Set;
jdesprezbc580f92017-06-02 11:41:40 -070048import java.util.concurrent.TimeUnit;
Julien Desprezc8474552016-02-17 10:59:27 +000049import java.util.regex.Matcher;
50import java.util.regex.Pattern;
51
52import javax.imageio.ImageIO;
Brett Chabotbbea34d2011-06-10 13:48:41 -070053
Brett Chabot74121d82010-01-28 20:14:27 -080054/**
Julien Desprez6961b272016-02-01 09:58:23 +000055 * Implementation of a {@link ITestDevice} for a full stack android device
Brett Chabot74121d82010-01-28 20:14:27 -080056 */
Julien Desprez2f34e382016-06-21 12:30:39 +010057public class TestDevice extends NativeDevice {
Guang Zhubd90e4d2015-09-08 17:44:12 -070058
Julien Desprezc8474552016-02-17 10:59:27 +000059 /** number of attempts made to clear dialogs */
60 private static final int NUM_CLEAR_ATTEMPTS = 5;
61 /** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */
62 static final String DISMISS_DIALOG_CMD = "input keyevent 23";
Julien Desprez0e849332017-01-18 12:22:30 +000063 /** Commands that can be used to dismiss the keyguard. */
64 static final String DISMISS_KEYGUARD_CMD = "input keyevent 82";
65
66 /**
67 * Alternative command to dismiss the keyguard by requesting the Window Manager service to do
68 * it. Api 23 and after.
69 */
70 static final String DISMISS_KEYGUARD_WM_CMD = "wm dismiss-keyguard";
71
Julien Desprezc8474552016-02-17 10:59:27 +000072 /** Timeout to wait for input dispatch to become ready **/
73 private static final long INPUT_DISPATCH_READY_TIMEOUT = 5 * 1000;
74 /** command to test input dispatch readiness **/
75 private static final String TEST_INPUT_CMD = "dumpsys input";
76
Julien Desprez4c8aabc2016-03-14 15:53:48 +000077 private static final long AM_COMMAND_TIMEOUT = 10 * 1000;
78 private static final long CHECK_NEW_USER = 1000;
79
Julien Desprezc8474552016-02-17 10:59:27 +000080 static final String LIST_PACKAGES_CMD = "pm list packages -f";
81 private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)=(.*)");
82
83 private static final int FLAG_PRIMARY = 1; // From the UserInfo class
84
Julien Desprez4c8aabc2016-03-14 15:53:48 +000085 private static final String[] SETTINGS_NAMESPACE = {"system", "secure", "global"};
86
Julien Desprez0f0a02a2017-03-01 16:59:10 +000087 /** user pattern in the output of "pm list users" = TEXT{<id>:<name>:<flags>} TEXT * */
88 private static final String USER_PATTERN = "(.*?\\{)(\\d+)(:)(.*)(:)(\\d+)(\\}.*)";
Julien Desprez4c8aabc2016-03-14 15:53:48 +000089
Julien Desprez3faaefd2016-05-24 19:25:12 +010090 private static final int API_LEVEL_GET_CURRENT_USER = 24;
jdesprezbc580f92017-06-02 11:41:40 -070091 /** Timeout to wait for a screenshot before giving up to avoid hanging forever */
92 private static final long MAX_SCREENSHOT_TIMEOUT = 5 * 60 * 1000; // 5 min
Julien Desprez3faaefd2016-05-24 19:25:12 +010093
jdesprezd7670322017-08-30 14:12:28 -070094 /** adb shell am dumpheap <service pid> <dump file path> */
95 private static final String DUMPHEAP_CMD = "am dumpheap %s %s";
96 /** Time given to a file to be dumped on device side */
97 private static final long DUMPHEAP_TIME = 5000l;
98
jdesprezb1469112018-02-15 09:57:25 -080099 /** Timeout in minutes for the package installation */
100 static final long INSTALL_TIMEOUT_MINUTES = 4;
101 /** Max timeout to output for package installation */
102 static final long INSTALL_TIMEOUT_TO_OUTPUT_MINUTES = 3;
103
Julien Desprezb1301842018-04-16 10:12:52 -0700104 private boolean mWasWifiHelperInstalled = false;
105
Julien Desprezc8474552016-02-17 10:59:27 +0000106 /**
Julien Desprez6961b272016-02-01 09:58:23 +0000107 * @param device
108 * @param stateMonitor
109 * @param allocationMonitor
Omari Stephens718c9622011-02-02 18:17:32 -0800110 */
Julien Desprez6961b272016-02-01 09:58:23 +0000111 public TestDevice(IDevice device, IDeviceStateMonitor stateMonitor,
112 IDeviceMonitor allocationMonitor) {
113 super(device, stateMonitor, allocationMonitor);
Kevin Lau Fang3435aff2016-01-29 01:47:12 +0000114 }
Julien Desprezc8474552016-02-17 10:59:27 +0000115
116 /**
117 * Core implementation of package installation, with retries around
118 * {@link IDevice#installPackage(String, boolean, String...)}
119 * @param packageFile
120 * @param reinstall
121 * @param extraArgs
122 * @return the response from the installation
123 * @throws DeviceNotAvailableException
124 */
125 private String internalInstallPackage(
126 final File packageFile, final boolean reinstall, final List<String> extraArgs)
127 throws DeviceNotAvailableException {
128 // use array to store response, so it can be returned to caller
129 final String[] response = new String[1];
jdesprezb1469112018-02-15 09:57:25 -0800130 DeviceAction installAction =
131 new DeviceAction() {
132 @Override
133 public boolean run() throws InstallException {
134 try {
135 InstallReceiver receiver = createInstallReceiver();
136 getIDevice()
137 .installPackage(
138 packageFile.getAbsolutePath(),
139 reinstall,
140 receiver,
141 INSTALL_TIMEOUT_MINUTES,
142 INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
143 TimeUnit.MINUTES,
144 extraArgs.toArray(new String[] {}));
145 if (receiver.isSuccessfullyCompleted()) {
146 response[0] = null;
147 } else if (receiver.getErrorMessage() == null) {
148 response[0] =
149 String.format(
150 "Installation of %s timed out",
151 packageFile.getAbsolutePath());
152 } else {
153 response[0] = receiver.getErrorMessage();
154 }
155 } catch (InstallException e) {
156 response[0] = e.getMessage();
157 }
158 return response[0] == null;
159 }
160 };
Julien Desprezc8474552016-02-17 10:59:27 +0000161 performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
162 installAction, MAX_RETRY_ATTEMPTS);
163 return response[0];
164 }
165
166 /**
jdesprezb1469112018-02-15 09:57:25 -0800167 * Creates and return an {@link InstallReceiver} for {@link #internalInstallPackage(File,
168 * boolean, List)} and {@link #installPackage(File, File, boolean, String...)} testing.
169 */
170 @VisibleForTesting
171 InstallReceiver createInstallReceiver() {
172 return new InstallReceiver();
173 }
174
175 /**
Julien Desprezc8474552016-02-17 10:59:27 +0000176 * {@inheritDoc}
177 */
178 @Override
179 public String installPackage(final File packageFile, final boolean reinstall,
180 final String... extraArgs) throws DeviceNotAvailableException {
181 boolean runtimePermissionSupported = isRuntimePermissionSupported();
182 List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
183 // grant all permissions by default if feature is supported
184 if (runtimePermissionSupported) {
185 args.add("-g");
186 }
187 return internalInstallPackage(packageFile, reinstall, args);
188 }
189
190 /**
191 * {@inheritDoc}
192 */
193 @Override
194 public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions,
195 String... extraArgs) throws DeviceNotAvailableException {
196 ensureRuntimePermissionSupported();
197 List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
198 if (grantPermissions) {
199 args.add("-g");
200 }
201 return internalInstallPackage(packageFile, reinstall, args);
202 }
203
204 /**
205 * {@inheritDoc}
206 */
207 @Override
208 public String installPackageForUser(File packageFile, boolean reinstall, int userId,
209 String... extraArgs) throws DeviceNotAvailableException {
210 boolean runtimePermissionSupported = isRuntimePermissionSupported();
211 List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
212 // grant all permissions by default if feature is supported
213 if (runtimePermissionSupported) {
214 args.add("-g");
215 }
216 args.add("--user");
217 args.add(Integer.toString(userId));
218 return internalInstallPackage(packageFile, reinstall, args);
219 }
220
221 /**
222 * {@inheritDoc}
223 */
224 @Override
225 public String installPackageForUser(File packageFile, boolean reinstall,
226 boolean grantPermissions, int userId, String... extraArgs)
227 throws DeviceNotAvailableException {
228 ensureRuntimePermissionSupported();
229 List<String> args = new ArrayList<>(Arrays.asList(extraArgs));
230 if (grantPermissions) {
231 args.add("-g");
232 }
233 args.add("--user");
234 args.add(Integer.toString(userId));
235 return internalInstallPackage(packageFile, reinstall, args);
236 }
237
238 public String installPackage(final File packageFile, final File certFile,
239 final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException {
240 // use array to store response, so it can be returned to caller
241 final String[] response = new String[1];
jdesprezb1469112018-02-15 09:57:25 -0800242 DeviceAction installAction =
243 new DeviceAction() {
244 @Override
245 public boolean run()
246 throws InstallException, SyncException, IOException, TimeoutException,
247 AdbCommandRejectedException {
248 // TODO: create a getIDevice().installPackage(File, File...) method when the
249 // dist cert functionality is ready to be open sourced
250 String remotePackagePath =
251 getIDevice().syncPackageToDevice(packageFile.getAbsolutePath());
252 String remoteCertPath =
253 getIDevice().syncPackageToDevice(certFile.getAbsolutePath());
254 // trick installRemotePackage into issuing a 'pm install <apk> <cert>'
255 // command, by adding apk path to extraArgs, and using cert as the
256 // 'apk file'.
257 String[] newExtraArgs = new String[extraArgs.length + 1];
258 System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length);
259 newExtraArgs[newExtraArgs.length - 1] =
260 String.format("\"%s\"", remotePackagePath);
261 try {
262 InstallReceiver receiver = createInstallReceiver();
263 getIDevice()
264 .installRemotePackage(
265 remoteCertPath,
266 reinstall,
267 receiver,
268 INSTALL_TIMEOUT_MINUTES,
269 INSTALL_TIMEOUT_TO_OUTPUT_MINUTES,
270 TimeUnit.MINUTES,
271 newExtraArgs);
272 if (receiver.isSuccessfullyCompleted()) {
273 response[0] = null;
274 } else if (receiver.getErrorMessage() == null) {
275 response[0] =
276 String.format(
277 "Installation of %s timed out.",
278 packageFile.getAbsolutePath());
279 } else {
280 response[0] = receiver.getErrorMessage();
281 }
282 } catch (InstallException e) {
283 response[0] = e.getMessage();
284 } finally {
285 getIDevice().removeRemotePackage(remotePackagePath);
286 getIDevice().removeRemotePackage(remoteCertPath);
287 }
288 return true;
289 }
290 };
Julien Desprezc8474552016-02-17 10:59:27 +0000291 performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()),
292 installAction, MAX_RETRY_ATTEMPTS);
293 return response[0];
294 }
295
296 /**
297 * {@inheritDoc}
298 */
299 @Override
300 public String uninstallPackage(final String packageName) throws DeviceNotAvailableException {
301 // use array to store response, so it can be returned to caller
302 final String[] response = new String[1];
303 DeviceAction uninstallAction = new DeviceAction() {
304 @Override
305 public boolean run() throws InstallException {
306 CLog.d("Uninstalling %s", packageName);
307 String result = getIDevice().uninstallPackage(packageName);
308 response[0] = result;
309 return result == null;
310 }
311 };
312 performDeviceAction(String.format("uninstall %s", packageName), uninstallAction,
313 MAX_RETRY_ATTEMPTS);
314 return response[0];
315 }
316
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000317 /** {@inheritDoc} */
Julien Desprezc8474552016-02-17 10:59:27 +0000318 @Override
319 public InputStreamSource getScreenshot() throws DeviceNotAvailableException {
320 return getScreenshot("PNG");
321 }
322
323 /**
324 * {@inheritDoc}
325 */
326 @Override
327 public InputStreamSource getScreenshot(String format) throws DeviceNotAvailableException {
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000328 return getScreenshot(format, true);
329 }
330
331 /** {@inheritDoc} */
332 @Override
333 public InputStreamSource getScreenshot(String format, boolean rescale)
334 throws DeviceNotAvailableException {
Julien Desprezc8474552016-02-17 10:59:27 +0000335 if (!format.equalsIgnoreCase("PNG") && !format.equalsIgnoreCase("JPEG")){
336 CLog.e("Screenshot: Format %s is not supported, defaulting to PNG.", format);
337 format = "PNG";
338 }
339 ScreenshotAction action = new ScreenshotAction();
340 if (performDeviceAction("screenshot", action, MAX_RETRY_ATTEMPTS)) {
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000341 byte[] imageData =
342 compressRawImage(action.mRawScreenshot, format.toUpperCase(), rescale);
Julien Desprezc8474552016-02-17 10:59:27 +0000343 if (imageData != null) {
344 return new ByteArrayInputStreamSource(imageData);
345 }
346 }
jdesprez8694ce92017-06-01 16:56:14 -0700347 // Return an error in the buffer
348 return new ByteArrayInputStreamSource(
349 "Error: device reported null for screenshot.".getBytes());
Julien Desprezc8474552016-02-17 10:59:27 +0000350 }
351
352 private class ScreenshotAction implements DeviceAction {
353
354 RawImage mRawScreenshot;
355
356 /**
357 * {@inheritDoc}
358 */
359 @Override
360 public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
361 ShellCommandUnresponsiveException, InstallException, SyncException {
jdesprezbc580f92017-06-02 11:41:40 -0700362 mRawScreenshot =
363 getIDevice().getScreenshot(MAX_SCREENSHOT_TIMEOUT, TimeUnit.MILLISECONDS);
Julien Desprezc8474552016-02-17 10:59:27 +0000364 return mRawScreenshot != null;
365 }
366 }
367
Julien Desprez1fadf1a2016-10-20 15:50:46 +0100368 /**
369 * Helper to compress a rawImage obtained from the screen.
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000370 *
371 * @param rawImage {@link RawImage} to compress.
372 * @param format resulting format of compressed image. PNG and JPEG are supported.
373 * @param rescale if rescaling should be done to further reduce size of compressed image.
374 * @return compressed image.
Julien Desprez1fadf1a2016-10-20 15:50:46 +0100375 */
376 @VisibleForTesting
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000377 byte[] compressRawImage(RawImage rawImage, String format, boolean rescale) {
378 BufferedImage image = rawImageToBufferedImage(rawImage, format);
379
380 // Rescale to reduce size if needed
381 // Screenshot default format is 1080 x 1920, 8-bit/color RGBA
382 // By cutting in half we can easily keep good quality and smaller size
383 if (rescale) {
384 image = rescaleImage(image);
385 }
386
387 return getImageData(image, format);
388 }
389
390 /**
391 * Converts {@link RawImage} to {@link BufferedImage} in specified format.
392 *
393 * @param rawImage {@link RawImage} to convert.
394 * @param format resulting format of image. PNG and JPEG are supported.
395 * @return converted image.
396 */
397 @VisibleForTesting
398 BufferedImage rawImageToBufferedImage(RawImage rawImage, String format) {
Julien Desprezc8474552016-02-17 10:59:27 +0000399 BufferedImage image = null;
400
401 if ("JPEG".equalsIgnoreCase(format)) {
402 //JPEG does not support ARGB without a special encoder
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000403 image =
404 new BufferedImage(
405 rawImage.width, rawImage.height, BufferedImage.TYPE_3BYTE_BGR);
Julien Desprezc8474552016-02-17 10:59:27 +0000406 }
407 else {
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000408 image = new BufferedImage(rawImage.width, rawImage.height, BufferedImage.TYPE_INT_ARGB);
Julien Desprezc8474552016-02-17 10:59:27 +0000409 }
410
411 // borrowed conversion logic from platform/sdk/screenshot/.../Screenshot.java
412 int index = 0;
413 int IndexInc = rawImage.bpp >> 3;
414 for (int y = 0 ; y < rawImage.height ; y++) {
415 for (int x = 0 ; x < rawImage.width ; x++) {
416 int value = rawImage.getARGB(index);
417 index += IndexInc;
418 image.setRGB(x, y, value);
419 }
420 }
421
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000422 return image;
423 }
424
425 /**
426 * Rescales image cutting it in half.
427 *
428 * @param image source {@link BufferedImage}.
429 * @return resulting scaled image.
430 */
431 @VisibleForTesting
432 BufferedImage rescaleImage(BufferedImage image) {
Julien Desprezc8474552016-02-17 10:59:27 +0000433 int shortEdge = Math.min(image.getHeight(), image.getWidth());
434 if (shortEdge > 720) {
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000435 Image resized =
436 image.getScaledInstance(
437 image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_SMOOTH);
438 image =
439 new BufferedImage(
440 image.getWidth() / 2, image.getHeight() / 2, Image.SCALE_REPLICATE);
Julien Desprezc8474552016-02-17 10:59:27 +0000441 image.getGraphics().drawImage(resized, 0, 0, null);
442 }
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000443 return image;
444 }
Julien Desprezc8474552016-02-17 10:59:27 +0000445
Insaf Latypov73f7aeb2017-03-03 11:05:08 +0000446 /**
447 * Gets byte array representation of {@link BufferedImage}.
448 *
449 * @param image source {@link BufferedImage}.
450 * @param format resulting format of image. PNG and JPEG are supported.
451 * @return byte array representation of the image.
452 */
453 @VisibleForTesting
454 byte[] getImageData(BufferedImage image, String format) {
Julien Desprezc8474552016-02-17 10:59:27 +0000455 // store compressed image in memory, and let callers write to persistent storage
456 // use initial buffer size of 128K
457 byte[] imageData = null;
458 ByteArrayOutputStream imageOut = new ByteArrayOutputStream(128*1024);
459 try {
460 if (ImageIO.write(image, format, imageOut)) {
461 imageData = imageOut.toByteArray();
462 } else {
463 CLog.e("Failed to compress screenshot to png");
464 }
465 } catch (IOException e) {
466 CLog.e("Failed to compress screenshot to png");
467 CLog.e(e);
468 }
469 StreamUtil.close(imageOut);
470 return imageData;
471 }
472
473 /**
474 * {@inheritDoc}
475 */
476 @Override
477 public boolean clearErrorDialogs() throws DeviceNotAvailableException {
478 // attempt to clear error dialogs multiple times
479 for (int i = 0; i < NUM_CLEAR_ATTEMPTS; i++) {
480 int numErrorDialogs = getErrorDialogCount();
481 if (numErrorDialogs == 0) {
482 return true;
483 }
484 doClearDialogs(numErrorDialogs);
485 }
486 if (getErrorDialogCount() > 0) {
487 // at this point, all attempts to clear error dialogs completely have failed
488 // it might be the case that the process keeps showing new dialogs immediately after
489 // clearing. There's really no workaround, but to dump an error
490 CLog.e("error dialogs still exist on %s.", getSerialNumber());
491 return false;
492 }
493 return true;
494 }
495
496 /**
497 * Detects the number of crash or ANR dialogs currently displayed.
498 * <p/>
499 * Parses output of 'dump activity processes'
500 *
501 * @return count of dialogs displayed
502 * @throws DeviceNotAvailableException
503 */
504 private int getErrorDialogCount() throws DeviceNotAvailableException {
505 int errorDialogCount = 0;
506 Pattern crashPattern = Pattern.compile(".*crashing=true.*AppErrorDialog.*");
507 Pattern anrPattern = Pattern.compile(".*notResponding=true.*AppNotRespondingDialog.*");
508 String systemStatusOutput = executeShellCommand("dumpsys activity processes");
509 Matcher crashMatcher = crashPattern.matcher(systemStatusOutput);
510 while (crashMatcher.find()) {
511 errorDialogCount++;
512 }
513 Matcher anrMatcher = anrPattern.matcher(systemStatusOutput);
514 while (anrMatcher.find()) {
515 errorDialogCount++;
516 }
517
518 return errorDialogCount;
519 }
520
521 private void doClearDialogs(int numDialogs) throws DeviceNotAvailableException {
522 CLog.i("Attempted to clear %d dialogs on %s", numDialogs, getSerialNumber());
523 for (int i=0; i < numDialogs; i++) {
524 // send DPAD_CENTER
525 executeShellCommand(DISMISS_DIALOG_CMD);
526 }
527 }
528
529 /**
Guang Zhu03c985e2017-04-28 14:53:55 -0700530 * {@inheritDoc}
Julien Desprezc8474552016-02-17 10:59:27 +0000531 */
Guang Zhu03c985e2017-04-28 14:53:55 -0700532 @Override
533 public void disableKeyguard() throws DeviceNotAvailableException {
Julien Desprezc8474552016-02-17 10:59:27 +0000534 long start = System.currentTimeMillis();
535 while (true) {
536 Boolean ready = isDeviceInputReady();
537 if (ready == null) {
538 // unsupported API level, bail
539 break;
540 }
541 if (ready) {
542 // input dispatch is ready, bail
543 break;
544 }
545 long timeSpent = System.currentTimeMillis() - start;
546 if (timeSpent > INPUT_DISPATCH_READY_TIMEOUT) {
547 CLog.w("Timeout after waiting %dms on enabling of input dispatch", timeSpent);
548 // break & proceed anyway
549 break;
550 } else {
551 getRunUtil().sleep(1000);
552 }
553 }
Julien Desprez0e849332017-01-18 12:22:30 +0000554 if (getApiLevel() >= 23) {
555 CLog.i(
556 "Attempting to disable keyguard on %s using %s",
557 getSerialNumber(), DISMISS_KEYGUARD_WM_CMD);
558 String output = executeShellCommand(DISMISS_KEYGUARD_WM_CMD);
559 CLog.i("output of %s: %s", DISMISS_KEYGUARD_WM_CMD, output);
560 } else {
Tsu Chiang Chuangbfee7362017-01-25 14:22:54 -0800561 CLog.i("Command: %s, is not supported, falling back to %s", DISMISS_KEYGUARD_WM_CMD,
562 DISMISS_KEYGUARD_CMD);
Julien Desprez0e849332017-01-18 12:22:30 +0000563 executeShellCommand(DISMISS_KEYGUARD_CMD);
564 }
565 // TODO: check that keyguard was actually dismissed.
Julien Desprezc8474552016-02-17 10:59:27 +0000566 }
567
Julien Desprez14e96692017-01-12 12:31:29 +0000568 /** {@inheritDoc} */
569 @Override
570 public KeyguardControllerState getKeyguardState() throws DeviceNotAvailableException {
571 String output =
572 executeShellCommand("dumpsys activity activities | grep -A3 KeyguardController:");
Julien Desprez8a7bff32017-01-24 13:53:48 +0000573 CLog.d("Output from KeyguardController: %s", output);
Julien Desprez510a4832017-01-27 11:58:17 +0000574 KeyguardControllerState state =
575 KeyguardControllerState.create(Arrays.asList(output.trim().split("\n")));
576 return state;
Julien Desprez14e96692017-01-12 12:31:29 +0000577 }
578
Julien Desprezc8474552016-02-17 10:59:27 +0000579 /**
580 * Tests the device to see if input dispatcher is ready
Julien Desprez14e96692017-01-12 12:31:29 +0000581 *
Julien Desprezc8474552016-02-17 10:59:27 +0000582 * @return <code>null</code> if not supported by platform, or the actual readiness state
583 * @throws DeviceNotAvailableException
584 */
585 Boolean isDeviceInputReady() throws DeviceNotAvailableException {
586 CollectingOutputReceiver receiver = new CollectingOutputReceiver();
587 executeShellCommand(TEST_INPUT_CMD, receiver);
588 String output = receiver.getOutput();
589 Matcher m = INPUT_DISPATCH_STATE_REGEX.matcher(output);
590 if (!m.find()) {
591 // output does not contain the line at all, implying unsupported API level, bail
592 return null;
593 }
594 return "1".equals(m.group(1));
595 }
596
597 /**
598 * {@inheritDoc}
599 */
600 @Override
601 protected void prePostBootSetup() throws DeviceNotAvailableException {
602 if (mOptions.isDisableKeyguard()) {
603 disableKeyguard();
604 }
605 }
606
607 /**
608 * Performs an reboot via framework power manager
609 *
610 * Must have root access, device must be API Level 18 or above
611 *
612 * @param into the mode to reboot into, currently supported: bootloader, recovery, leave it
613 * null for a plain reboot
614 * @return <code>true</code> if the device rebooted, <code>false</code> if not successful or
615 * unsupported
616 * @throws DeviceNotAvailableException
617 */
618 private boolean doAdbFrameworkReboot(final String into) throws DeviceNotAvailableException {
619 // use framework reboot when:
620 // 1. device API level >= 18
621 // 2. has adb root
622 // 3. framework is running
623 if (!isEnableAdbRoot()) {
624 CLog.i("framework reboot is not supported; when enable root is disabled");
625 return false;
626 }
627 enableAdbRoot();
628 if (getApiLevel() >= 18 && isAdbRoot()) {
629 try {
630 // check framework running
631 String output = executeShellCommand("pm path android");
632 if (output == null || !output.contains("package:")) {
633 CLog.v("framework reboot: can't detect framework running");
634 return false;
635 }
636 String command = "svc power reboot";
637 if (into != null && !into.isEmpty()) {
638 command = String.format("%s %s", command, into);
639 }
640 executeShellCommand(command);
641 } catch (DeviceUnresponsiveException due) {
642 CLog.v("framework reboot: device unresponsive to shell command, using fallback");
643 return false;
644 }
Julien Desprezba60f9d2018-08-20 20:59:10 +0000645 boolean notAvailable = waitForDeviceNotAvailable(30 * 1000);
646 if (notAvailable) {
Julien Desprez78344aa2018-09-04 16:06:05 -0700647 postAdbReboot();
Julien Desprezba60f9d2018-08-20 20:59:10 +0000648 }
649 return notAvailable;
Julien Desprezc8474552016-02-17 10:59:27 +0000650 } else {
651 CLog.v("framework reboot: not supported");
652 return false;
653 }
654 }
655
656 /**
657 * Perform a adb reboot.
658 *
659 * @param into the bootloader name to reboot into, or <code>null</code> to just reboot the
660 * device.
661 * @throws DeviceNotAvailableException
662 */
663 @Override
664 protected void doAdbReboot(final String into) throws DeviceNotAvailableException {
Julien Desprezc8474552016-02-17 10:59:27 +0000665 if (!doAdbFrameworkReboot(into)) {
Julien Desprez78344aa2018-09-04 16:06:05 -0700666 super.doAdbReboot(into);
Julien Desprezc8474552016-02-17 10:59:27 +0000667 }
668 }
669
670 /**
671 * {@inheritDoc}
672 */
673 @Override
674 public Set<String> getInstalledPackageNames() throws DeviceNotAvailableException {
675 return getInstalledPackageNames(new PkgFilter() {
676 @Override
677 public boolean accept(String pkgName, String apkPath) {
678 return true;
679 }
680 });
681 }
682
683 /**
Julien Desprez2f34e382016-06-21 12:30:39 +0100684 * A {@link com.android.tradefed.device.NativeDevice.DeviceAction}
Julien Desprezc8474552016-02-17 10:59:27 +0000685 * for retrieving package system service info, and do retries on
686 * failures.
687 */
688 private class DumpPkgAction implements DeviceAction {
689
690 Map<String, PackageInfo> mPkgInfoMap;
691
692 DumpPkgAction() {
693 }
694
695 @Override
696 public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException,
697 ShellCommandUnresponsiveException, InstallException, SyncException {
698 DumpsysPackageReceiver receiver = new DumpsysPackageReceiver();
699 getIDevice().executeShellCommand("dumpsys package p", receiver);
700 mPkgInfoMap = receiver.getPackages();
701 if (mPkgInfoMap.size() == 0) {
702 // Package parsing can fail if package manager is currently down. throw exception
703 // to retry
704 CLog.w("no packages found from dumpsys package p.");
705 throw new IOException();
706 }
707 return true;
708 }
709 }
710
711 /**
712 * {@inheritDoc}
713 */
714 @Override
715 public Set<String> getUninstallablePackageNames() throws DeviceNotAvailableException {
716 DumpPkgAction action = new DumpPkgAction();
717 performDeviceAction("dumpsys package p", action, MAX_RETRY_ATTEMPTS);
718
719 Set<String> pkgs = new HashSet<String>();
720 for (PackageInfo pkgInfo : action.mPkgInfoMap.values()) {
721 if (!pkgInfo.isSystemApp() || pkgInfo.isUpdatedSystemApp()) {
722 CLog.d("Found uninstallable package %s", pkgInfo.getPackageName());
723 pkgs.add(pkgInfo.getPackageName());
724 }
725 }
726 return pkgs;
727 }
728
729 /**
730 * {@inheritDoc}
731 */
732 @Override
733 public PackageInfo getAppPackageInfo(String packageName) throws DeviceNotAvailableException {
734 DumpPkgAction action = new DumpPkgAction();
735 performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS);
736 return action.mPkgInfoMap.get(packageName);
737 }
738
739 private static interface PkgFilter {
740 boolean accept(String pkgName, String apkPath);
741 }
742
743 // TODO: convert this to use DumpPkgAction
744 private Set<String> getInstalledPackageNames(PkgFilter filter)
745 throws DeviceNotAvailableException {
746 Set<String> packages= new HashSet<String>();
747 String output = executeShellCommand(LIST_PACKAGES_CMD);
748 if (output != null) {
749 Matcher m = PACKAGE_REGEX.matcher(output);
750 while (m.find()) {
751 String packagePath = m.group(1);
752 String packageName = m.group(2);
753 if (filter.accept(packageName, packagePath)) {
754 packages.add(packageName);
755 }
756 }
757 }
758 return packages;
759 }
760
761 /**
762 * {@inheritDoc}
763 */
764 @Override
765 public ArrayList<Integer> listUsers() throws DeviceNotAvailableException {
766 ArrayList<String[]> users = tokenizeListUsers();
Julien Desprezc8474552016-02-17 10:59:27 +0000767 ArrayList<Integer> userIds = new ArrayList<Integer>(users.size());
768 for (String[] user : users) {
769 userIds.add(Integer.parseInt(user[1]));
770 }
771 return userIds;
772 }
773
774 /**
775 * Tokenizes the output of 'pm list users'.
776 * The returned tokens for each user have the form: {"\tUserInfo", Integer.toString(id), name,
777 * Integer.toHexString(flag), "[running]"}; (the last one being optional)
778 * @return a list of arrays of strings, each element of the list representing the tokens
779 * for a user, or {@code null} if there was an error while tokenizing the adb command output.
780 */
781 private ArrayList<String[]> tokenizeListUsers() throws DeviceNotAvailableException {
782 String command = "pm list users";
783 String commandOutput = executeShellCommand(command);
784 // Extract the id of all existing users.
785 String[] lines = commandOutput.split("\\r?\\n");
Julien Desprezc8474552016-02-17 10:59:27 +0000786 if (!lines[0].equals("Users:")) {
jdesprez6b295b12017-11-02 16:00:48 -0700787 throw new DeviceRuntimeException(
788 String.format("'%s' in not a valid output for 'pm list users'", commandOutput));
Julien Desprezc8474552016-02-17 10:59:27 +0000789 }
790 ArrayList<String[]> users = new ArrayList<String[]>(lines.length - 1);
791 for (int i = 1; i < lines.length; i++) {
792 // Individual user is printed out like this:
793 // \tUserInfo{$id$:$name$:$Integer.toHexString(flags)$} [running]
794 String[] tokens = lines[i].split("\\{|\\}|:");
795 if (tokens.length != 4 && tokens.length != 5) {
jdesprez6b295b12017-11-02 16:00:48 -0700796 throw new DeviceRuntimeException(
797 String.format(
798 "device output: '%s' \nline: '%s' was not in the expected "
799 + "format for user info.",
800 commandOutput, lines[i]));
Julien Desprezc8474552016-02-17 10:59:27 +0000801 }
802 users.add(tokens);
803 }
804 return users;
805 }
806
807 /**
808 * {@inheritDoc}
809 */
810 @Override
811 public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException {
812 String command = "pm get-max-users";
813 String commandOutput = executeShellCommand(command);
814 try {
815 return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
816 } catch (NumberFormatException e) {
817 CLog.e("Failed to parse result: %s", commandOutput);
818 }
819 return 0;
820 }
821
Alex Chau93617352018-01-16 15:22:25 +0000822 /** {@inheritDoc} */
823 @Override
824 public int getMaxNumberOfRunningUsersSupported() throws DeviceNotAvailableException {
825 checkApiLevelAgainstNextRelease("get-max-running-users", 28);
826 String command = "pm get-max-running-users";
827 String commandOutput = executeShellCommand(command);
828 try {
829 return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim());
830 } catch (NumberFormatException e) {
831 CLog.e("Failed to parse result: %s", commandOutput);
832 }
833 return 0;
834 }
835
Julien Desprezc8474552016-02-17 10:59:27 +0000836 /**
837 * {@inheritDoc}
838 */
839 @Override
840 public boolean isMultiUserSupported() throws DeviceNotAvailableException {
841 return getMaxNumberOfUsersSupported() > 1;
842 }
843
844 /**
845 * {@inheritDoc}
846 */
847 @Override
848 public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException {
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000849 return createUser(name, false, false);
850 }
851
852 /**
853 * {@inheritDoc}
854 */
855 @Override
856 public int createUser(String name, boolean guest, boolean ephemeral)
857 throws DeviceNotAvailableException, IllegalStateException {
858 String command ="pm create-user " + (guest ? "--guest " : "")
859 + (ephemeral ? "--ephemeral " : "") + name;
860 final String output = executeShellCommand(command);
Julien Desprezc8474552016-02-17 10:59:27 +0000861 if (output.startsWith("Success")) {
862 try {
863 return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim());
864 } catch (NumberFormatException e) {
865 CLog.e("Failed to parse result: %s", output);
866 }
Julien Desprezc8474552016-02-17 10:59:27 +0000867 }
jdesprez86eb7742017-08-10 09:16:41 -0700868 throw new IllegalStateException(String.format("Failed to create user: %s", output));
Julien Desprezc8474552016-02-17 10:59:27 +0000869 }
870
871 /**
872 * {@inheritDoc}
873 */
874 @Override
875 public boolean removeUser(int userId) throws DeviceNotAvailableException {
876 final String output = executeShellCommand(String.format("pm remove-user %s", userId));
877 if (output.startsWith("Error")) {
878 CLog.e("Failed to remove user: %s", output);
879 return false;
880 }
881 return true;
882 }
883
884 /**
885 * {@inheritDoc}
886 */
887 @Override
888 public boolean startUser(int userId) throws DeviceNotAvailableException {
889 final String output = executeShellCommand(String.format("am start-user %s", userId));
890 if (output.startsWith("Error")) {
891 CLog.e("Failed to start user: %s", output);
892 return false;
893 }
894 return true;
895 }
896
897 /**
898 * {@inheritDoc}
899 */
900 @Override
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000901 public boolean stopUser(int userId) throws DeviceNotAvailableException {
Julien Desprezc8474552016-02-17 10:59:27 +0000902 // No error or status code is returned.
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000903 return stopUser(userId, false, false);
904 }
905
906 /**
907 * {@inheritDoc}
908 */
909 @Override
910 public boolean stopUser(int userId, boolean waitFlag, boolean forceFlag)
911 throws DeviceNotAvailableException {
Tony Mak2ec98fa2017-05-31 14:43:39 +0100912 final int apiLevel = getApiLevel();
913 if (waitFlag && apiLevel < 23) {
914 throw new IllegalArgumentException("stop-user -w requires API level >= 23");
915 }
916 if (forceFlag && apiLevel < 24) {
917 throw new IllegalArgumentException("stop-user -f requires API level >= 24");
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000918 }
Julien Desprezf06ab1e2016-11-18 11:52:37 +0000919 StringBuilder cmd = new StringBuilder("am stop-user ");
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000920 if (waitFlag) {
Julien Desprezf06ab1e2016-11-18 11:52:37 +0000921 cmd.append("-w ");
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000922 }
923 if (forceFlag) {
Julien Desprezf06ab1e2016-11-18 11:52:37 +0000924 cmd.append("-f ");
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000925 }
Julien Desprezf06ab1e2016-11-18 11:52:37 +0000926 cmd.append(userId);
927
928 CLog.d("stopping user with command: %s", cmd.toString());
929 final String output = executeShellCommand(cmd.toString());
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000930 if (output.contains("Error: Can't stop system user")) {
931 CLog.e("Cannot stop System user.");
932 return false;
933 }
Tony Mak2ec98fa2017-05-31 14:43:39 +0100934 if (output.contains("Can't stop current user")) {
935 CLog.e("Cannot stop current user.");
936 return false;
937 }
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000938 if (isUserRunning(userId)) {
939 CLog.w("User Id: %s is still running after the stop-user command.", userId);
940 return false;
941 }
942 return true;
Julien Desprezc8474552016-02-17 10:59:27 +0000943 }
944
945 /**
946 * {@inheritDoc}
947 */
948 @Override
949 public Integer getPrimaryUserId() throws DeviceNotAvailableException {
950 ArrayList<String[]> users = tokenizeListUsers();
Julien Desprezc8474552016-02-17 10:59:27 +0000951 for (String[] user : users) {
952 int flag = Integer.parseInt(user[3], 16);
953 if ((flag & FLAG_PRIMARY) != 0) {
954 return Integer.parseInt(user[1]);
955 }
956 }
957 return null;
958 }
959
960 /**
961 * {@inheritDoc}
962 */
963 @Override
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000964 public int getCurrentUser() throws DeviceNotAvailableException {
Julien Desprez3faaefd2016-05-24 19:25:12 +0100965 checkApiLevelAgainstNextRelease("get-current-user", API_LEVEL_GET_CURRENT_USER);
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000966 final String output = executeShellCommand("am get-current-user");
967 try {
Julien Desprez34ce18b2016-04-21 22:17:18 +0100968 int userId = Integer.parseInt(output.trim());
Julien Desprez4c8aabc2016-03-14 15:53:48 +0000969 return userId;
970 } catch (NumberFormatException e) {
971 CLog.e(e);
972 }
973 return INVALID_USER_ID;
974 }
975
976 private Matcher findUserInfo(String pmListUsersOutput) {
977 Pattern pattern = Pattern.compile(USER_PATTERN);
978 Matcher matcher = pattern.matcher(pmListUsersOutput);
979 return matcher;
980 }
981
982 /**
983 * {@inheritDoc}
984 */
985 @Override
986 public int getUserFlags(int userId) throws DeviceNotAvailableException {
987 checkApiLevelAgainst("getUserFlags", 22);
988 final String commandOutput = executeShellCommand("pm list users");
989 Matcher matcher = findUserInfo(commandOutput);
990 while(matcher.find()) {
991 if (Integer.parseInt(matcher.group(2)) == userId) {
992 return Integer.parseInt(matcher.group(6), 16);
993 }
994 }
995 CLog.w("Could not find any flags for userId: %d in output: %s", userId, commandOutput);
996 return INVALID_USER_ID;
997 }
998
999 /**
1000 * {@inheritDoc}
1001 */
1002 @Override
1003 public boolean isUserRunning(int userId) throws DeviceNotAvailableException {
1004 checkApiLevelAgainst("isUserIdRunning", 22);
1005 final String commandOutput = executeShellCommand("pm list users");
1006 Matcher matcher = findUserInfo(commandOutput);
1007 while(matcher.find()) {
1008 if (Integer.parseInt(matcher.group(2)) == userId) {
1009 if (matcher.group(7).contains("running")) {
1010 return true;
1011 }
1012 }
1013 }
1014 return false;
1015 }
1016
1017 /**
1018 * {@inheritDoc}
1019 */
1020 @Override
1021 public int getUserSerialNumber(int userId) throws DeviceNotAvailableException {
1022 checkApiLevelAgainst("getUserSerialNumber", 22);
1023 final String commandOutput = executeShellCommand("dumpsys user");
1024 // example: UserInfo{0:Test:13} serialNo=0
1025 String userSerialPatter = "(.*\\{)(\\d+)(.*\\})(.*=)(\\d+)";
1026 Pattern pattern = Pattern.compile(userSerialPatter);
1027 Matcher matcher = pattern.matcher(commandOutput);
1028 while(matcher.find()) {
1029 if (Integer.parseInt(matcher.group(2)) == userId) {
1030 return Integer.parseInt(matcher.group(5));
1031 }
1032 }
1033 CLog.w("Could not find user serial number for userId: %d, in output: %s",
1034 userId, commandOutput);
1035 return INVALID_USER_ID;
1036 }
1037
1038 /**
1039 * {@inheritDoc}
1040 */
1041 @Override
1042 public boolean switchUser(int userId) throws DeviceNotAvailableException {
1043 return switchUser(userId, AM_COMMAND_TIMEOUT);
1044 }
1045
1046 /**
1047 * {@inheritDoc}
1048 */
1049 @Override
1050 public boolean switchUser(int userId, long timeout) throws DeviceNotAvailableException {
Julien Desprez3faaefd2016-05-24 19:25:12 +01001051 checkApiLevelAgainstNextRelease("switchUser", API_LEVEL_GET_CURRENT_USER);
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001052 if (userId == getCurrentUser()) {
1053 CLog.w("Already running as user id: %s. Nothing to be done.", userId);
1054 return true;
1055 }
1056 executeShellCommand(String.format("am switch-user %d", userId));
1057 long initialTime = getHostCurrentTime();
1058 while (getHostCurrentTime() - initialTime <= timeout) {
1059 if (userId == getCurrentUser()) {
1060 // disable keyguard if option is true
1061 prePostBootSetup();
1062 return true;
1063 } else {
1064 RunUtil.getDefault().sleep(getCheckNewUserSleep());
1065 }
1066 }
1067 CLog.e("User did not switch in the given %d timeout", timeout);
1068 return false;
1069 }
1070
1071 /**
1072 * Exposed for testing.
1073 */
1074 protected long getCheckNewUserSleep() {
1075 return CHECK_NEW_USER;
1076 }
1077
1078 /**
1079 * Exposed for testing
1080 */
1081 protected long getHostCurrentTime() {
1082 return System.currentTimeMillis();
1083 }
1084
1085 /**
1086 * {@inheritDoc}
1087 */
1088 @Override
1089 public boolean hasFeature(String feature) throws DeviceNotAvailableException {
1090 final String output = executeShellCommand("pm list features");
1091 if (output.contains(feature)) {
1092 return true;
1093 }
1094 CLog.w("Feature: %s is not available on %s", feature, getSerialNumber());
1095 return false;
1096 }
1097
1098 /**
1099 * {@inheritDoc}
1100 */
1101 @Override
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001102 public String getSetting(String namespace, String key) throws DeviceNotAvailableException {
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001103 return getSettingInternal("", namespace.trim(), key.trim());
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001104 }
1105
1106 /**
1107 * {@inheritDoc}
1108 */
1109 @Override
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001110 public String getSetting(int userId, String namespace, String key)
1111 throws DeviceNotAvailableException {
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001112 return getSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim());
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001113 }
1114
1115 /**
1116 * Internal Helper to get setting with or without a userId provided.
1117 */
1118 private String getSettingInternal(String userFlag, String namespace, String key)
1119 throws DeviceNotAvailableException {
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001120 namespace = namespace.toLowerCase();
1121 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) {
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001122 String cmd = String.format("settings %s get %s %s", userFlag, namespace, key);
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001123 String output = executeShellCommand(cmd);
1124 if ("null".equals(output)) {
1125 CLog.w("settings returned null for command: %s. "
1126 + "please check if the namespace:key exists", cmd);
1127 return null;
1128 }
1129 return output.trim();
1130 }
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001131 CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace);
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001132 return null;
1133 }
1134
Yichun Lib1900022018-05-11 09:54:48 -07001135 /** {@inheritDoc} */
1136 @Override
1137 public Map<String, String> getAllSettings(String namespace) throws DeviceNotAvailableException {
1138 return getAllSettingsInternal(namespace.trim());
1139 }
1140
1141 /** Internal helper to get all settings */
1142 private Map<String, String> getAllSettingsInternal(String namespace)
1143 throws DeviceNotAvailableException {
1144 namespace = namespace.toLowerCase();
1145 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) {
1146 Map<String, String> map = new HashMap<>();
1147 String cmd = String.format("settings list %s", namespace);
1148 String output = executeShellCommand(cmd);
1149 for (String line : output.split("\\n")) {
1150 // Setting's value could be empty
1151 String[] pair = line.trim().split("=", -1);
1152 if (pair.length > 1) {
1153 map.putIfAbsent(pair[0], pair[1]);
1154 } else {
1155 CLog.e("Unable to get setting from string: %s", line);
1156 }
1157 }
1158 return map;
1159 }
1160 CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace);
1161 return null;
1162 }
1163
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001164 /**
1165 * {@inheritDoc}
1166 */
1167 @Override
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001168 public void setSetting(String namespace, String key, String value)
1169 throws DeviceNotAvailableException {
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001170 setSettingInternal("", namespace.trim(), key.trim(), value.trim());
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001171 }
1172
1173 /**
1174 * {@inheritDoc}
1175 */
1176 @Override
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001177 public void setSetting(int userId, String namespace, String key, String value)
1178 throws DeviceNotAvailableException {
Julien Desprez70d8ebd2016-06-01 16:04:21 +01001179 setSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim(),
1180 value.trim());
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001181 }
1182
1183 /**
1184 * Internal helper to set a setting with or without a userId provided.
1185 */
1186 private void setSettingInternal(String userFlag, String namespace, String key, String value)
1187 throws DeviceNotAvailableException {
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001188 checkApiLevelAgainst("Changing settings", 22);
1189 if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace.toLowerCase())) {
Julien Despreza3d9c0f2016-03-21 12:18:22 +00001190 executeShellCommand(String.format("settings %s put %s %s %s",
1191 userFlag, namespace, key, value));
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001192 } else {
1193 throw new IllegalArgumentException("Namespace must be one of system, secure, global."
1194 + " You provided: " + namespace);
1195 }
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001196 }
1197
1198 /**
1199 * {@inheritDoc}
1200 */
1201 @Override
1202 public String getAndroidId(int userId) throws DeviceNotAvailableException {
1203 if (isAdbRoot()) {
1204 String cmd = String.format(
1205 "sqlite3 /data/user/%d/com.google.android.gsf/databases/gservices.db "
1206 + "'select value from main where name = \"android_id\"'", userId);
1207 String output = executeShellCommand(cmd).trim();
1208 if (!output.contains("unable to open database")) {
1209 return output;
1210 }
1211 CLog.w("Couldn't find android-id, output: %s", output);
1212 } else {
1213 CLog.w("adb root is required.");
1214 }
1215 return null;
1216 }
1217
1218 /**
1219 * {@inheritDoc}
1220 */
1221 @Override
1222 public Map<Integer, String> getAndroidIds() throws DeviceNotAvailableException {
1223 ArrayList<Integer> userIds = listUsers();
1224 if (userIds == null) {
1225 return null;
1226 }
1227 Map<Integer, String> androidIds = new HashMap<Integer, String>();
1228 for (Integer id : userIds) {
1229 String androidId = getAndroidId(id);
1230 androidIds.put(id, androidId);
1231 }
1232 return androidIds;
1233 }
1234
1235 /**
1236 * {@inheritDoc}
1237 */
1238 @Override
Julien Desprezc8474552016-02-17 10:59:27 +00001239 IWifiHelper createWifiHelper() throws DeviceNotAvailableException {
Julien Desprezb1301842018-04-16 10:12:52 -07001240 mWasWifiHelperInstalled = true;
Avinankumar Vellore Suriyakumarc321ecd2017-08-08 16:44:40 -07001241 return new WifiHelper(this, mOptions.getWifiUtilAPKPath());
Julien Desprezc8474552016-02-17 10:59:27 +00001242 }
Julien Desprez4c8aabc2016-03-14 15:53:48 +00001243
Julien Desprezb1301842018-04-16 10:12:52 -07001244 /**
1245 * Alternative to {@link #createWifiHelper()} where we can choose whether to do the wifi helper
1246 * setup or not.
1247 */
1248 @VisibleForTesting
1249 IWifiHelper createWifiHelper(boolean doSetup) throws DeviceNotAvailableException {
1250 if (doSetup) {
1251 mWasWifiHelperInstalled = true;
1252 }
1253 return new WifiHelper(this, mOptions.getWifiUtilAPKPath(), doSetup);
1254 }
1255
1256 /** {@inheritDoc} */
1257 @Override
1258 public void postInvocationTearDown() {
1259 super.postInvocationTearDown();
1260 // If wifi was installed and it's a real device, attempt to clean it.
1261 if (mWasWifiHelperInstalled) {
1262 mWasWifiHelperInstalled = false;
1263 if (getIDevice() instanceof StubDevice) {
1264 return;
1265 }
1266 if (!TestDeviceState.ONLINE.equals(getDeviceState())) {
1267 return;
1268 }
1269 try {
1270 // Uninstall the wifi utility if it was installed.
1271 IWifiHelper wifi = createWifiHelper(false);
1272 wifi.cleanUp();
1273 } catch (DeviceNotAvailableException e) {
1274 CLog.e("Device became unavailable while uninstalling wifi util.");
1275 CLog.e(e);
1276 }
1277 }
1278 }
1279
Tony Mak14e4dee2017-04-12 14:37:34 +01001280 /** {@inheritDoc} */
1281 @Override
1282 public boolean setDeviceOwner(String componentName, int userId)
1283 throws DeviceNotAvailableException {
1284 final String command = "dpm set-device-owner --user " + userId + " '" + componentName + "'";
1285 final String commandOutput = executeShellCommand(command);
1286 return commandOutput.startsWith("Success:");
1287 }
1288
1289 /** {@inheritDoc} */
1290 @Override
1291 public boolean removeAdmin(String componentName, int userId)
1292 throws DeviceNotAvailableException {
1293 final String command =
1294 "dpm remove-active-admin --user " + userId + " '" + componentName + "'";
1295 final String commandOutput = executeShellCommand(command);
1296 return commandOutput.startsWith("Success:");
1297 }
1298
1299 /** {@inheritDoc} */
1300 @Override
1301 public void removeOwners() throws DeviceNotAvailableException {
1302 String command = "dumpsys device_policy";
1303 String commandOutput = executeShellCommand(command);
1304 String[] lines = commandOutput.split("\\r?\\n");
1305 for (int i = 0; i < lines.length; ++i) {
1306 String line = lines[i].trim();
1307 if (line.contains("Profile Owner")) {
1308 // Line is "Profile owner (User <id>):
1309 String[] tokens = line.split("\\(|\\)| ");
1310 int userId = Integer.parseInt(tokens[4]);
1311 i++;
1312 line = lines[i].trim();
1313 // Line is admin=ComponentInfo{<component>}
1314 tokens = line.split("\\{|\\}");
1315 String componentName = tokens[1];
1316 CLog.d("Cleaning up profile owner " + userId + " " + componentName);
1317 removeAdmin(componentName, userId);
1318 } else if (line.contains("Device Owner:")) {
1319 i++;
1320 line = lines[i].trim();
1321 // Line is admin=ComponentInfo{<component>}
1322 String[] tokens = line.split("\\{|\\}");
1323 String componentName = tokens[1];
1324 // Skip to user id line.
1325 i += 3;
1326 line = lines[i].trim();
1327 // Line is User ID: <N>
1328 tokens = line.split(":");
1329 int userId = Integer.parseInt(tokens[1].trim());
1330 CLog.d("Cleaning up device owner " + userId + " " + componentName);
1331 removeAdmin(componentName, userId);
1332 }
1333 }
1334 }
1335
Julien Desprez3faaefd2016-05-24 19:25:12 +01001336 /**
1337 * Helper for Api level checking of features in the new release before we incremented the api
1338 * number.
1339 */
1340 private void checkApiLevelAgainstNextRelease(String feature, int strictMinLevel)
1341 throws DeviceNotAvailableException {
1342 String codeName = getProperty(BUILD_CODENAME_PROP).trim();
Julien Desprez71489f32017-03-03 12:46:34 +00001343 int apiLevel = getApiLevel() + ("REL".equals(codeName) ? 0 : 1);
Julien Desprez3faaefd2016-05-24 19:25:12 +01001344 if (apiLevel < strictMinLevel){
1345 throw new IllegalArgumentException(String.format("%s not supported on %s. "
1346 + "Must be API %d.", feature, getSerialNumber(), strictMinLevel));
1347 }
1348 }
jdesprezd7670322017-08-30 14:12:28 -07001349
1350 @Override
1351 public File dumpHeap(String process, String devicePath) throws DeviceNotAvailableException {
1352 if (Strings.isNullOrEmpty(devicePath) || Strings.isNullOrEmpty(process)) {
1353 throw new IllegalArgumentException("devicePath or process cannot be null or empty.");
1354 }
1355 String pid = getProcessPid(process);
1356 if (pid == null) {
1357 return null;
1358 }
1359 File dump = dumpAndPullHeap(pid, devicePath);
1360 // Clean the device.
1361 executeShellCommand(String.format("rm %s", devicePath));
1362 return dump;
1363 }
1364
1365 /** Dump the heap file and pull it from the device. */
1366 private File dumpAndPullHeap(String pid, String devicePath) throws DeviceNotAvailableException {
1367 executeShellCommand(String.format(DUMPHEAP_CMD, pid, devicePath));
1368 // Allow a little bit of time for the file to populate on device side.
1369 int attempt = 0;
1370 // TODO: add an API to check device file size
1371 while (!doesFileExist(devicePath) && attempt < 3) {
1372 getRunUtil().sleep(DUMPHEAP_TIME);
1373 attempt++;
1374 }
1375 File dumpFile = pullFile(devicePath);
1376 return dumpFile;
1377 }
Brett Chabot74121d82010-01-28 20:14:27 -08001378}