// /* // * Copyright (C) 2015 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.tools.fd.client; // // import static com.android.tools.fd.client.InstantRunArtifactType.DEX; // import static com.android.tools.fd.client.InstantRunArtifactType.SPLIT; // import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_EOF; // import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_PATCHES; // import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_PING; // import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_RESTART_ACTIVITY; // import static com.android.tools.fd.common.ProtocolConstants.MESSAGE_SHOW_TOAST; // import static com.android.tools.fd.common.ProtocolConstants.PROTOCOL_IDENTIFIER; // import static com.android.tools.fd.common.ProtocolConstants.PROTOCOL_VERSION; // import static com.android.tools.fd.runtime.Paths.getDeviceIdFolder; // // import com.android.annotations.NonNull; // import com.android.annotations.Nullable; // import com.android.ddmlib.AdbCommandRejectedException; // import com.android.ddmlib.CollectingOutputReceiver; // import com.android.ddmlib.IDevice; // import com.android.ddmlib.ShellCommandUnresponsiveException; // import com.android.ddmlib.SyncException; // import com.android.ddmlib.TimeoutException; // import com.android.tools.fd.runtime.ApplicationPatch; // import com.android.tools.fd.runtime.Paths; // import com.android.utils.ILogger; // import com.android.utils.NullLogger; // import com.google.common.annotations.VisibleForTesting; // import com.google.common.base.CharMatcher; // import com.google.common.base.Charsets; // import com.google.common.base.Joiner; // import com.google.common.base.Splitter; // import com.google.common.base.Throwables; // import com.google.common.collect.Lists; // import com.google.common.collect.Sets; // import com.google.common.io.Files; // // import java.io.DataInputStream; // import java.io.DataOutputStream; // import java.io.File; // import java.io.IOException; // import java.net.Socket; // import java.util.ArrayList; // import java.util.List; // import java.util.Locale; // import java.util.Set; // // public class InstantRunClient { // public static final String BROKEN_RUN_AS = "run-as command broken on this device"; // // private static final String LOCAL_HOST = "127.0.0.1"; // // /** Local port on the desktop machine via which we tunnel to the Android device */ // private static final int DEFAULT_LOCAL_PORT = 46622; // Note: just a random number, hopefully it is a free/available port on the host // // /** Prefix for classes.dex files */ // private static final String CLASSES_DEX_PREFIX = "classes"; // // /** Suffix for classes.dex files */ // private static final String CLASSES_DEX_SUFFIX = ".dex"; // // // /** // * Instead of writing to the data folder, we can read/write to a local temp file instead. // * This is required because some devices (Samsung Galaxy Edge atleast) doesn't allow access into the package folder even with run-as. // */ // public static final boolean USE_BUILD_ID_TEMP_FILE = // !Boolean.getBoolean("instantrun.use_datadir"); // // @NonNull // private final ILogger mLogger; // // @NonNull // private final String mPackageName; // // private final long mToken; // private final int mLocalPort; // // public InstantRunClient( // @NonNull String packageName, // @NonNull ILogger logger, // long token) { // this(packageName, logger, token, DEFAULT_LOCAL_PORT); // } // // @VisibleForTesting // public InstantRunClient( // @NonNull String packageName, // @NonNull ILogger logger, // long token, // int port) { // mPackageName = packageName; // mLogger = logger; // mToken = token; // mLocalPort = port; // } // // @NonNull // private static String copyToDeviceScratchFile(@NonNull IDevice device, @NonNull String pkgName, // @NonNull String contents) // throws IOException, AdbCommandRejectedException, SyncException, TimeoutException { // // File local = null; // try { // local = createTempFile("data", "fdr"); // Files.write(contents.getBytes(Charsets.UTF_8), local); // return copyToDeviceScratchFile(device, pkgName, local); // } finally { // if (local != null) { // //noinspection ResultOfMethodCallIgnored // local.delete(); // } // } // } // // @NonNull // private static String copyToDeviceScratchFile(@NonNull IDevice device, @NonNull String pkgName, // @NonNull File local) // throws IOException, AdbCommandRejectedException, SyncException, TimeoutException { // String remoteTmpBuildId = Paths.DEVICE_TEMP_DIR + "/" + pkgName + "-data.fdr"; // device.pushFile(local.getAbsolutePath(), remoteTmpBuildId); // return remoteTmpBuildId; // } // // private static int getMaxDexFileNumber(@NonNull String fileListing) { // int max = -1; // // for (String name : Splitter.on(CharMatcher.WHITESPACE).omitEmptyStrings() // .splitToList(fileListing)) { // if (name.startsWith(CLASSES_DEX_PREFIX) && name.endsWith(CLASSES_DEX_SUFFIX)) { // String middle = name.substring(CLASSES_DEX_PREFIX.length(), // name.length() - CLASSES_DEX_SUFFIX.length()); // try { // int version = Integer.decode(middle); // if (version > max) { // max = version; // } // } catch (NumberFormatException ignore) { // } // } // } // // return max; // } // // private static File createTempFile(String prefix, String suffix) throws IOException { // //noinspection SSBasedInspection Tests use this in tools/base // File file = File.createTempFile(prefix, suffix); // file.deleteOnExit(); // return file; // } // // /** // * Attempts to connect to a given device and sees if an instant run enabled app is running // * there. // */ // @NonNull // public AppState getAppState(@NonNull IDevice device) throws IOException { // return talkToApp(device, // new Communicator<AppState>() { // @Override // public AppState communicate(@NonNull DataInputStream input, // @NonNull DataOutputStream output) throws // IOException { // output.writeInt(MESSAGE_PING); // boolean foreground = input.readBoolean(); // Wait for "pong" // mLogger.info( // "Ping sent and replied successfully, application seems to be running. Foreground=" // + foreground); // return foreground ? AppState.FOREGROUND : AppState.BACKGROUND; // } // }); // } // // @NonNull // private <T> T talkToApp(@NonNull IDevice device, @NonNull Communicator<T> communicator) // throws IOException { // // try { // device.createForward(mLocalPort, mPackageName, // IDevice.DeviceUnixSocketNamespace.ABSTRACT); // } // catch (TimeoutException | AdbCommandRejectedException e) { // throw new IOException(e); // } // // try { // return talkToAppWithinPortForward(communicator, mLocalPort); // } finally { // try { // device.removeForward(mLocalPort, mPackageName, // IDevice.DeviceUnixSocketNamespace.ABSTRACT); // } // catch (IOException | TimeoutException | AdbCommandRejectedException e) { // // we don't worry that much about failures while removing port forwarding // mLogger.warning("Exception while removing port forward: " + e); // } // } // } // // private static <T> T talkToAppWithinPortForward(@NonNull Communicator<T> communicator, // int localPort) throws IOException { // try (Socket socket = new Socket(LOCAL_HOST, localPort)) { // try (DataInputStream input = new DataInputStream(socket.getInputStream()); // DataOutputStream output = new DataOutputStream(socket.getOutputStream())) { // output.writeLong(PROTOCOL_IDENTIFIER); // output.writeInt(PROTOCOL_VERSION); // // socket.setSoTimeout(2 * 1000); // Allow up to 2 seconds before timing out // int version = input.readInt(); // if (version != PROTOCOL_VERSION) { // String msg = String.format(Locale.US, // "Client and server protocol versions don't match (%1$d != %2$d)", // version, PROTOCOL_VERSION); // throw new IOException(msg); // } // // socket.setSoTimeout(communicator.getTimeout()); // T value = communicator.communicate(input, output); // // output.writeInt(MESSAGE_EOF); // // return value; // } // } // } // // public void showToast(@NonNull IDevice device, @NonNull final String message) // throws IOException { // talkToApp(device, new Communicator<Boolean>() { // @Override // public Boolean communicate(@NonNull DataInputStream input, // @NonNull DataOutputStream output) throws IOException { // output.writeInt(MESSAGE_SHOW_TOAST); // output.writeUTF(message); // return false; // } // }); // } // // /** // * Restart the activity on this device, if it's running and is in the foreground. // */ // public void restartActivity(@NonNull IDevice device) throws IOException { // AppState appState = getAppState(device); // if (appState == AppState.FOREGROUND || appState == AppState.BACKGROUND) { // talkToApp(device, new Communicator<Void>() { // @Override // public Void communicate(@NonNull DataInputStream input, // @NonNull DataOutputStream output) throws IOException { // output.writeInt(MESSAGE_RESTART_ACTIVITY); // writeToken(output); // return null; // } // }); // } // } // // public UpdateMode pushPatches(@NonNull IDevice device, // @NonNull final InstantRunBuildInfo buildInfo, // @NonNull UpdateMode updateMode, // final boolean isRestartActivity, // final boolean isShowToastEnabled) throws InstantRunPushFailedException, IOException { // if (!buildInfo.canHotswap()) { // updateMode = updateMode.combine(UpdateMode.COLD_SWAP); // } // // List<FileTransfer> files = Lists.newArrayList(); // // boolean appInForeground; // boolean appRunning; // try { // AppState appState = getAppState(device); // appInForeground = appState == AppState.FOREGROUND; // appRunning = appState == AppState.FOREGROUND || appState == AppState.BACKGROUND; // } catch (IOException e) { // appInForeground = appRunning = false; // } // // List<InstantRunArtifact> artifacts = buildInfo.getArtifacts(); // mLogger.info("Artifacts from build-info.xml: " + Joiner.on("-").join(artifacts)); // for (InstantRunArtifact artifact : artifacts) { // InstantRunArtifactType type = artifact.type; // File file = artifact.file; // switch (type) { // case MAIN: // // Should never be used with this method: APKs should be pushed by DeployApkTask // assert false : artifact; // break; // case SPLIT_MAIN: // // Should only be used here when we're doing a *compatible* // // resource swap and also got an APK for split. Ignore here. // continue; // case SPLIT: // // Should never be used with this method: APK splits should // // be pushed by SplitApkDeployTask // assert false : artifact; // break; // case RESOURCES: // updateMode = updateMode.combine(UpdateMode.WARM_SWAP); // files.add(FileTransfer.createResourceFile(file)); // break; // case DEX: // String name = file.getParentFile().getName() + "-" + file.getName(); // files.add(FileTransfer.createSliceDex(file, name)); // break; // case RELOAD_DEX: // if (appInForeground) { // files.add(FileTransfer.createHotswapPatch(file)); // } else { // // Gradle created a reload dex, but the app is no longer running. // // If it created a cold swap artifact, we can use it; otherwise we're out of luck. // if (!buildInfo.hasOneOf(DEX, SPLIT)) { // throw new InstantRunPushFailedException("Can't apply hot swap patch: app is no longer running"); // } // } // break; // default: // assert false : artifact; // } // } // // boolean needRestart; // // if (appRunning) { // List<ApplicationPatch> changes = new ArrayList<ApplicationPatch>(files.size()); // for (FileTransfer file : files) { // try { // changes.add(file.getPatch()); // } // catch (IOException e) { // throw new InstantRunPushFailedException("Could not read file " + file); // } // } // updateMode = pushPatches(device, buildInfo.getTimeStamp(), changes, updateMode, isRestartActivity, // isShowToastEnabled); // // needRestart = false; // if (!appInForeground || !buildInfo.canHotswap()) { // stopApp(device, false /* sendChangeBroadcast */); // needRestart = true; // } // } // else { // // Push to data directory // pushFiles(files, device, buildInfo.getTimeStamp()); // needRestart = true; // } // // logFilesPushed(files, needRestart); // // if (needRestart) { // // TODO: this should not need to be explicit, but leaving in to ensure no behaviour change. // return UpdateMode.COLD_SWAP; // } // return updateMode; // } // // public UpdateMode pushPatches(@NonNull IDevice device, // @NonNull final String buildId, // @NonNull final List<ApplicationPatch> changes, // @NonNull UpdateMode updateMode, // final boolean isRestartActivity, // final boolean isShowToastEnabled) throws IOException { // if (changes.isEmpty() || updateMode == UpdateMode.NO_CHANGES) { // // Sync the build id to the device; Gradle might rev the build id even when there are no changes, // // and we need to make sure that the device id reflects this new build id, or the next // // build will discover different id's and will conclude that it needs to do a full rebuild // transferLocalIdToDeviceId(device, buildId); // // return UpdateMode.NO_CHANGES; // } // // if (updateMode == UpdateMode.HOT_SWAP && isRestartActivity) { // updateMode = updateMode.combine(UpdateMode.WARM_SWAP); // } // // final UpdateMode updateMode1 = updateMode; // talkToApp(device, new Communicator<Boolean>() { // @Override // public Boolean communicate(@NonNull DataInputStream input, // @NonNull DataOutputStream output) throws IOException { // output.writeInt(MESSAGE_PATCHES); // writeToken(output); // ApplicationPatchUtil.write(output, changes, updateMode1); // // // Let the app know whether it should show toasts // output.writeBoolean(isShowToastEnabled); // // // Finally read a boolean back from the other side; this has the net effect of // // waiting until applying/verifying code on the other side is done. (It doesn't // // count the actual restart time, but for activity restarts it's typically instant, // // and for cold starts we have no easy way to handle it (the process will die and a // // new process come up; to measure that we'll need to work a lot harder.) // input.readBoolean(); // // return false; // } // // @Override // int getTimeout() { // return 8000; // allow up to 8 seconds for resource push // } // }); // // transferLocalIdToDeviceId(device, buildId); // // return updateMode; // } // // /** // * Called after a build & successful push to device: updates the build id on the device to // * whatever the build id was assigned by Gradle. // * // * @param device the device to push to // */ // public void transferLocalIdToDeviceId(@NonNull IDevice device, @NonNull String buildId) { // transferBuildIdToDevice(device, buildId, mPackageName, mLogger); // } // // // Note: This method can be called even if IR is turned off, as even when IR is off, we want to // // trash any existing build ids saved on the device. // public static void transferBuildIdToDevice(@NonNull IDevice device, // @NonNull String buildId, // @NonNull String applicationId, // @Nullable ILogger logger) { // if (logger == null) { // logger = new NullLogger(); // } // // try { // if (USE_BUILD_ID_TEMP_FILE) { // String remoteIdFile = getDeviceIdFolder(applicationId); // //noinspection SSBasedInspection This should work // File local = File.createTempFile("build-id", "txt"); // local.deleteOnExit(); // Files.write(buildId, local, Charsets.UTF_8); // device.pushFile(local.getPath(), remoteIdFile); // } else { // String remote = copyToDeviceScratchFile(device, applicationId, buildId); // String dataDir = Paths.getDataDirectory(applicationId); // // // We used to do this here: // //String cmd = "run-as " + pkg + " mkdir -p " + dataDir + "; run-as " + pkg + " cp " + remote + " " + dataDir + "/" + BUILD_ID_TXT; // // but it turns out "cp" is missing on API 15! Let's use cat and sh instead which seems to be available everywhere. // // (Note: echo is not, it's missing on API 19.) // String cmd = "run-as " + applicationId + " mkdir -p " + dataDir + "; cat " + remote // + " | run-as " + applicationId + " sh -c 'cat > " + dataDir + "/" // + Paths.BUILD_ID_TXT + "'"; // CollectingOutputReceiver receiver = new CollectingOutputReceiver(); // device.executeShellCommand(cmd, receiver); // String output = receiver.getOutput(); // if (!output.trim().isEmpty()) { // logger.warning("Unexpected shell output: " + output); // } // } // } catch (IOException ioe) { // logger.warning("Couldn't write build id file: %s", ioe); // } catch (AdbCommandRejectedException | TimeoutException | SyncException | ShellCommandUnresponsiveException e) { // logger.warning("%s", Throwables.getStackTraceAsString(e)); // } // } // // /** // * Returns the build timestamp on the device, or null if it is not found. // */ // @Nullable // public String getDeviceBuildTimestamp(@NonNull IDevice device) { // return getDeviceBuildTimestamp(device, mPackageName, mLogger); // } // // @Nullable // public static String getDeviceBuildTimestamp(@NonNull IDevice device, @NonNull String packageName, @NonNull ILogger logger) { // try { // if (USE_BUILD_ID_TEMP_FILE) { // String remoteIdFile = getDeviceIdFolder(packageName); // File localIdFile = createTempFile("build-id", "txt"); // try { // device.pullFile(remoteIdFile, localIdFile.getPath()); // return Files.toString(localIdFile, Charsets.UTF_8).trim(); // } catch (SyncException ignore) { // return null; // } finally { // //noinspection ResultOfMethodCallIgnored // localIdFile.delete(); // } // } else { // String remoteIdFile = Paths.getDataDirectory(packageName) + "/" // + Paths.BUILD_ID_TXT; // CollectingOutputReceiver receiver = new CollectingOutputReceiver(); // device.executeShellCommand("run-as " + packageName + " cat " + remoteIdFile, // receiver); // String output = receiver.getOutput().trim(); // String id; // if (output.contains(":")) { // cat: command not found, cat: permission denied etc // if (output.startsWith(remoteIdFile)) { // // /data/data/my.pkg.path/files/instant-run/build-id.txt: No such file or directory // return null; // } // // on a user device, we cannot pull from a path where the segments aren't readable (I think this is a ddmlib limitation) // // So we first copy to /data/local/tmp and pull from there.. // String remoteTmpFile = "/data/local/tmp/build-id.txt"; // device.executeShellCommand("cp " + remoteIdFile + " " + remoteTmpFile, // receiver); // output = receiver.getOutput().trim(); // if (!output.isEmpty()) { // logger.info(output); // } // File localIdFile = createTempFile("build-id", "txt"); // device.pullFile(remoteTmpFile, localIdFile.getPath()); // id = Files.toString(localIdFile, Charsets.UTF_8).trim(); // //noinspection ResultOfMethodCallIgnored // localIdFile.delete(); // } else { // id = output; // } // return id; // } // } catch (IOException ignore) { // } catch (AdbCommandRejectedException | SyncException | TimeoutException | ShellCommandUnresponsiveException e) { // logger.warning("%s", Throwables.getStackTraceAsString(e)); // } // // return null; // } // // private void writeToken(@NonNull DataOutputStream output) throws IOException { // output.writeLong(mToken); // } // // /** // * Transfer the file as a slice/sharded dex file. This means // * that its remote path should be the slice name, in the dex // * directory. // */ // public static final int TRANSFER_MODE_SLICE = 1; // // /** // * Transfer the file as a hotswap overlay file. This means // * that its remote path should be a temporary file. // */ // public static final int TRANSFER_MODE_HOTSWAP = 3; // // /** // * Transfer the file as a resource file. This means that it // * should be written to the inactive resource file section // * in the app data directory. // */ // public static final int TRANSFER_MODE_RESOURCES = 4; // // /** // * File to be transferred to the device. For use with // * {@link #pushFiles(List, IDevice, String)} // */ // public static class FileTransfer { // public final int mode; // public final File source; // public final String name; // // public FileTransfer(int mode, @NonNull File source, @NonNull String name) { // this.mode = mode; // this.source = source; // this.name = name; // } // // @NonNull // public static FileTransfer createSliceDex(@NonNull File source, @NonNull String name) { // return new FileTransfer(TRANSFER_MODE_SLICE, source, name); // } // // @NonNull // public static FileTransfer createResourceFile(@NonNull File source) { // return new FileTransfer(TRANSFER_MODE_RESOURCES, source, Paths.RESOURCE_FILE_NAME); // } // // @NonNull // public static FileTransfer createHotswapPatch(@NonNull File source) { // return new FileTransfer(TRANSFER_MODE_HOTSWAP, source, Paths.RELOAD_DEX_FILE_NAME); // } // // @NonNull // public ApplicationPatch getPatch() throws IOException { // byte[] bytes = Files.toByteArray(source); // String path; // // These path names are specially handled on the client side // // (e.g. it interprets "classes.dex" as meaning create a new // // unique class file in the class folder // switch (mode) { // case TRANSFER_MODE_SLICE: // path = Paths.DEX_SLICE_PREFIX + name; // break; // case TRANSFER_MODE_HOTSWAP: // case TRANSFER_MODE_RESOURCES: // path = name; // break; // default: // throw new IllegalArgumentException(Integer.toString(mode)); // } // // return new ApplicationPatch(path, bytes); // } // // @Override // public String toString() { // return source + " as " + name + " with mode " + mode; // } // } // // /** // * Stops the given app (via adb). // * // * @param device the device // * @param sendChangeBroadcast whether to also send a package change broadcast // * @throws InstantRunPushFailedException if there's a problem // */ // public void stopApp(@NonNull IDevice device, boolean sendChangeBroadcast) throws InstantRunPushFailedException { // try { // runCommand(device, "am force-stop " + mPackageName); // } catch (Throwable t) { // throw new InstantRunPushFailedException("Exception while stopping app: " + t.toString()); // } // if (sendChangeBroadcast) { // try { // // We think this might necessary to force the system not hold on to any data from the previous // // version of the process, such as the scenario described in // // https://code.google.com/p/android/issues/detail?id=200895#c9 // runCommand(device, "am broadcast -a android.intent.action.PACKAGE_CHANGED -p " + mPackageName); // } // catch (Throwable ignore) { // // We can live with this one not succeeding; may require root etc // // See https://code.google.com/p/android/issues/detail?id=201249 // } // } // } // // /** // * Install dex and resource files on the given device (using adb to push files // * to the device when the app isn't running so we can't send it patches via // * the socket connection.) // * // * @param files the files to push to the device, and the paths to push them as. // * @param device the device to push to // */ // public void pushFiles( // @NonNull List<FileTransfer> files, // @NonNull IDevice device, // @NonNull final String buildId) throws InstantRunPushFailedException { // try { // Set<String> createdDirs = Sets.newHashSet(); // // for (FileTransfer file : files) { // String folder; // String name; // switch (file.mode) { // case TRANSFER_MODE_SLICE: // folder = Paths.getDexFileDirectory(mPackageName); // name = Paths.DEX_SLICE_PREFIX + file.name; // break; // case TRANSFER_MODE_RESOURCES: // folder = Paths.getInboxDirectory(mPackageName); // name = Paths.RESOURCE_FILE_NAME; // break; // case TRANSFER_MODE_HOTSWAP: // throw new IllegalArgumentException("Hotswap patches can only be applied " // + "when the app is running"); // default: // throw new IllegalArgumentException(Integer.toString(file.mode)); // } // // // Copy the restart .dex file over to the device in the dex folder with the new name // String remote = copyToDeviceScratchFile(device, mPackageName, file.source); // // // Make sure directory exists // if (!createdDirs.contains(folder)) { // createdDirs.add(folder); // String cmd = "run-as " + mPackageName + " mkdir -p " + folder; // if (!runAsCommand(device, cmd)) { // mLogger.warning("pushFiles: %s", "Error creating folder with: " + cmd); // throw new InstantRunPushFailedException("Error creating folder with: " + cmd); // } // } // // String cmd = "run-as " + mPackageName + " cp " + remote + " " + folder + "/" + name; // if (!runAsCommand(device, cmd)) { // mLogger.warning("pushFiles: %s", "Error copying file with: " + cmd); // throw new InstantRunPushFailedException("Error copying file with: " + cmd); // } // } // // transferLocalIdToDeviceId(device, buildId); // } catch (IOException ioe) { // mLogger.warning("Couldn't write build id file: %s", ioe); // throw new InstantRunPushFailedException("IOException while pushing files: " + ioe.toString()); // } catch (AdbCommandRejectedException e) { // mLogger.warning("%s", Throwables.getStackTraceAsString(e)); // throw new InstantRunPushFailedException("Exception while pushing files: " + e.toString()); // } catch (TimeoutException e) { // mLogger.warning("%s", Throwables.getStackTraceAsString(e)); // throw new InstantRunPushFailedException("Exception while pushing files: " + e.toString()); // } catch (ShellCommandUnresponsiveException e) { // mLogger.warning("%s", Throwables.getStackTraceAsString(e)); // throw new InstantRunPushFailedException("Exception while pushing files: " + e.toString()); // } catch (SyncException e) { // mLogger.warning("%s", Throwables.getStackTraceAsString(e)); // throw new InstantRunPushFailedException("Exception while pushing files: " + e.toString()); // } // } // // private boolean runAsCommand(@NonNull IDevice device, @NonNull String cmd) // throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException, InstantRunPushFailedException { // String output = getCommandOutput(device, cmd).trim(); // if (!output.isEmpty()) { // mLogger.warning("Unexpected shell output for " + cmd + ": " + output); // // if (output.startsWith("run-as: Package '") && output.endsWith("' is unknown")) { // throw new InstantRunPushFailedException(BROKEN_RUN_AS); // } // return false; // } // return true; // } // // private boolean runCommand(@NonNull IDevice device, @NonNull String cmd) // throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { // String output = getCommandOutput(device, cmd).trim(); // if (!output.isEmpty()) { // mLogger.warning("Unexpected shell output for " + cmd + ": " + output); // return false; // } // return true; // } // // @NonNull // private static String getCommandOutput(@NonNull IDevice device, @NonNull String cmd) // throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { // CollectingOutputReceiver receiver; // receiver = new CollectingOutputReceiver(); // device.executeShellCommand(cmd, receiver); // return receiver.getOutput(); // } // // private void logFilesPushed(@NonNull List<FileTransfer> files, boolean needRestart) { // StringBuilder sb = new StringBuilder("Pushing files: "); // if (needRestart) { // sb.append("(needs restart) "); // } // // sb.append('['); // String separator = ""; // for (FileTransfer file : files) { // sb.append(separator); // sb.append(file.source.getName()); // sb.append(" as "); // sb.append(file.name); // // separator = ", "; // } // sb.append(']'); // // mLogger.info(sb.toString()); // } // }