/* * Copyright 2012-present Facebook, Inc. * * 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.facebook.buck.android; import static com.facebook.buck.util.concurrent.MostExecutors.newMultiThreadExecutor; import static com.google.common.util.concurrent.MoreExecutors.listeningDecorator; import com.android.ddmlib.AdbCommandRejectedException; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.CollectingOutputReceiver; import com.android.ddmlib.DdmPreferences; import com.android.ddmlib.IDevice; import com.android.ddmlib.InstallException; import com.android.ddmlib.MultiLineReceiver; import com.android.ddmlib.NullOutputReceiver; import com.android.ddmlib.ShellCommandUnresponsiveException; import com.android.ddmlib.TimeoutException; import com.facebook.buck.android.exopackage.ExopackageInstaller; import com.facebook.buck.annotations.SuppressForbidden; import com.facebook.buck.event.BuckEventBus; import com.facebook.buck.event.ConsoleEvent; import com.facebook.buck.event.InstallEvent; import com.facebook.buck.event.SimplePerfEvent; import com.facebook.buck.event.StartActivityEvent; import com.facebook.buck.event.UninstallEvent; import com.facebook.buck.log.CommandThreadFactory; import com.facebook.buck.rules.ExopackageInfo; import com.facebook.buck.rules.SourcePathResolver; import com.facebook.buck.step.AdbOptions; import com.facebook.buck.step.ExecutionContext; import com.facebook.buck.step.TargetDeviceOptions; import com.facebook.buck.util.Console; import com.facebook.buck.util.HumanReadableException; import com.facebook.buck.util.InterruptionFailedException; import com.facebook.buck.util.concurrent.MostExecutors; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import javax.annotation.Nullable; /** Helper for executing commands over ADB, especially for multiple devices. */ public class AdbHelper { private static final long ADB_CONNECT_TIMEOUT_MS = 5000; private static final long ADB_CONNECT_TIME_STEP_MS = ADB_CONNECT_TIMEOUT_MS / 10; /** Pattern that matches safe package names. (Must be a full string match). */ public static final Pattern PACKAGE_NAME_PATTERN = Pattern.compile("[\\w.-]+"); /** Pattern that matches Genymotion serial numbers. */ private static final Pattern RE_LOCAL_TRANSPORT_SERIAL = Pattern.compile("\\d+\\.\\d+\\.\\d+\\.\\d+:\\d+"); /** * If this environment variable is set, the device with the specified serial number is targeted. * The -s option overrides this. */ static final String SERIAL_NUMBER_ENV = "ANDROID_SERIAL"; // Taken from ddms source code. public static final long INSTALL_TIMEOUT = 2 * 60 * 1000; // 2 min public static final long GETPROP_TIMEOUT = 2 * 1000; // 2 seconds public static final String ECHO_COMMAND_SUFFIX = " ; echo -n :$?"; private final AdbOptions options; private final TargetDeviceOptions deviceOptions; private final ExecutionContext context; private final Console console; private final BuckEventBus buckEventBus; private final boolean restartAdbOnFailure; public AdbHelper( AdbOptions adbOptions, TargetDeviceOptions deviceOptions, ExecutionContext context, Console console, BuckEventBus buckEventBus, boolean restartAdbOnFailure) { this.options = adbOptions; this.deviceOptions = deviceOptions; this.context = context; this.console = console; this.buckEventBus = buckEventBus; this.restartAdbOnFailure = restartAdbOnFailure; } public static AdbHelper get(ExecutionContext context, boolean restartOnFailure) { Preconditions.checkArgument(context.getAdbOptions().isPresent()); Preconditions.checkArgument(context.getTargetDeviceOptions().isPresent()); return new AdbHelper( context.getAdbOptions().get(), context.getTargetDeviceOptions().get(), context, context.getConsole(), context.getBuckEventBus(), restartOnFailure); } private BuckEventBus getBuckEventBus() { return buckEventBus; } /** * Returns list of devices that pass the filter. If there is an invalid combination or no devices * are left after filtering this function prints an error and returns null. */ @Nullable @VisibleForTesting @SuppressForbidden List<IDevice> filterDevices(IDevice[] allDevices) { if (allDevices.length == 0) { console.printBuildFailure("No devices are found."); return null; } List<IDevice> devices = new ArrayList<>(); Optional<Boolean> emulatorsOnly = Optional.empty(); if (deviceOptions.isEmulatorsOnlyModeEnabled() && options.isMultiInstallModeEnabled()) { emulatorsOnly = Optional.empty(); } else if (deviceOptions.isEmulatorsOnlyModeEnabled()) { emulatorsOnly = Optional.of(true); } else if (deviceOptions.isRealDevicesOnlyModeEnabled()) { emulatorsOnly = Optional.of(false); } int onlineDevices = 0; for (IDevice device : allDevices) { boolean passed = false; if (device.isOnline()) { onlineDevices++; boolean serialMatches = true; if (deviceOptions.getSerialNumber().isPresent()) { serialMatches = device.getSerialNumber().equals(deviceOptions.getSerialNumber().get()); } else if (context.getEnvironment().containsKey(SERIAL_NUMBER_ENV)) { serialMatches = device.getSerialNumber().equals(context.getEnvironment().get(SERIAL_NUMBER_ENV)); } boolean deviceTypeMatches; if (emulatorsOnly.isPresent()) { // Only devices of specific type are accepted: // either real devices only or emulators only. deviceTypeMatches = (emulatorsOnly.get() == isEmulator(device)); } else { // All online devices match. deviceTypeMatches = true; } passed = serialMatches && deviceTypeMatches; } if (passed) { devices.add(device); } } // Filtered out all devices. if (onlineDevices == 0) { console.printBuildFailure("No devices are found."); return null; } if (devices.isEmpty()) { console.printBuildFailure( String.format( "Found %d connected device(s), but none of them matches specified filter.", onlineDevices)); return null; } return devices; } private boolean isEmulator(IDevice device) { return isLocalTransport(device) || device.isEmulator(); } /** * To be consistent with adb, we treat all local transports (as opposed to USB transports) as * emulators instead of devices. */ private boolean isLocalTransport(IDevice device) { return RE_LOCAL_TRANSPORT_SERIAL.matcher(device.getSerialNumber()).find(); } private boolean isAdbInitialized(AndroidDebugBridge adb) { return adb.isConnected() && adb.hasInitialDeviceList(); } /** * Creates connection to adb and waits for this connection to be initialized and receive initial * list of devices. */ @Nullable @SuppressWarnings("PMD.EmptyCatchBlock") private AndroidDebugBridge createAdb(ExecutionContext context) throws InterruptedException { DdmPreferences.setTimeOut(60000); try { AndroidDebugBridge.init(/* clientSupport */ false); } catch (IllegalStateException ex) { // ADB was already initialized, we're fine, so just ignore. } AndroidDebugBridge adb = AndroidDebugBridge.createBridge(context.getPathToAdbExecutable(), false); if (adb == null) { console.printBuildFailure("Failed to connect to adb. Make sure adb server is running."); return null; } long start = System.currentTimeMillis(); while (!isAdbInitialized(adb)) { long timeLeft = start + ADB_CONNECT_TIMEOUT_MS - System.currentTimeMillis(); if (timeLeft <= 0) { break; } Thread.sleep(ADB_CONNECT_TIME_STEP_MS); } return isAdbInitialized(adb) ? adb : null; } @SuppressForbidden public List<IDevice> getDevices(boolean quiet) throws InterruptedException { // Initialize adb connection. AndroidDebugBridge adb = createAdb(context); if (adb == null) { console.printBuildFailure("Failed to create adb connection."); return new ArrayList<>(); } // Build list of matching devices. List<IDevice> devices = filterDevices(adb.getDevices()); if (devices != null && devices.size() > 1) { // Found multiple devices but multi-install mode is not enabled. if (!options.isMultiInstallModeEnabled()) { console.printBuildFailure( String.format( "%d device(s) matches specified device filter (1 expected).\n" + "Either disconnect other devices or enable multi-install mode (%s).", devices.size(), AdbOptions.MULTI_INSTALL_MODE_SHORT_ARG)); return new ArrayList<>(); } if (!quiet) { // Report if multiple devices are matching the filter. console.getStdOut().printf("Found " + devices.size() + " matching devices.\n"); } } if (devices == null && restartAdbOnFailure) { console.printErrorText("No devices found with adb, restarting adb-server."); adb.restart(); devices = filterDevices(adb.getDevices()); } if (devices == null) { return new ArrayList<>(); } return devices; } public IDevice getSingleDevice() throws InterruptedException { List<IDevice> devices = getDevices(true); if (devices.isEmpty()) { throw new HumanReadableException("Expecting one android device/emulator to be attached."); } return devices.get(0); } public List<String> getDeviceCPUAbis() throws InterruptedException { class GetCpuAbiCallable extends AdbHelper.AdbCallable { public List<String> results = new ArrayList<>(); @Override public boolean call(IDevice device) throws Exception { String arch = device.getProperty("ro.product.cpu.abi"); if (arch == null) { console.printBuildFailure( String.format( "Failed to get property \"ro.prouct.cpu.abi\" from %s", device.getSerialNumber())); return false; } synchronized (results) { results.add(arch); } return true; } @Override public String toString() { return "get device cpu abi"; } } GetCpuAbiCallable callable = new GetCpuAbiCallable(); adbCall(callable, true); return callable.results; } /** * Execute an {@link AdbCallable} for all matching devices. This functions performs device * filtering based on three possible arguments: * * <p>-e (emulator-only) - only emulators are passing the filter -d (device-only) - only real * devices are passing the filter -s (serial) - only device/emulator with specific serial number * are passing the filter * * <p>If more than one device matches the filter this function will fail unless multi-install mode * is enabled (-x). This flag is used as a marker that user understands that multiple devices will * be used to install the apk if needed. */ @SuppressWarnings("PMD.EmptyCatchBlock") @SuppressForbidden public boolean adbCall(AdbCallable adbCallable, boolean quiet) throws InterruptedException { List<IDevice> devices; try (SimplePerfEvent.Scope ignored = SimplePerfEvent.scope(buckEventBus, "set_up_adb_call")) { devices = getDevices(quiet); if (devices.size() == 0) { return false; } } int adbThreadCount = options.getAdbThreadCount(); if (adbThreadCount <= 0) { adbThreadCount = devices.size(); } // Start executions on all matching devices. List<ListenableFuture<Boolean>> futures = new ArrayList<>(); ListeningExecutorService executorService = listeningDecorator( newMultiThreadExecutor( new CommandThreadFactory(getClass().getSimpleName()), adbThreadCount)); for (final IDevice device : devices) { futures.add(executorService.submit(adbCallable.forDevice(device))); } // Wait for all executions to complete or fail. List<Boolean> results = null; try { results = Futures.allAsList(futures).get(); } catch (ExecutionException ex) { console.printBuildFailure("Failed: " + adbCallable); ex.printStackTrace(console.getStdErr()); return false; } catch (InterruptedException e) { try { Futures.allAsList(futures).cancel(true); } catch (CancellationException ignored) { // Rethrow original InterruptedException instead. } Thread.currentThread().interrupt(); throw e; } finally { MostExecutors.shutdownOrThrow( executorService, 10, TimeUnit.MINUTES, new InterruptionFailedException("Failed to shutdown ExecutorService.")); } int successCount = 0; for (Boolean result : results) { if (result) { successCount++; } } int failureCount = results.size() - successCount; // Report results. if (successCount > 0 && !quiet) { console.printSuccess( String.format("Successfully ran %s on %d device(s)", adbCallable, successCount)); } if (failureCount > 0) { console.printBuildFailure( String.format("Failed to %s on %d device(s).", adbCallable, failureCount)); } return failureCount == 0; } /** Base class for commands to be run against an {@link com.android.ddmlib.IDevice IDevice}. */ public abstract static class AdbCallable { /** * Perform the actions specified by this {@code AdbCallable} and return true on success. * * @param device the {@link com.android.ddmlib.IDevice IDevice} to run against * @return {@code true} if the command succeeded. */ public abstract boolean call(IDevice device) throws Exception; /** * Wraps this as a {@link java.util.concurrent.Callable Callable<Boolean>} whose {@link * Callable#call() call()} method calls {@link AdbHelper.AdbCallable#call(IDevice) * call(IDevice)} against the specified device. * * @param device the {@link com.android.ddmlib.IDevice IDevice} to run against. * @return a {@code Callable} */ public Callable<Boolean> forDevice(final IDevice device) { return new Callable<Boolean>() { @Override public Boolean call() throws Exception { return AdbCallable.this.call(device); } @Override public String toString() { return AdbCallable.this.toString(); } }; } } /** * Implementation of {@link com.android.ddmlib.IShellOutputReceiver} with helper functions to * parse output lines and figure out if a call to {@link * com.android.ddmlib.IDevice#executeShellCommand(String, * com.android.ddmlib.IShellOutputReceiver)} succeeded. */ private abstract static class ErrorParsingReceiver extends MultiLineReceiver { @Nullable private String errorMessage = null; /** * Look for an error message in {@code line}. * * @param line * @return an error message if {@code line} is indicative of an error, {@code null} otherwise. */ @Nullable protected abstract String matchForError(String line); @Override public void processNewLines(String[] lines) { for (String line : lines) { if (line.length() > 0) { String err = matchForError(line); if (err != null) { errorMessage = err; } } } } @Override public boolean isCancelled() { return false; } @Nullable public String getErrorMessage() { return errorMessage; } } /** An exception that indicates that an executed command returned an unsuccessful exit code. */ @SuppressWarnings("serial") public static class CommandFailedException extends IOException { public final String command; public final int exitCode; public final String output; public CommandFailedException(String command, int exitCode, String output) { super("Command '" + command + "' failed with code " + exitCode + ". Output:\n" + output); this.command = command; this.exitCode = exitCode; this.output = output; } } /** * Runs a command on a device and throws an exception if it fails. * * <p>This will not work if your command contains "exit" or "trap" statements. * * @param device Device to run the command on. * @param command Shell command to execute. Must not use "exit" or "trap". * @return The full text output of the command. * @throws CommandFailedException if the command fails. */ public static String executeCommandWithErrorChecking(IDevice device, String command) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { CollectingOutputReceiver receiver = new CollectingOutputReceiver(); device.executeShellCommand(command + ECHO_COMMAND_SUFFIX, receiver); return checkReceiverOutput(command, receiver); } /** * This was made public for one specific call site in ExopackageInstaller. If you're reading this, * you probably shouldn't call it. Pretend this method is private. */ public static String checkReceiverOutput(String command, CollectingOutputReceiver receiver) throws CommandFailedException { String fullOutput = receiver.getOutput(); int colon = fullOutput.lastIndexOf(':'); String realOutput = fullOutput.substring(0, colon); String exitCodeStr = fullOutput.substring(colon + 1); int exitCode = Integer.parseInt(exitCodeStr); if (exitCode != 0) { throw new CommandFailedException(command, exitCode, realOutput); } return realOutput; } /** * Install apk on all matching devices. This functions performs device filtering based on three * possible arguments: * * <p>-e (emulator-only) - only emulators are passing the filter -d (device-only) - only real * devices are passing the filter -s (serial) - only device/emulator with specific serial number * are passing the filter * * <p>If more than one device matches the filter this function will fail unless multi-install mode * is enabled (-x). This flag is used as a marker that user understands that multiple devices will * be used to install the apk if needed. */ @SuppressForbidden public boolean installApk( SourcePathResolver pathResolver, HasInstallableApk hasInstallableApk, boolean installViaSd, boolean quiet) throws InterruptedException { Optional<ExopackageInfo> exopackageInfo = hasInstallableApk.getApkInfo().getExopackageInfo(); if (exopackageInfo.isPresent()) { return new ExopackageInstaller(pathResolver, context, this, hasInstallableApk).install(quiet); } InstallEvent.Started started = InstallEvent.started(hasInstallableApk.getBuildTarget()); if (!quiet) { getBuckEventBus().post(started); } File apk = pathResolver.getAbsolutePath(hasInstallableApk.getApkInfo().getApkPath()).toFile(); boolean success = adbCall( new AdbHelper.AdbCallable() { @Override public boolean call(IDevice device) throws Exception { return installApkOnDevice(device, apk, installViaSd, quiet); } @Override @SuppressForbidden public String toString() { return String.format( "install apk %s", hasInstallableApk.getBuildTarget().toString()); } }, quiet); if (!quiet) { AdbHelper.tryToExtractPackageNameFromManifest(pathResolver, hasInstallableApk.getApkInfo()); getBuckEventBus() .post( InstallEvent.finished( started, success, Optional.empty(), Optional.of( AdbHelper.tryToExtractPackageNameFromManifest( pathResolver, hasInstallableApk.getApkInfo())))); } return success; } /** Installs apk on specific device. Reports success or failure to console. */ @SuppressWarnings("PMD.PrematureDeclaration") @SuppressForbidden public boolean installApkOnDevice(IDevice device, File apk, boolean installViaSd, boolean quiet) { String name; if (device.isEmulator()) { name = device.getSerialNumber() + " (" + device.getAvdName() + ")"; } else { name = device.getSerialNumber(); String model = device.getProperty("ro.product.model"); if (model != null) { name += " (" + model + ")"; } } if (!isDeviceTempWritable(device, name)) { return false; } if (!quiet) { getBuckEventBus().post(ConsoleEvent.info("Installing apk on %s.", name)); } try { String reason = null; if (installViaSd) { reason = deviceInstallPackageViaSd(device, apk.getAbsolutePath()); } else { device.installPackage(apk.getAbsolutePath(), true); } if (reason != null) { console.printBuildFailure(String.format("Failed to install apk on %s: %s.", name, reason)); return false; } return true; } catch (InstallException ex) { console.printBuildFailure(String.format("Failed to install apk on %s.", name)); ex.printStackTrace(console.getStdErr()); return false; } } @VisibleForTesting @SuppressForbidden protected boolean isDeviceTempWritable(IDevice device, String name) { StringBuilder loggingInfo = new StringBuilder(); try { String output; try { output = executeCommandWithErrorChecking(device, "ls -l -d /data/local/tmp"); if (!( // Pattern for Android's "toolbox" version of ls output.matches("\\Adrwx....-x +shell +shell.* tmp[\\r\\n]*\\z") || // Pattern for CyanogenMod's busybox version of ls output.matches("\\Adrwx....-x +[0-9]+ +shell +shell.* /data/local/tmp[\\r\\n]*\\z"))) { loggingInfo.append( String.format(Locale.ENGLISH, "Bad ls output for /data/local/tmp: '%s'\n", output)); } executeCommandWithErrorChecking(device, "echo exo > /data/local/tmp/buck-experiment"); output = executeCommandWithErrorChecking(device, "cat /data/local/tmp/buck-experiment"); if (!output.matches("\\Aexo[\\r\\n]*\\z")) { loggingInfo.append( String.format( Locale.ENGLISH, "Bad echo/cat output for /data/local/tmp: '%s'\n", output)); } executeCommandWithErrorChecking(device, "rm /data/local/tmp/buck-experiment"); } catch (CommandFailedException e) { loggingInfo.append( String.format( Locale.ENGLISH, "Failed (%d) '%s':\n%s\n", e.exitCode, e.command, e.output)); } if (!loggingInfo.toString().isEmpty()) { CollectingOutputReceiver receiver = new CollectingOutputReceiver(); device.executeShellCommand("getprop", receiver); for (String line : com.google.common.base.Splitter.on('\n').split(receiver.getOutput())) { if (line.contains("ro.product.model") || line.contains("ro.build.description")) { loggingInfo.append(line).append('\n'); } } } } catch (AdbCommandRejectedException | ShellCommandUnresponsiveException | TimeoutException | IOException e) { console.printBuildFailure(String.format("Failed to test /data/local/tmp on %s.", name)); e.printStackTrace(console.getStdErr()); return false; } String logMessage = loggingInfo.toString(); if (!logMessage.isEmpty()) { StringBuilder fullMessage = new StringBuilder(); fullMessage.append("============================================================\n"); fullMessage.append('\n'); fullMessage.append("HEY! LISTEN!\n"); fullMessage.append('\n'); fullMessage.append("The /data/local/tmp directory on your device isn't fully-functional.\n"); fullMessage.append("Here's some extra info:\n"); fullMessage.append(logMessage); fullMessage.append("============================================================\n"); console.getStdErr().println(fullMessage.toString()); } return true; } /** Installs apk on device, copying apk to external storage first. */ @SuppressForbidden @Nullable private String deviceInstallPackageViaSd(IDevice device, String apk) { try { // Figure out where the SD card is mounted. String externalStorage = deviceGetExternalStorage(device); if (externalStorage == null) { return "Cannot get external storage location."; } String remotePackage = String.format("%s/%s.apk", externalStorage, UUID.randomUUID()); // Copy APK to device device.pushFile(apk, remotePackage); // Install device.installRemotePackage(remotePackage, true); // Delete temporary file device.removeRemotePackage(remotePackage); return null; } catch (Throwable t) { return String.valueOf(t.getMessage()); } } /** Retrieves external storage location (SD card) from device. */ @Nullable private String deviceGetExternalStorage(IDevice device) throws TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { CollectingOutputReceiver receiver = new CollectingOutputReceiver(); device.executeShellCommand( "echo $EXTERNAL_STORAGE", receiver, AdbHelper.GETPROP_TIMEOUT, TimeUnit.MILLISECONDS); String value = receiver.getOutput().trim(); if (value.isEmpty()) { return null; } return value; } @SuppressForbidden public int startActivity( SourcePathResolver pathResolver, HasInstallableApk hasInstallableApk, @Nullable String activity, boolean waitForDebugger) throws IOException, InterruptedException { // Might need the package name and activities from the AndroidManifest. Path pathToManifest = pathResolver.getAbsolutePath(hasInstallableApk.getApkInfo().getManifestPath()); AndroidManifestReader reader = DefaultAndroidManifestReader.forPath( hasInstallableApk.getProjectFilesystem().resolve(pathToManifest)); if (activity == null) { // Get list of activities that show up in the launcher. List<String> launcherActivities = reader.getLauncherActivities(); // Sanity check. if (launcherActivities.isEmpty()) { console.printBuildFailure("No launchable activities found."); return 1; } else if (launcherActivities.size() > 1) { console.printBuildFailure("Default activity is ambiguous."); return 1; } // Construct a component for the '-n' argument of 'adb shell am start'. activity = reader.getPackage() + "/" + launcherActivities.get(0); } else if (!activity.contains("/")) { // If no package name was provided, assume the one in the manifest. activity = reader.getPackage() + "/" + activity; } final String activityToRun = activity; PrintStream stdOut = console.getStdOut(); stdOut.println(String.format("Starting activity %s...", activityToRun)); StartActivityEvent.Started started = StartActivityEvent.started(hasInstallableApk.getBuildTarget(), activityToRun); getBuckEventBus().post(started); boolean success = adbCall( new AdbHelper.AdbCallable() { @Override public boolean call(IDevice device) throws Exception { String err = deviceStartActivity(device, activityToRun, waitForDebugger); if (err != null) { console.printBuildFailure(err); return false; } else { return true; } } @Override public String toString() { return "start activity"; } }, false); getBuckEventBus().post(StartActivityEvent.finished(started, success)); return success ? 0 : 1; } @VisibleForTesting @Nullable @SuppressForbidden String deviceStartActivity(IDevice device, String activityToRun, boolean waitForDebugger) { try { AdbHelper.ErrorParsingReceiver receiver = new AdbHelper.ErrorParsingReceiver() { @Override @Nullable protected String matchForError(String line) { // Parses output from shell am to determine if activity was started correctly. return (Pattern.matches("^([\\w_$.])*(Exception|Error|error).*$", line) || line.contains("am: not found")) ? line : null; } }; final String waitForDebuggerFlag = waitForDebugger ? "-D" : ""; device.executeShellCommand( // 0x10200000 is FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | FLAG_ACTIVITY_NEW_TASK; the // constant values are public ABI. This way of invoking "am start" makes buck install -r // act just like the launcher, avoiding activity duplication on subsequent // launcher starts. String.format( "am start -f 0x10200000 -a android.intent.action.MAIN " + "-c android.intent.category.LAUNCHER -n %s %s", activityToRun, waitForDebuggerFlag), receiver, AdbHelper.INSTALL_TIMEOUT, TimeUnit.MILLISECONDS); return receiver.getErrorMessage(); } catch (Exception e) { return e.toString(); } } /** * Uninstall apk from all matching devices. * * @see #installApk(SourcePathResolver, HasInstallableApk, boolean, boolean) */ public boolean uninstallApp(final String packageName, final boolean shouldKeepUserData) throws InterruptedException { Preconditions.checkArgument(AdbHelper.PACKAGE_NAME_PATTERN.matcher(packageName).matches()); UninstallEvent.Started started = UninstallEvent.started(packageName); getBuckEventBus().post(started); boolean success = adbCall( new AdbHelper.AdbCallable() { @Override public boolean call(IDevice device) throws Exception { // Remove any exopackage data as well. GB doesn't support "rm -f", so just ignore output. device.executeShellCommand( "rm -r /data/local/tmp/exopackage/" + packageName, NullOutputReceiver.getReceiver()); return uninstallApkFromDevice(device, packageName, shouldKeepUserData); } @Override public String toString() { return "uninstall apk"; } }, false); getBuckEventBus().post(UninstallEvent.finished(started, success)); return success; } /** * Uninstalls apk from specific device. Reports success or failure to console. It's currently here * because it's used both by {@link com.facebook.buck.cli.InstallCommand} and {@link * com.facebook.buck.cli.UninstallCommand}. */ @SuppressWarnings("PMD.PrematureDeclaration") @SuppressForbidden private boolean uninstallApkFromDevice(IDevice device, String packageName, boolean keepData) { String name; if (device.isEmulator()) { name = device.getSerialNumber() + " (" + device.getAvdName() + ")"; } else { name = device.getSerialNumber(); String model = device.getProperty("ro.product.model"); if (model != null) { name += " (" + model + ")"; } } PrintStream stdOut = console.getStdOut(); stdOut.printf("Removing apk from %s.\n", name); try { long start = System.currentTimeMillis(); String reason = deviceUninstallPackage(device, packageName, keepData); long end = System.currentTimeMillis(); if (reason != null) { console.printBuildFailure( String.format("Failed to uninstall apk from %s: %s.", name, reason)); return false; } long delta = end - start; stdOut.printf("Uninstalled apk from %s in %d.%03ds.\n", name, delta / 1000, delta % 1000); return true; } catch (InstallException ex) { console.printBuildFailure(String.format("Failed to uninstall apk from %s.", name)); ex.printStackTrace(console.getStdErr()); return false; } } /** * Modified version of <a href="http://fburl.com/8840769">Device.uninstallPackage()</a>. * * @param device an {@link IDevice} * @param packageName application package name * @param keepData true if user data is to be kept * @return error message or null if successful * @throws InstallException */ @Nullable private String deviceUninstallPackage(IDevice device, String packageName, boolean keepData) throws InstallException { try { AdbHelper.ErrorParsingReceiver receiver = new AdbHelper.ErrorParsingReceiver() { @Override @Nullable protected String matchForError(String line) { return line.toLowerCase(Locale.US).contains("failure") ? line : null; } }; device.executeShellCommand( "pm uninstall " + (keepData ? "-k " : "") + packageName, receiver, AdbHelper.INSTALL_TIMEOUT, TimeUnit.MILLISECONDS); return receiver.getErrorMessage(); } catch (AdbCommandRejectedException | IOException | ShellCommandUnresponsiveException | TimeoutException e) { throw new InstallException(e); } } public static String tryToExtractPackageNameFromManifest( SourcePathResolver pathResolver, ApkInfo apkInfo) { Path pathToManifest = pathResolver.getAbsolutePath(apkInfo.getManifestPath()); // Note that the file may not exist if AndroidManifest.xml is a generated file // and the rule has not been built yet. if (!Files.isRegularFile(pathToManifest)) { throw new HumanReadableException( "Manifest file %s does not exist, so could not extract package name.", pathToManifest); } try { return DefaultAndroidManifestReader.forPath(pathToManifest).getPackage(); } catch (IOException e) { throw new HumanReadableException("Could not extract package name from %s", pathToManifest); } } public static String tryToExtractInstrumentationTestRunnerFromManifest( SourcePathResolver pathResolver, ApkInfo apkInfo) { Path pathToManifest = pathResolver.getAbsolutePath(apkInfo.getManifestPath()); if (!Files.isRegularFile(pathToManifest)) { throw new HumanReadableException( "Manifest file %s does not exist, so could not extract package name.", pathToManifest); } try { return DefaultAndroidManifestReader.forPath(pathToManifest).getInstrumentationTestRunner(); } catch (IOException e) { throw new HumanReadableException("Could not extract package name from %s", pathToManifest); } } }