/* * Copyright (C) 2007 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.ddmlib; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.annotations.VisibleForTesting; import com.android.annotations.concurrency.GuardedBy; import com.android.ddmlib.log.LogReceiver; import com.google.common.base.CharMatcher; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Atomics; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A Device. It can be a physical device or an emulator. */ final class Device implements IDevice { /** Emulator Serial Number regexp. */ static final String RE_EMULATOR_SN = "emulator-(\\d+)"; //$NON-NLS-1$ /** Serial number of the device */ private final String mSerialNumber; /** Name of the AVD */ private String mAvdName = null; /** State of the device. */ private DeviceState mState = null; /** Device properties. */ private final PropertyFetcher mPropFetcher = new PropertyFetcher(this); private final Map<String, String> mMountPoints = new HashMap<String, String>(); private final BatteryFetcher mBatteryFetcher = new BatteryFetcher(this); @GuardedBy("mClients") private final List<Client> mClients = new ArrayList<Client>(); /** Maps pid's of clients in {@link #mClients} to their package name. */ private final Map<Integer, String> mClientInfo = new ConcurrentHashMap<Integer, String>(); private DeviceMonitor mMonitor; private static final String LOG_TAG = "Device"; private static final char SEPARATOR = '-'; private static final String UNKNOWN_PACKAGE = ""; //$NON-NLS-1$ private static final long GET_PROP_TIMEOUT_MS = 100; private static final long INSTALL_TIMEOUT_MINUTES; static { String installTimeout = System.getenv("ADB_INSTALL_TIMEOUT"); long time = 4; if (installTimeout != null) { try { time = Long.parseLong(installTimeout); } catch (NumberFormatException e) { // use default value } } INSTALL_TIMEOUT_MINUTES = time; } /** * Socket for the connection monitoring client connection/disconnection. */ private SocketChannel mSocketChannel; private Integer mLastBatteryLevel = null; private long mLastBatteryCheckTime = 0; /** Path to the screen recorder binary on the device. */ private static final String SCREEN_RECORDER_DEVICE_PATH = "/system/bin/screenrecord"; private static final long LS_TIMEOUT_SEC = 2; /** Flag indicating whether the device has the screen recorder binary. */ private Boolean mHasScreenRecorder; /** Cached list of hardware characteristics */ private Set<String> mHardwareCharacteristics; private int mApiLevel; private String mName; /** * Output receiver for "pm install package.apk" command line. */ private static final class InstallReceiver extends MultiLineReceiver { private static final String SUCCESS_OUTPUT = "Success"; //$NON-NLS-1$ private static final Pattern FAILURE_PATTERN = Pattern.compile("Failure\\s+\\[(.*)\\]"); //$NON-NLS-1$ private String mErrorMessage = null; public InstallReceiver() { } @Override public void processNewLines(String[] lines) { for (String line : lines) { if (!line.isEmpty()) { if (line.startsWith(SUCCESS_OUTPUT)) { mErrorMessage = null; } else { Matcher m = FAILURE_PATTERN.matcher(line); if (m.matches()) { mErrorMessage = m.group(1); } else { mErrorMessage = "Unknown failure"; } } } } } @Override public boolean isCancelled() { return false; } public String getErrorMessage() { return mErrorMessage; } } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getSerialNumber() */ @NonNull @Override public String getSerialNumber() { return mSerialNumber; } @Override public String getAvdName() { return mAvdName; } /** * Sets the name of the AVD */ void setAvdName(String avdName) { if (!isEmulator()) { throw new IllegalArgumentException( "Cannot set the AVD name of the device is not an emulator"); } mAvdName = avdName; } @Override public String getName() { if (mName != null) { return mName; } if (isOnline()) { // cache name only if device is online mName = constructName(); return mName; } else { return constructName(); } } private String constructName() { if (isEmulator()) { String avdName = getAvdName(); if (avdName != null) { return String.format("%s [%s]", avdName, getSerialNumber()); } else { return getSerialNumber(); } } else { String manufacturer = null; String model = null; try { manufacturer = cleanupStringForDisplay(getProperty(PROP_DEVICE_MANUFACTURER)); model = cleanupStringForDisplay(getProperty(PROP_DEVICE_MODEL)); } catch (Exception e) { // If there are exceptions thrown while attempting to get these properties, // we can just use the serial number, so ignore these exceptions. } StringBuilder sb = new StringBuilder(20); if (manufacturer != null) { sb.append(manufacturer); sb.append(SEPARATOR); } if (model != null) { sb.append(model); sb.append(SEPARATOR); } sb.append(getSerialNumber()); return sb.toString(); } } private String cleanupStringForDisplay(String s) { if (s == null) { return null; } StringBuilder sb = new StringBuilder(s.length()); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (Character.isLetterOrDigit(c)) { sb.append(Character.toLowerCase(c)); } else { sb.append('_'); } } return sb.toString(); } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getState() */ @Override public DeviceState getState() { return mState; } /** * Changes the state of the device. */ void setState(DeviceState state) { mState = state; } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getProperties() */ @Override public Map<String, String> getProperties() { return Collections.unmodifiableMap(mPropFetcher.getProperties()); } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getPropertyCount() */ @Override public int getPropertyCount() { return mPropFetcher.getProperties().size(); } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getProperty(java.lang.String) */ @Override public String getProperty(String name) { Future<String> future = mPropFetcher.getProperty(name); try { return future.get(GET_PROP_TIMEOUT_MS, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { // ignore } catch (ExecutionException e) { // ignore } catch (java.util.concurrent.TimeoutException e) { // ignore } return null; } @Override public boolean arePropertiesSet() { return mPropFetcher.arePropertiesSet(); } @Override public String getPropertyCacheOrSync(String name) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { Future<String> future = mPropFetcher.getProperty(name); try { return future.get(); } catch (InterruptedException e) { // ignore } catch (ExecutionException e) { // ignore } return null; } @Override public String getPropertySync(String name) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { Future<String> future = mPropFetcher.getProperty(name); try { return future.get(); } catch (InterruptedException e) { // ignore } catch (ExecutionException e) { // ignore } return null; } @NonNull @Override public Future<String> getSystemProperty(@NonNull String name) { return mPropFetcher.getProperty(name); } @Override public boolean supportsFeature(@NonNull Feature feature) { switch (feature) { case SCREEN_RECORD: if (getApiLevel() < 19) { return false; } if (mHasScreenRecorder == null) { mHasScreenRecorder = hasBinary(SCREEN_RECORDER_DEVICE_PATH); } return mHasScreenRecorder; case PROCSTATS: return getApiLevel() >= 19; default: return false; } } // The full list of features can be obtained from /etc/permissions/features* // However, the smaller set of features we are interested in can be obtained by // reading the build characteristics property. @Override public boolean supportsFeature(@NonNull HardwareFeature feature) { if (mHardwareCharacteristics == null) { try { String characteristics = getProperty(PROP_BUILD_CHARACTERISTICS); if (characteristics == null) { return false; } mHardwareCharacteristics = Sets.newHashSet(Splitter.on(',').split(characteristics)); } catch (Exception e) { mHardwareCharacteristics = Collections.emptySet(); } } return mHardwareCharacteristics.contains(feature.getCharacteristic()); } private int getApiLevel() { if (mApiLevel > 0) { return mApiLevel; } try { String buildApi = getProperty(PROP_BUILD_API_LEVEL); mApiLevel = buildApi == null ? -1 : Integer.parseInt(buildApi); return mApiLevel; } catch (Exception e) { return -1; } } private boolean hasBinary(String path) { CountDownLatch latch = new CountDownLatch(1); CollectingOutputReceiver receiver = new CollectingOutputReceiver(latch); try { executeShellCommand("ls " + path, receiver); } catch (Exception e) { return false; } try { latch.await(LS_TIMEOUT_SEC, TimeUnit.SECONDS); } catch (InterruptedException e) { return false; } String value = receiver.getOutput().trim(); return !value.endsWith("No such file or directory"); } @Nullable @Override public String getMountPoint(@NonNull String name) { String mount = mMountPoints.get(name); if (mount == null) { try { mount = queryMountPoint(name); mMountPoints.put(name, mount); } catch (TimeoutException ignored) { } catch (AdbCommandRejectedException ignored) { } catch (ShellCommandUnresponsiveException ignored) { } catch (IOException ignored) { } } return mount; } @Nullable private String queryMountPoint(@NonNull final String name) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { final AtomicReference<String> ref = Atomics.newReference(); executeShellCommand("echo $" + name, new MultiLineReceiver() { //$NON-NLS-1$ @Override public boolean isCancelled() { return false; } @Override public void processNewLines(String[] lines) { for (String line : lines) { if (!line.isEmpty()) { // this should be the only one. ref.set(line); } } } }); return ref.get(); } @Override public String toString() { return mSerialNumber; } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#isOnline() */ @Override public boolean isOnline() { return mState == DeviceState.ONLINE; } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#isEmulator() */ @Override public boolean isEmulator() { return mSerialNumber.matches(RE_EMULATOR_SN); } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#isOffline() */ @Override public boolean isOffline() { return mState == DeviceState.OFFLINE; } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#isBootLoader() */ @Override public boolean isBootLoader() { return mState == DeviceState.BOOTLOADER; } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getSyncService() */ @Override public SyncService getSyncService() throws TimeoutException, AdbCommandRejectedException, IOException { SyncService syncService = new SyncService(AndroidDebugBridge.getSocketAddress(), this); if (syncService.openSync()) { return syncService; } return null; } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#getFileListingService() */ @Override public FileListingService getFileListingService() { return new FileListingService(this); } @Override public RawImage getScreenshot() throws TimeoutException, AdbCommandRejectedException, IOException { return getScreenshot(0, TimeUnit.MILLISECONDS); } @Override public RawImage getScreenshot(long timeout, TimeUnit unit) throws TimeoutException, AdbCommandRejectedException, IOException { return AdbHelper.getFrameBuffer(AndroidDebugBridge.getSocketAddress(), this, timeout, unit); } @Override public void startScreenRecorder(String remoteFilePath, ScreenRecorderOptions options, IShellOutputReceiver receiver) throws TimeoutException, AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException { executeShellCommand(getScreenRecorderCommand(remoteFilePath, options), receiver, 0, null); } @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) static String getScreenRecorderCommand(@NonNull String remoteFilePath, @NonNull ScreenRecorderOptions options) { StringBuilder sb = new StringBuilder(); sb.append("screenrecord"); sb.append(' '); if (options.width > 0 && options.height > 0) { sb.append("--size "); sb.append(options.width); sb.append('x'); sb.append(options.height); sb.append(' '); } if (options.bitrateMbps > 0) { sb.append("--bit-rate "); sb.append(options.bitrateMbps * 1000000); sb.append(' '); } if (options.timeLimit > 0) { sb.append("--time-limit "); long seconds = TimeUnit.SECONDS.convert(options.timeLimit, options.timeLimitUnits); if (seconds > 180) { seconds = 180; } sb.append(seconds); sb.append(' '); } sb.append(remoteFilePath); return sb.toString(); } @Override public void executeShellCommand(String command, IShellOutputReceiver receiver) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), command, this, receiver, DdmPreferences.getTimeOut()); } @Override public void executeShellCommand(String command, IShellOutputReceiver receiver, int maxTimeToOutputResponse) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), command, this, receiver, maxTimeToOutputResponse); } @Override public void executeShellCommand(String command, IShellOutputReceiver receiver, long maxTimeToOutputResponse, TimeUnit maxTimeUnits) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), command, this, receiver, maxTimeToOutputResponse, maxTimeUnits); } @Override public void runEventLogService(LogReceiver receiver) throws TimeoutException, AdbCommandRejectedException, IOException { AdbHelper.runEventLogService(AndroidDebugBridge.getSocketAddress(), this, receiver); } @Override public void runLogService(String logname, LogReceiver receiver) throws TimeoutException, AdbCommandRejectedException, IOException { AdbHelper.runLogService(AndroidDebugBridge.getSocketAddress(), this, logname, receiver); } @Override public void createForward(int localPort, int remotePort) throws TimeoutException, AdbCommandRejectedException, IOException { AdbHelper.createForward(AndroidDebugBridge.getSocketAddress(), this, String.format("tcp:%d", localPort), //$NON-NLS-1$ String.format("tcp:%d", remotePort)); //$NON-NLS-1$ } @Override public void createForward(int localPort, String remoteSocketName, DeviceUnixSocketNamespace namespace) throws TimeoutException, AdbCommandRejectedException, IOException { AdbHelper.createForward(AndroidDebugBridge.getSocketAddress(), this, String.format("tcp:%d", localPort), //$NON-NLS-1$ String.format("%s:%s", namespace.getType(), remoteSocketName)); //$NON-NLS-1$ } @Override public void removeForward(int localPort, int remotePort) throws TimeoutException, AdbCommandRejectedException, IOException { AdbHelper.removeForward(AndroidDebugBridge.getSocketAddress(), this, String.format("tcp:%d", localPort), //$NON-NLS-1$ String.format("tcp:%d", remotePort)); //$NON-NLS-1$ } @Override public void removeForward(int localPort, String remoteSocketName, DeviceUnixSocketNamespace namespace) throws TimeoutException, AdbCommandRejectedException, IOException { AdbHelper.removeForward(AndroidDebugBridge.getSocketAddress(), this, String.format("tcp:%d", localPort), //$NON-NLS-1$ String.format("%s:%s", namespace.getType(), remoteSocketName)); //$NON-NLS-1$ } Device(DeviceMonitor monitor, String serialNumber, DeviceState deviceState) { mMonitor = monitor; mSerialNumber = serialNumber; mState = deviceState; } DeviceMonitor getMonitor() { return mMonitor; } @Override public boolean hasClients() { synchronized (mClients) { return !mClients.isEmpty(); } } @Override public Client[] getClients() { synchronized (mClients) { return mClients.toArray(new Client[mClients.size()]); } } @Override public Client getClient(String applicationName) { synchronized (mClients) { for (Client c : mClients) { if (applicationName.equals(c.getClientData().getClientDescription())) { return c; } } } return null; } void addClient(Client client) { synchronized (mClients) { mClients.add(client); } addClientInfo(client); } List<Client> getClientList() { return mClients; } void clearClientList() { synchronized (mClients) { mClients.clear(); } clearClientInfo(); } /** * Removes a {@link Client} from the list. * @param client the client to remove. * @param notify Whether or not to notify the listeners of a change. */ void removeClient(Client client, boolean notify) { mMonitor.addPortToAvailableList(client.getDebuggerListenPort()); synchronized (mClients) { mClients.remove(client); } if (notify) { mMonitor.getServer().deviceChanged(this, CHANGE_CLIENT_LIST); } removeClientInfo(client); } /** Sets the socket channel on which a track-jdwp command for this device has been sent. */ void setClientMonitoringSocket(@NonNull SocketChannel socketChannel) { mSocketChannel = socketChannel; } /** * Returns the channel on which responses to the track-jdwp command will be available if it * has been set, null otherwise. The channel is set via {@link #setClientMonitoringSocket(SocketChannel)}, * which is usually invoked when the device goes online. */ @Nullable SocketChannel getClientMonitoringSocket() { return mSocketChannel; } void update(int changeMask) { mMonitor.getServer().deviceChanged(this, changeMask); } void update(Client client, int changeMask) { mMonitor.getServer().clientChanged(client, changeMask); updateClientInfo(client, changeMask); } void setMountingPoint(String name, String value) { mMountPoints.put(name, value); } private void addClientInfo(Client client) { ClientData cd = client.getClientData(); setClientInfo(cd.getPid(), cd.getClientDescription()); } private void updateClientInfo(Client client, int changeMask) { if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) { addClientInfo(client); } } private void removeClientInfo(Client client) { int pid = client.getClientData().getPid(); mClientInfo.remove(pid); } private void clearClientInfo() { mClientInfo.clear(); } private void setClientInfo(int pid, String pkgName) { if (pkgName == null) { pkgName = UNKNOWN_PACKAGE; } mClientInfo.put(pid, pkgName); } @Override public String getClientName(int pid) { String pkgName = mClientInfo.get(pid); return pkgName == null ? UNKNOWN_PACKAGE : pkgName; } @Override public void pushFile(String local, String remote) throws IOException, AdbCommandRejectedException, TimeoutException, SyncException { SyncService sync = null; try { String targetFileName = getFileName(local); Log.d(targetFileName, String.format("Uploading %1$s onto device '%2$s'", targetFileName, getSerialNumber())); sync = getSyncService(); if (sync != null) { String message = String.format("Uploading file onto device '%1$s'", getSerialNumber()); Log.d(LOG_TAG, message); sync.pushFile(local, remote, SyncService.getNullProgressMonitor()); } else { throw new IOException("Unable to open sync connection!"); } } catch (TimeoutException e) { Log.e(LOG_TAG, "Error during Sync: timeout."); throw e; } catch (SyncException e) { Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); throw e; } catch (IOException e) { Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); throw e; } finally { if (sync != null) { sync.close(); } } } @Override public void pullFile(String remote, String local) throws IOException, AdbCommandRejectedException, TimeoutException, SyncException { SyncService sync = null; try { String targetFileName = getFileName(remote); Log.d(targetFileName, String.format("Downloading %1$s from device '%2$s'", targetFileName, getSerialNumber())); sync = getSyncService(); if (sync != null) { String message = String.format("Downloading file from device '%1$s'", getSerialNumber()); Log.d(LOG_TAG, message); sync.pullFile(remote, local, SyncService.getNullProgressMonitor()); } else { throw new IOException("Unable to open sync connection!"); } } catch (TimeoutException e) { Log.e(LOG_TAG, "Error during Sync: timeout."); throw e; } catch (SyncException e) { Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); throw e; } catch (IOException e) { Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); throw e; } finally { if (sync != null) { sync.close(); } } } @Override public String installPackage(String packageFilePath, boolean reinstall, String... extraArgs) throws InstallException { try { String remoteFilePath = syncPackageToDevice(packageFilePath); String result = installRemotePackage(remoteFilePath, reinstall, extraArgs); removeRemotePackage(remoteFilePath); return result; } catch (IOException e) { throw new InstallException(e); } catch (AdbCommandRejectedException e) { throw new InstallException(e); } catch (TimeoutException e) { throw new InstallException(e); } catch (SyncException e) { throw new InstallException(e); } } @Override public void installPackages(List<String> apkFilePaths, int timeOutInMs, boolean reinstall, String... extraArgs) throws InstallException { assert(!apkFilePaths.isEmpty()); if (getApiLevel() < 21) { Log.w("Internal error : installPackages invoked with device < 21 for %s", Joiner.on(",").join(apkFilePaths)); if (apkFilePaths.size() == 1) { installPackage(apkFilePaths.get(0), reinstall, extraArgs); return; } Log.e("Internal error : installPackages invoked with device < 21 for multiple APK : %s", Joiner.on(",").join(apkFilePaths)); throw new InstallException( "Internal error : installPackages invoked with device < 21 for multiple APK : " + Joiner.on(",").join(apkFilePaths)); } String mainPackageFilePath = apkFilePaths.get(0); Log.d(mainPackageFilePath, String.format("Uploading main %1$s and %2$s split APKs onto device '%3$s'", mainPackageFilePath, Joiner.on(',').join(apkFilePaths), getSerialNumber())); try { // create a installation session. List<String> extraArgsList = extraArgs != null ? ImmutableList.copyOf(extraArgs) : ImmutableList.<String>of(); String sessionId = createMultiInstallSession(apkFilePaths, extraArgsList, reinstall); if (sessionId == null) { Log.d(mainPackageFilePath, "Failed to establish session, quit installation"); throw new InstallException("Failed to establish session"); } Log.d(mainPackageFilePath, String.format("Established session id=%1$s", sessionId)); // now upload each APK in turn. int index = 0; boolean allUploadSucceeded = true; while (allUploadSucceeded && index < apkFilePaths.size()) { allUploadSucceeded = uploadAPK(sessionId, apkFilePaths.get(index), index++); } // if all files were upload successfully, commit otherwise abandon the installation. String command = allUploadSucceeded ? "pm install-commit " + sessionId : "pm install-abandon " + sessionId; InstallReceiver receiver = new InstallReceiver(); executeShellCommand(command, receiver, timeOutInMs, TimeUnit.MILLISECONDS); String errorMessage = receiver.getErrorMessage(); if (errorMessage != null) { String message = String.format("Failed to finalize session : %1$s", errorMessage); Log.e(mainPackageFilePath, message); throw new InstallException(message); } // in case not all files were upload and we abandoned the install, make sure to // notifier callers. if (!allUploadSucceeded) { throw new InstallException("Unable to upload some APKs"); } } catch (TimeoutException e) { Log.e(LOG_TAG, "Error during Sync: timeout."); throw new InstallException(e); } catch (IOException e) { Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); throw new InstallException(e); } catch (AdbCommandRejectedException e) { throw new InstallException(e); } catch (ShellCommandUnresponsiveException e) { Log.e(LOG_TAG, String.format("Error during shell execution: %1$s", e.getMessage())); throw new InstallException(e); } } /** * Implementation of {@link com.android.ddmlib.MultiLineReceiver} that can receive a * Success message from ADB followed by a session ID. */ private static class MultiInstallReceiver extends MultiLineReceiver { private static final Pattern successPattern = Pattern.compile("Success: .*\\[(\\d*)\\]"); @Nullable String sessionId = null; @Override public boolean isCancelled() { return false; } @Override public void processNewLines(String[] lines) { for (String line : lines) { Matcher matcher = successPattern.matcher(line); if (matcher.matches()) { sessionId = matcher.group(1); } } } @Nullable public String getSessionId() { return sessionId; } } @Nullable private String createMultiInstallSession(List<String> apkFileNames, @NonNull Collection<String> extraArgs, boolean reinstall) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { List<File> apkFiles = Lists.transform(apkFileNames, new Function<String, File>() { @Override public File apply(String input) { return new File(input); } }); long totalFileSize = 0L; for (File apkFile : apkFiles) { if (apkFile.exists() && apkFile.isFile()) { totalFileSize += apkFile.length(); } else { throw new IllegalArgumentException(apkFile.getAbsolutePath() + " is not a file"); } } StringBuilder parameters = new StringBuilder(); if (reinstall) { parameters.append(("-r ")); } parameters.append(Joiner.on(' ').join(extraArgs)); MultiInstallReceiver receiver = new MultiInstallReceiver(); String cmd = String.format("pm install-create %1$s -S %2$d", parameters.toString(), totalFileSize); executeShellCommand(cmd, receiver, DdmPreferences.getTimeOut()); return receiver.getSessionId(); } private static final CharMatcher UNSAFE_PM_INSTALL_SESSION_SPLIT_NAME_CHARS = CharMatcher.inRange('a','z').or(CharMatcher.inRange('A','Z')) .or(CharMatcher.anyOf("_-")).negate(); private boolean uploadAPK(final String sessionId, String apkFilePath, int uniqueId) { Log.d(sessionId, String.format("Uploading APK %1$s ", apkFilePath)); File fileToUpload = new File(apkFilePath); if (!fileToUpload.exists()) { Log.e(sessionId, String.format("File not found: %1$s", apkFilePath)); return false; } if (fileToUpload.isDirectory()) { Log.e(sessionId, String.format("Directory upload not supported: %1$s", apkFilePath)); return false; } String baseName = fileToUpload.getName().lastIndexOf('.') != -1 ? fileToUpload.getName().substring(0, fileToUpload.getName().lastIndexOf('.')) : fileToUpload.getName(); baseName = UNSAFE_PM_INSTALL_SESSION_SPLIT_NAME_CHARS.replaceFrom(baseName, '_'); String command = String.format("pm install-write -S %d %s %d_%s -", fileToUpload.length(), sessionId, uniqueId, baseName); Log.d(sessionId, String.format("Executing : %1$s", command)); InputStream inputStream = null; try { inputStream = new BufferedInputStream(new FileInputStream(fileToUpload)); InstallReceiver receiver = new InstallReceiver(); AdbHelper.executeRemoteCommand(AndroidDebugBridge.getSocketAddress(), AdbHelper.AdbService.EXEC, command, this, receiver, DdmPreferences.getTimeOut(), TimeUnit.MILLISECONDS, inputStream); if (receiver.getErrorMessage() != null) { Log.e(sessionId, String.format("Error while uploading %1$s : %2$s", fileToUpload.getName(), receiver.getErrorMessage())); } else { Log.d(sessionId, String.format("Successfully uploaded %1$s", fileToUpload.getName())); } return receiver.getErrorMessage() == null; } catch (Exception e) { Log.e(sessionId, e); return false; } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { Log.e(sessionId, e); } } } } @Override public String syncPackageToDevice(String localFilePath) throws IOException, AdbCommandRejectedException, TimeoutException, SyncException { SyncService sync = null; try { String packageFileName = getFileName(localFilePath); String remoteFilePath = String.format("/data/local/tmp/%1$s", packageFileName); //$NON-NLS-1$ Log.d(packageFileName, String.format("Uploading %1$s onto device '%2$s'", packageFileName, getSerialNumber())); sync = getSyncService(); if (sync != null) { String message = String.format("Uploading file onto device '%1$s'", getSerialNumber()); Log.d(LOG_TAG, message); sync.pushFile(localFilePath, remoteFilePath, SyncService.getNullProgressMonitor()); } else { throw new IOException("Unable to open sync connection!"); } return remoteFilePath; } catch (TimeoutException e) { Log.e(LOG_TAG, "Error during Sync: timeout."); throw e; } catch (SyncException e) { Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); throw e; } catch (IOException e) { Log.e(LOG_TAG, String.format("Error during Sync: %1$s", e.getMessage())); throw e; } finally { if (sync != null) { sync.close(); } } } /** * Helper method to retrieve the file name given a local file path * @param filePath full directory path to file * @return {@link String} file name */ private static String getFileName(String filePath) { return new File(filePath).getName(); } @Override public String installRemotePackage(String remoteFilePath, boolean reinstall, String... extraArgs) throws InstallException { try { InstallReceiver receiver = new InstallReceiver(); StringBuilder optionString = new StringBuilder(); if (reinstall) { optionString.append("-r "); } if (extraArgs != null) { optionString.append(Joiner.on(' ').join(extraArgs)); } String cmd = String.format("pm install %1$s \"%2$s\"", optionString.toString(), remoteFilePath); executeShellCommand(cmd, receiver, INSTALL_TIMEOUT_MINUTES, TimeUnit.MINUTES); return receiver.getErrorMessage(); } catch (TimeoutException e) { throw new InstallException(e); } catch (AdbCommandRejectedException e) { throw new InstallException(e); } catch (ShellCommandUnresponsiveException e) { throw new InstallException(e); } catch (IOException e) { throw new InstallException(e); } } @Override public void removeRemotePackage(String remoteFilePath) throws InstallException { try { executeShellCommand(String.format("rm \"%1$s\"", remoteFilePath), new NullOutputReceiver(), INSTALL_TIMEOUT_MINUTES, TimeUnit.MINUTES); } catch (IOException e) { throw new InstallException(e); } catch (TimeoutException e) { throw new InstallException(e); } catch (AdbCommandRejectedException e) { throw new InstallException(e); } catch (ShellCommandUnresponsiveException e) { throw new InstallException(e); } } @Override public String uninstallPackage(String packageName) throws InstallException { try { InstallReceiver receiver = new InstallReceiver(); executeShellCommand("pm uninstall " + packageName, receiver, INSTALL_TIMEOUT_MINUTES, TimeUnit.MINUTES); return receiver.getErrorMessage(); } catch (TimeoutException e) { throw new InstallException(e); } catch (AdbCommandRejectedException e) { throw new InstallException(e); } catch (ShellCommandUnresponsiveException e) { throw new InstallException(e); } catch (IOException e) { throw new InstallException(e); } } /* * (non-Javadoc) * @see com.android.ddmlib.IDevice#reboot() */ @Override public void reboot(String into) throws TimeoutException, AdbCommandRejectedException, IOException { AdbHelper.reboot(into, AndroidDebugBridge.getSocketAddress(), this); } @Override public Integer getBatteryLevel() throws TimeoutException, AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException { // use default of 5 minutes return getBatteryLevel(5 * 60 * 1000); } @Override public Integer getBatteryLevel(long freshnessMs) throws TimeoutException, AdbCommandRejectedException, IOException, ShellCommandUnresponsiveException { Future<Integer> futureBattery = getBattery(freshnessMs, TimeUnit.MILLISECONDS); try { return futureBattery.get(); } catch (InterruptedException e) { return null; } catch (ExecutionException e) { return null; } } @NonNull @Override public Future<Integer> getBattery() { return getBattery(5, TimeUnit.MINUTES); } @NonNull @Override public Future<Integer> getBattery(long freshnessTime, @NonNull TimeUnit timeUnit) { return mBatteryFetcher.getBattery(freshnessTime, timeUnit); } @NonNull @Override public List<String> getAbis() { /* Try abiList (implemented in L onwards) otherwise fall back to abi and abi2. */ String abiList = getProperty(IDevice.PROP_DEVICE_CPU_ABI_LIST); if(abiList != null) { return Lists.newArrayList(abiList.split(",")); } else { List<String> abis = Lists.newArrayListWithExpectedSize(2); String abi = getProperty(IDevice.PROP_DEVICE_CPU_ABI); if (abi != null) { abis.add(abi); } abi = getProperty(IDevice.PROP_DEVICE_CPU_ABI2); if (abi != null) { abis.add(abi); } return abis; } } @Override public int getDensity() { String densityValue = getProperty(IDevice.PROP_DEVICE_DENSITY); if (densityValue != null) { try { return Integer.parseInt(densityValue); } catch (NumberFormatException e) { return -1; } } return -1; } @Override public String getLanguage() { return getProperties().get(IDevice.PROP_DEVICE_LANGUAGE); } @Override public String getRegion() { return getProperty(IDevice.PROP_DEVICE_REGION); } }