| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.tradefed.device; |
| |
| import com.android.ddmlib.AdbCommandRejectedException; |
| import com.android.ddmlib.IDevice; |
| import com.android.ddmlib.InstallException; |
| import com.android.ddmlib.RawImage; |
| import com.android.ddmlib.ShellCommandUnresponsiveException; |
| import com.android.ddmlib.SyncException; |
| import com.android.ddmlib.TimeoutException; |
| import com.android.tradefed.log.LogUtil.CLog; |
| import com.android.tradefed.result.ByteArrayInputStreamSource; |
| import com.android.tradefed.result.InputStreamSource; |
| import com.android.tradefed.util.RunUtil; |
| import com.android.tradefed.util.StreamUtil; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import java.awt.Image; |
| import java.awt.image.BufferedImage; |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import javax.imageio.ImageIO; |
| |
| /** |
| * Implementation of a {@link ITestDevice} for a full stack android device |
| */ |
| public class TestDevice extends NativeDevice { |
| |
| /** number of attempts made to clear dialogs */ |
| private static final int NUM_CLEAR_ATTEMPTS = 5; |
| /** the command used to dismiss a error dialog. Currently sends a DPAD_CENTER key event */ |
| static final String DISMISS_DIALOG_CMD = "input keyevent 23"; |
| /** Timeout to wait for input dispatch to become ready **/ |
| private static final long INPUT_DISPATCH_READY_TIMEOUT = 5 * 1000; |
| /** command to test input dispatch readiness **/ |
| private static final String TEST_INPUT_CMD = "dumpsys input"; |
| |
| private static final long AM_COMMAND_TIMEOUT = 10 * 1000; |
| private static final long CHECK_NEW_USER = 1000; |
| |
| static final String LIST_PACKAGES_CMD = "pm list packages -f"; |
| private static final Pattern PACKAGE_REGEX = Pattern.compile("package:(.*)=(.*)"); |
| |
| private static final int FLAG_PRIMARY = 1; // From the UserInfo class |
| |
| private static final String[] SETTINGS_NAMESPACE = {"system", "secure", "global"}; |
| |
| /** user pattern in the output of "pm list users" = TEXT{<id>:<name>:<flags>} TEXT **/ |
| private static String USER_PATTERN = "(.*?\\{)(\\d+)(:)(.*)(:)(\\d+)(\\}.*)"; |
| |
| private static final int API_LEVEL_GET_CURRENT_USER = 24; |
| |
| /** |
| * @param device |
| * @param stateMonitor |
| * @param allocationMonitor |
| */ |
| public TestDevice(IDevice device, IDeviceStateMonitor stateMonitor, |
| IDeviceMonitor allocationMonitor) { |
| super(device, stateMonitor, allocationMonitor); |
| } |
| |
| /** |
| * Core implementation of package installation, with retries around |
| * {@link IDevice#installPackage(String, boolean, String...)} |
| * @param packageFile |
| * @param reinstall |
| * @param extraArgs |
| * @return the response from the installation |
| * @throws DeviceNotAvailableException |
| */ |
| private String internalInstallPackage( |
| final File packageFile, final boolean reinstall, final List<String> extraArgs) |
| throws DeviceNotAvailableException { |
| // use array to store response, so it can be returned to caller |
| final String[] response = new String[1]; |
| DeviceAction installAction = new DeviceAction() { |
| @Override |
| public boolean run() throws InstallException { |
| try { |
| getIDevice().installPackage(packageFile.getAbsolutePath(), |
| reinstall, extraArgs.toArray(new String[]{})); |
| response[0] = null; |
| } catch (InstallException e) { |
| response[0] = e.getMessage(); |
| } |
| return response[0] == null; |
| } |
| }; |
| performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()), |
| installAction, MAX_RETRY_ATTEMPTS); |
| return response[0]; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String installPackage(final File packageFile, final boolean reinstall, |
| final String... extraArgs) throws DeviceNotAvailableException { |
| boolean runtimePermissionSupported = isRuntimePermissionSupported(); |
| List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); |
| // grant all permissions by default if feature is supported |
| if (runtimePermissionSupported) { |
| args.add("-g"); |
| } |
| return internalInstallPackage(packageFile, reinstall, args); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String installPackage(File packageFile, boolean reinstall, boolean grantPermissions, |
| String... extraArgs) throws DeviceNotAvailableException { |
| ensureRuntimePermissionSupported(); |
| List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); |
| if (grantPermissions) { |
| args.add("-g"); |
| } |
| return internalInstallPackage(packageFile, reinstall, args); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String installPackageForUser(File packageFile, boolean reinstall, int userId, |
| String... extraArgs) throws DeviceNotAvailableException { |
| boolean runtimePermissionSupported = isRuntimePermissionSupported(); |
| List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); |
| // grant all permissions by default if feature is supported |
| if (runtimePermissionSupported) { |
| args.add("-g"); |
| } |
| args.add("--user"); |
| args.add(Integer.toString(userId)); |
| return internalInstallPackage(packageFile, reinstall, args); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String installPackageForUser(File packageFile, boolean reinstall, |
| boolean grantPermissions, int userId, String... extraArgs) |
| throws DeviceNotAvailableException { |
| ensureRuntimePermissionSupported(); |
| List<String> args = new ArrayList<>(Arrays.asList(extraArgs)); |
| if (grantPermissions) { |
| args.add("-g"); |
| } |
| args.add("--user"); |
| args.add(Integer.toString(userId)); |
| return internalInstallPackage(packageFile, reinstall, args); |
| } |
| |
| public String installPackage(final File packageFile, final File certFile, |
| final boolean reinstall, final String... extraArgs) throws DeviceNotAvailableException { |
| // use array to store response, so it can be returned to caller |
| final String[] response = new String[1]; |
| DeviceAction installAction = new DeviceAction() { |
| @Override |
| public boolean run() throws InstallException, SyncException, IOException, |
| TimeoutException, AdbCommandRejectedException { |
| // TODO: create a getIDevice().installPackage(File, File...) method when the dist |
| // cert functionality is ready to be open sourced |
| String remotePackagePath = getIDevice().syncPackageToDevice( |
| packageFile.getAbsolutePath()); |
| String remoteCertPath = getIDevice().syncPackageToDevice( |
| certFile.getAbsolutePath()); |
| // trick installRemotePackage into issuing a 'pm install <apk> <cert>' command, |
| // by adding apk path to extraArgs, and using cert as the 'apk file' |
| String[] newExtraArgs = new String[extraArgs.length + 1]; |
| System.arraycopy(extraArgs, 0, newExtraArgs, 0, extraArgs.length); |
| newExtraArgs[newExtraArgs.length - 1] = String.format("\"%s\"", remotePackagePath); |
| try { |
| getIDevice().installRemotePackage(remoteCertPath, reinstall, |
| newExtraArgs); |
| response[0] = null; |
| } catch (InstallException e) { |
| response[0] = e.getMessage(); |
| } finally { |
| getIDevice().removeRemotePackage(remotePackagePath); |
| getIDevice().removeRemotePackage(remoteCertPath); |
| } |
| return true; |
| } |
| }; |
| performDeviceAction(String.format("install %s", packageFile.getAbsolutePath()), |
| installAction, MAX_RETRY_ATTEMPTS); |
| return response[0]; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String uninstallPackage(final String packageName) throws DeviceNotAvailableException { |
| // use array to store response, so it can be returned to caller |
| final String[] response = new String[1]; |
| DeviceAction uninstallAction = new DeviceAction() { |
| @Override |
| public boolean run() throws InstallException { |
| CLog.d("Uninstalling %s", packageName); |
| String result = getIDevice().uninstallPackage(packageName); |
| response[0] = result; |
| return result == null; |
| } |
| }; |
| performDeviceAction(String.format("uninstall %s", packageName), uninstallAction, |
| MAX_RETRY_ATTEMPTS); |
| return response[0]; |
| } |
| |
| @Override |
| public InputStreamSource getScreenshot() throws DeviceNotAvailableException { |
| return getScreenshot("PNG"); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public InputStreamSource getScreenshot(String format) throws DeviceNotAvailableException { |
| if (!format.equalsIgnoreCase("PNG") && !format.equalsIgnoreCase("JPEG")){ |
| CLog.e("Screenshot: Format %s is not supported, defaulting to PNG.", format); |
| format = "PNG"; |
| } |
| ScreenshotAction action = new ScreenshotAction(); |
| if (performDeviceAction("screenshot", action, MAX_RETRY_ATTEMPTS)) { |
| byte[] imageData = compressRawImage(action.mRawScreenshot, format.toUpperCase()); |
| if (imageData != null) { |
| return new ByteArrayInputStreamSource(imageData); |
| } |
| } |
| return null; |
| } |
| |
| private class ScreenshotAction implements DeviceAction { |
| |
| RawImage mRawScreenshot; |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException, |
| ShellCommandUnresponsiveException, InstallException, SyncException { |
| mRawScreenshot = getIDevice().getScreenshot(); |
| return mRawScreenshot != null; |
| } |
| } |
| |
| /** |
| * Helper to compress a rawImage obtained from the screen. |
| */ |
| @VisibleForTesting |
| protected byte[] compressRawImage(RawImage rawImage, String format) { |
| BufferedImage image = null; |
| |
| if ("JPEG".equalsIgnoreCase(format)) { |
| //JPEG does not support ARGB without a special encoder |
| image = new BufferedImage(rawImage.width, rawImage.height, |
| BufferedImage.TYPE_3BYTE_BGR); |
| } |
| else { |
| image = new BufferedImage(rawImage.width, rawImage.height, |
| BufferedImage.TYPE_INT_ARGB); |
| } |
| |
| // borrowed conversion logic from platform/sdk/screenshot/.../Screenshot.java |
| int index = 0; |
| int IndexInc = rawImage.bpp >> 3; |
| for (int y = 0 ; y < rawImage.height ; y++) { |
| for (int x = 0 ; x < rawImage.width ; x++) { |
| int value = rawImage.getARGB(index); |
| index += IndexInc; |
| image.setRGB(x, y, value); |
| } |
| } |
| |
| // Rescale to reduce size if needed |
| // Screenshot default format is 1080 x 1920, 8-bit/color RGBA |
| // By cutting in half we can easily keep good quality and smaller size |
| int shortEdge = Math.min(image.getHeight(), image.getWidth()); |
| if (shortEdge > 720) { |
| Image resized = image.getScaledInstance(image.getWidth() / 2, image.getHeight() / 2, |
| Image.SCALE_SMOOTH); |
| image = new BufferedImage(image.getWidth() / 2, image.getHeight() / 2, |
| Image.SCALE_REPLICATE); |
| image.getGraphics().drawImage(resized, 0, 0, null); |
| } |
| |
| // store compressed image in memory, and let callers write to persistent storage |
| // use initial buffer size of 128K |
| byte[] imageData = null; |
| ByteArrayOutputStream imageOut = new ByteArrayOutputStream(128*1024); |
| try { |
| if (ImageIO.write(image, format, imageOut)) { |
| imageData = imageOut.toByteArray(); |
| } else { |
| CLog.e("Failed to compress screenshot to png"); |
| } |
| } catch (IOException e) { |
| CLog.e("Failed to compress screenshot to png"); |
| CLog.e(e); |
| } |
| StreamUtil.close(imageOut); |
| return imageData; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean clearErrorDialogs() throws DeviceNotAvailableException { |
| // attempt to clear error dialogs multiple times |
| for (int i = 0; i < NUM_CLEAR_ATTEMPTS; i++) { |
| int numErrorDialogs = getErrorDialogCount(); |
| if (numErrorDialogs == 0) { |
| return true; |
| } |
| doClearDialogs(numErrorDialogs); |
| } |
| if (getErrorDialogCount() > 0) { |
| // at this point, all attempts to clear error dialogs completely have failed |
| // it might be the case that the process keeps showing new dialogs immediately after |
| // clearing. There's really no workaround, but to dump an error |
| CLog.e("error dialogs still exist on %s.", getSerialNumber()); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Detects the number of crash or ANR dialogs currently displayed. |
| * <p/> |
| * Parses output of 'dump activity processes' |
| * |
| * @return count of dialogs displayed |
| * @throws DeviceNotAvailableException |
| */ |
| private int getErrorDialogCount() throws DeviceNotAvailableException { |
| int errorDialogCount = 0; |
| Pattern crashPattern = Pattern.compile(".*crashing=true.*AppErrorDialog.*"); |
| Pattern anrPattern = Pattern.compile(".*notResponding=true.*AppNotRespondingDialog.*"); |
| String systemStatusOutput = executeShellCommand("dumpsys activity processes"); |
| Matcher crashMatcher = crashPattern.matcher(systemStatusOutput); |
| while (crashMatcher.find()) { |
| errorDialogCount++; |
| } |
| Matcher anrMatcher = anrPattern.matcher(systemStatusOutput); |
| while (anrMatcher.find()) { |
| errorDialogCount++; |
| } |
| |
| return errorDialogCount; |
| } |
| |
| private void doClearDialogs(int numDialogs) throws DeviceNotAvailableException { |
| CLog.i("Attempted to clear %d dialogs on %s", numDialogs, getSerialNumber()); |
| for (int i=0; i < numDialogs; i++) { |
| // send DPAD_CENTER |
| executeShellCommand(DISMISS_DIALOG_CMD); |
| } |
| } |
| |
| /** |
| * Gets the adb shell command to disable the keyguard for this device. |
| * <p/> |
| * Exposed for unit testing. |
| */ |
| String getDisableKeyguardCmd() { |
| return mOptions.getDisableKeyguardCmd(); |
| } |
| |
| /** |
| * Attempts to disable the keyguard. |
| * <p> |
| * First wait for the input dispatch to become ready, this happens around the same time when the |
| * device reports BOOT_COMPLETE, apparently asynchronously, because current framework |
| * implementation has occasional race condition. Then command is sent to dismiss keyguard (works |
| * on non-secure ones only) |
| * @throws DeviceNotAvailableException |
| */ |
| void disableKeyguard() throws DeviceNotAvailableException { |
| long start = System.currentTimeMillis(); |
| while (true) { |
| Boolean ready = isDeviceInputReady(); |
| if (ready == null) { |
| // unsupported API level, bail |
| break; |
| } |
| if (ready) { |
| // input dispatch is ready, bail |
| break; |
| } |
| long timeSpent = System.currentTimeMillis() - start; |
| if (timeSpent > INPUT_DISPATCH_READY_TIMEOUT) { |
| CLog.w("Timeout after waiting %dms on enabling of input dispatch", timeSpent); |
| // break & proceed anyway |
| break; |
| } else { |
| getRunUtil().sleep(1000); |
| } |
| } |
| CLog.i("Attempting to disable keyguard on %s using %s", getSerialNumber(), |
| getDisableKeyguardCmd()); |
| executeShellCommand(getDisableKeyguardCmd()); |
| } |
| |
| /** |
| * Tests the device to see if input dispatcher is ready |
| * @return <code>null</code> if not supported by platform, or the actual readiness state |
| * @throws DeviceNotAvailableException |
| */ |
| Boolean isDeviceInputReady() throws DeviceNotAvailableException { |
| CollectingOutputReceiver receiver = new CollectingOutputReceiver(); |
| executeShellCommand(TEST_INPUT_CMD, receiver); |
| String output = receiver.getOutput(); |
| Matcher m = INPUT_DISPATCH_STATE_REGEX.matcher(output); |
| if (!m.find()) { |
| // output does not contain the line at all, implying unsupported API level, bail |
| return null; |
| } |
| return "1".equals(m.group(1)); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| protected void prePostBootSetup() throws DeviceNotAvailableException { |
| if (mOptions.isDisableKeyguard()) { |
| disableKeyguard(); |
| } |
| } |
| |
| /** |
| * Performs an reboot via framework power manager |
| * |
| * Must have root access, device must be API Level 18 or above |
| * |
| * @param into the mode to reboot into, currently supported: bootloader, recovery, leave it |
| * null for a plain reboot |
| * @return <code>true</code> if the device rebooted, <code>false</code> if not successful or |
| * unsupported |
| * @throws DeviceNotAvailableException |
| */ |
| private boolean doAdbFrameworkReboot(final String into) throws DeviceNotAvailableException { |
| // use framework reboot when: |
| // 1. device API level >= 18 |
| // 2. has adb root |
| // 3. framework is running |
| if (!isEnableAdbRoot()) { |
| CLog.i("framework reboot is not supported; when enable root is disabled"); |
| return false; |
| } |
| enableAdbRoot(); |
| if (getApiLevel() >= 18 && isAdbRoot()) { |
| try { |
| // check framework running |
| String output = executeShellCommand("pm path android"); |
| if (output == null || !output.contains("package:")) { |
| CLog.v("framework reboot: can't detect framework running"); |
| return false; |
| } |
| String command = "svc power reboot"; |
| if (into != null && !into.isEmpty()) { |
| command = String.format("%s %s", command, into); |
| } |
| executeShellCommand(command); |
| } catch (DeviceUnresponsiveException due) { |
| CLog.v("framework reboot: device unresponsive to shell command, using fallback"); |
| return false; |
| } |
| return waitForDeviceNotAvailable(30 * 1000); |
| } else { |
| CLog.v("framework reboot: not supported"); |
| return false; |
| } |
| } |
| |
| /** |
| * Perform a adb reboot. |
| * |
| * @param into the bootloader name to reboot into, or <code>null</code> to just reboot the |
| * device. |
| * @throws DeviceNotAvailableException |
| */ |
| @Override |
| protected void doAdbReboot(final String into) throws DeviceNotAvailableException { |
| if (!doAdbFrameworkReboot(into)) { |
| DeviceAction rebootAction = new DeviceAction() { |
| @Override |
| public boolean run() throws TimeoutException, IOException, |
| AdbCommandRejectedException { |
| getIDevice().reboot(into); |
| return true; |
| } |
| }; |
| performDeviceAction("reboot", rebootAction, MAX_RETRY_ATTEMPTS); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Set<String> getInstalledPackageNames() throws DeviceNotAvailableException { |
| return getInstalledPackageNames(new PkgFilter() { |
| @Override |
| public boolean accept(String pkgName, String apkPath) { |
| return true; |
| } |
| }); |
| } |
| |
| /** |
| * A {@link com.android.tradefed.device.NativeDevice.DeviceAction} |
| * for retrieving package system service info, and do retries on |
| * failures. |
| */ |
| private class DumpPkgAction implements DeviceAction { |
| |
| Map<String, PackageInfo> mPkgInfoMap; |
| |
| DumpPkgAction() { |
| } |
| |
| @Override |
| public boolean run() throws IOException, TimeoutException, AdbCommandRejectedException, |
| ShellCommandUnresponsiveException, InstallException, SyncException { |
| DumpsysPackageReceiver receiver = new DumpsysPackageReceiver(); |
| getIDevice().executeShellCommand("dumpsys package p", receiver); |
| mPkgInfoMap = receiver.getPackages(); |
| if (mPkgInfoMap.size() == 0) { |
| // Package parsing can fail if package manager is currently down. throw exception |
| // to retry |
| CLog.w("no packages found from dumpsys package p."); |
| throw new IOException(); |
| } |
| return true; |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Set<String> getUninstallablePackageNames() throws DeviceNotAvailableException { |
| DumpPkgAction action = new DumpPkgAction(); |
| performDeviceAction("dumpsys package p", action, MAX_RETRY_ATTEMPTS); |
| |
| Set<String> pkgs = new HashSet<String>(); |
| for (PackageInfo pkgInfo : action.mPkgInfoMap.values()) { |
| if (!pkgInfo.isSystemApp() || pkgInfo.isUpdatedSystemApp()) { |
| CLog.d("Found uninstallable package %s", pkgInfo.getPackageName()); |
| pkgs.add(pkgInfo.getPackageName()); |
| } |
| } |
| return pkgs; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public PackageInfo getAppPackageInfo(String packageName) throws DeviceNotAvailableException { |
| DumpPkgAction action = new DumpPkgAction(); |
| performDeviceAction("dumpsys package", action, MAX_RETRY_ATTEMPTS); |
| return action.mPkgInfoMap.get(packageName); |
| } |
| |
| private static interface PkgFilter { |
| boolean accept(String pkgName, String apkPath); |
| } |
| |
| // TODO: convert this to use DumpPkgAction |
| private Set<String> getInstalledPackageNames(PkgFilter filter) |
| throws DeviceNotAvailableException { |
| Set<String> packages= new HashSet<String>(); |
| String output = executeShellCommand(LIST_PACKAGES_CMD); |
| if (output != null) { |
| Matcher m = PACKAGE_REGEX.matcher(output); |
| while (m.find()) { |
| String packagePath = m.group(1); |
| String packageName = m.group(2); |
| if (filter.accept(packageName, packagePath)) { |
| packages.add(packageName); |
| } |
| } |
| } |
| return packages; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public ArrayList<Integer> listUsers() throws DeviceNotAvailableException { |
| ArrayList<String[]> users = tokenizeListUsers(); |
| if (users == null) { |
| return null; |
| } |
| ArrayList<Integer> userIds = new ArrayList<Integer>(users.size()); |
| for (String[] user : users) { |
| userIds.add(Integer.parseInt(user[1])); |
| } |
| return userIds; |
| } |
| |
| /** |
| * Tokenizes the output of 'pm list users'. |
| * The returned tokens for each user have the form: {"\tUserInfo", Integer.toString(id), name, |
| * Integer.toHexString(flag), "[running]"}; (the last one being optional) |
| * @return a list of arrays of strings, each element of the list representing the tokens |
| * for a user, or {@code null} if there was an error while tokenizing the adb command output. |
| */ |
| private ArrayList<String[]> tokenizeListUsers() throws DeviceNotAvailableException { |
| String command = "pm list users"; |
| String commandOutput = executeShellCommand(command); |
| // Extract the id of all existing users. |
| String[] lines = commandOutput.split("\\r?\\n"); |
| if (lines.length < 1) { |
| CLog.e("%s should contain at least one line", commandOutput); |
| return null; |
| } |
| if (!lines[0].equals("Users:")) { |
| CLog.e("%s in not a valid output for 'pm list users'", commandOutput); |
| return null; |
| } |
| ArrayList<String[]> users = new ArrayList<String[]>(lines.length - 1); |
| for (int i = 1; i < lines.length; i++) { |
| // Individual user is printed out like this: |
| // \tUserInfo{$id$:$name$:$Integer.toHexString(flags)$} [running] |
| String[] tokens = lines[i].split("\\{|\\}|:"); |
| if (tokens.length != 4 && tokens.length != 5) { |
| CLog.e("%s doesn't contain 4 or 5 tokens", lines[i]); |
| return null; |
| } |
| users.add(tokens); |
| } |
| return users; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int getMaxNumberOfUsersSupported() throws DeviceNotAvailableException { |
| String command = "pm get-max-users"; |
| String commandOutput = executeShellCommand(command); |
| try { |
| return Integer.parseInt(commandOutput.substring(commandOutput.lastIndexOf(" ")).trim()); |
| } catch (NumberFormatException e) { |
| CLog.e("Failed to parse result: %s", commandOutput); |
| } |
| return 0; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean isMultiUserSupported() throws DeviceNotAvailableException { |
| return getMaxNumberOfUsersSupported() > 1; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int createUser(String name) throws DeviceNotAvailableException, IllegalStateException { |
| return createUser(name, false, false); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int createUser(String name, boolean guest, boolean ephemeral) |
| throws DeviceNotAvailableException, IllegalStateException { |
| String command ="pm create-user " + (guest ? "--guest " : "") |
| + (ephemeral ? "--ephemeral " : "") + name; |
| final String output = executeShellCommand(command); |
| if (output.startsWith("Success")) { |
| try { |
| return Integer.parseInt(output.substring(output.lastIndexOf(" ")).trim()); |
| } catch (NumberFormatException e) { |
| CLog.e("Failed to parse result: %s", output); |
| } |
| } else { |
| CLog.e("Failed to create user: %s", output); |
| } |
| throw new IllegalStateException(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean removeUser(int userId) throws DeviceNotAvailableException { |
| final String output = executeShellCommand(String.format("pm remove-user %s", userId)); |
| if (output.startsWith("Error")) { |
| CLog.e("Failed to remove user: %s", output); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean startUser(int userId) throws DeviceNotAvailableException { |
| final String output = executeShellCommand(String.format("am start-user %s", userId)); |
| if (output.startsWith("Error")) { |
| CLog.e("Failed to start user: %s", output); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean stopUser(int userId) throws DeviceNotAvailableException { |
| // No error or status code is returned. |
| return stopUser(userId, false, false); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean stopUser(int userId, boolean waitFlag, boolean forceFlag) |
| throws DeviceNotAvailableException { |
| checkApiLevelAgainstNextRelease("stopUser", API_LEVEL_GET_CURRENT_USER); |
| if (userId == getCurrentUser()) { |
| CLog.d("Cannot stop current user."); |
| return false; |
| } |
| StringBuilder cmd = new StringBuilder("am stop-user "); |
| if (waitFlag) { |
| cmd.append("-w "); |
| } |
| if (forceFlag) { |
| cmd.append("-f "); |
| } |
| cmd.append(userId); |
| |
| CLog.d("stopping user with command: %s", cmd.toString()); |
| final String output = executeShellCommand(cmd.toString()); |
| if (output.contains("Error: Can't stop system user")) { |
| CLog.e("Cannot stop System user."); |
| return false; |
| } |
| if (isUserRunning(userId)) { |
| CLog.w("User Id: %s is still running after the stop-user command.", userId); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Integer getPrimaryUserId() throws DeviceNotAvailableException { |
| ArrayList<String[]> users = tokenizeListUsers(); |
| if (users == null) { |
| return null; |
| } |
| for (String[] user : users) { |
| int flag = Integer.parseInt(user[3], 16); |
| if ((flag & FLAG_PRIMARY) != 0) { |
| return Integer.parseInt(user[1]); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int getCurrentUser() throws DeviceNotAvailableException { |
| checkApiLevelAgainstNextRelease("get-current-user", API_LEVEL_GET_CURRENT_USER); |
| final String output = executeShellCommand("am get-current-user"); |
| try { |
| int userId = Integer.parseInt(output.trim()); |
| return userId; |
| } catch (NumberFormatException e) { |
| CLog.e(e); |
| } |
| return INVALID_USER_ID; |
| } |
| |
| private Matcher findUserInfo(String pmListUsersOutput) { |
| Pattern pattern = Pattern.compile(USER_PATTERN); |
| Matcher matcher = pattern.matcher(pmListUsersOutput); |
| return matcher; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int getUserFlags(int userId) throws DeviceNotAvailableException { |
| checkApiLevelAgainst("getUserFlags", 22); |
| final String commandOutput = executeShellCommand("pm list users"); |
| Matcher matcher = findUserInfo(commandOutput); |
| while(matcher.find()) { |
| if (Integer.parseInt(matcher.group(2)) == userId) { |
| return Integer.parseInt(matcher.group(6), 16); |
| } |
| } |
| CLog.w("Could not find any flags for userId: %d in output: %s", userId, commandOutput); |
| return INVALID_USER_ID; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean isUserRunning(int userId) throws DeviceNotAvailableException { |
| checkApiLevelAgainst("isUserIdRunning", 22); |
| final String commandOutput = executeShellCommand("pm list users"); |
| Matcher matcher = findUserInfo(commandOutput); |
| while(matcher.find()) { |
| if (Integer.parseInt(matcher.group(2)) == userId) { |
| if (matcher.group(7).contains("running")) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public int getUserSerialNumber(int userId) throws DeviceNotAvailableException { |
| checkApiLevelAgainst("getUserSerialNumber", 22); |
| final String commandOutput = executeShellCommand("dumpsys user"); |
| // example: UserInfo{0:Test:13} serialNo=0 |
| String userSerialPatter = "(.*\\{)(\\d+)(.*\\})(.*=)(\\d+)"; |
| Pattern pattern = Pattern.compile(userSerialPatter); |
| Matcher matcher = pattern.matcher(commandOutput); |
| while(matcher.find()) { |
| if (Integer.parseInt(matcher.group(2)) == userId) { |
| return Integer.parseInt(matcher.group(5)); |
| } |
| } |
| CLog.w("Could not find user serial number for userId: %d, in output: %s", |
| userId, commandOutput); |
| return INVALID_USER_ID; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean switchUser(int userId) throws DeviceNotAvailableException { |
| return switchUser(userId, AM_COMMAND_TIMEOUT); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean switchUser(int userId, long timeout) throws DeviceNotAvailableException { |
| checkApiLevelAgainstNextRelease("switchUser", API_LEVEL_GET_CURRENT_USER); |
| if (userId == getCurrentUser()) { |
| CLog.w("Already running as user id: %s. Nothing to be done.", userId); |
| return true; |
| } |
| executeShellCommand(String.format("am switch-user %d", userId)); |
| long initialTime = getHostCurrentTime(); |
| while (getHostCurrentTime() - initialTime <= timeout) { |
| if (userId == getCurrentUser()) { |
| // disable keyguard if option is true |
| prePostBootSetup(); |
| return true; |
| } else { |
| RunUtil.getDefault().sleep(getCheckNewUserSleep()); |
| } |
| } |
| CLog.e("User did not switch in the given %d timeout", timeout); |
| return false; |
| } |
| |
| /** |
| * Exposed for testing. |
| */ |
| protected long getCheckNewUserSleep() { |
| return CHECK_NEW_USER; |
| } |
| |
| /** |
| * Exposed for testing |
| */ |
| protected long getHostCurrentTime() { |
| return System.currentTimeMillis(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean hasFeature(String feature) throws DeviceNotAvailableException { |
| final String output = executeShellCommand("pm list features"); |
| if (output.contains(feature)) { |
| return true; |
| } |
| CLog.w("Feature: %s is not available on %s", feature, getSerialNumber()); |
| return false; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String getSetting(String namespace, String key) throws DeviceNotAvailableException { |
| return getSettingInternal("", namespace.trim(), key.trim()); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String getSetting(int userId, String namespace, String key) |
| throws DeviceNotAvailableException { |
| return getSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim()); |
| } |
| |
| /** |
| * Internal Helper to get setting with or without a userId provided. |
| */ |
| private String getSettingInternal(String userFlag, String namespace, String key) |
| throws DeviceNotAvailableException { |
| namespace = namespace.toLowerCase(); |
| if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace)) { |
| String cmd = String.format("settings %s get %s %s", userFlag, namespace, key); |
| String output = executeShellCommand(cmd); |
| if ("null".equals(output)) { |
| CLog.w("settings returned null for command: %s. " |
| + "please check if the namespace:key exists", cmd); |
| return null; |
| } |
| return output.trim(); |
| } |
| CLog.e("Namespace requested: '%s' is not part of {system, secure, global}", namespace); |
| return null; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void setSetting(String namespace, String key, String value) |
| throws DeviceNotAvailableException { |
| setSettingInternal("", namespace.trim(), key.trim(), value.trim()); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void setSetting(int userId, String namespace, String key, String value) |
| throws DeviceNotAvailableException { |
| setSettingInternal(String.format("--user %d", userId), namespace.trim(), key.trim(), |
| value.trim()); |
| } |
| |
| /** |
| * Internal helper to set a setting with or without a userId provided. |
| */ |
| private void setSettingInternal(String userFlag, String namespace, String key, String value) |
| throws DeviceNotAvailableException { |
| checkApiLevelAgainst("Changing settings", 22); |
| if (Arrays.asList(SETTINGS_NAMESPACE).contains(namespace.toLowerCase())) { |
| executeShellCommand(String.format("settings %s put %s %s %s", |
| userFlag, namespace, key, value)); |
| } else { |
| throw new IllegalArgumentException("Namespace must be one of system, secure, global." |
| + " You provided: " + namespace); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String getAndroidId(int userId) throws DeviceNotAvailableException { |
| if (isAdbRoot()) { |
| String cmd = String.format( |
| "sqlite3 /data/user/%d/com.google.android.gsf/databases/gservices.db " |
| + "'select value from main where name = \"android_id\"'", userId); |
| String output = executeShellCommand(cmd).trim(); |
| if (!output.contains("unable to open database")) { |
| return output; |
| } |
| CLog.w("Couldn't find android-id, output: %s", output); |
| } else { |
| CLog.w("adb root is required."); |
| } |
| return null; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Map<Integer, String> getAndroidIds() throws DeviceNotAvailableException { |
| ArrayList<Integer> userIds = listUsers(); |
| if (userIds == null) { |
| return null; |
| } |
| Map<Integer, String> androidIds = new HashMap<Integer, String>(); |
| for (Integer id : userIds) { |
| String androidId = getAndroidId(id); |
| androidIds.put(id, androidId); |
| } |
| return androidIds; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| IWifiHelper createWifiHelper() throws DeviceNotAvailableException { |
| return new WifiHelper(this); |
| } |
| |
| /** |
| * Helper for Api level checking of features in the new release before we incremented the api |
| * number. |
| */ |
| private void checkApiLevelAgainstNextRelease(String feature, int strictMinLevel) |
| throws DeviceNotAvailableException { |
| String codeName = getProperty(BUILD_CODENAME_PROP).trim(); |
| int apiLevel = getApiLevel() + (codeName == "REL" ? 0 : 1); |
| if (apiLevel < strictMinLevel){ |
| throw new IllegalArgumentException(String.format("%s not supported on %s. " |
| + "Must be API %d.", feature, getSerialNumber(), strictMinLevel)); |
| } |
| } |
| } |