| /* |
| * 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.cli; |
| |
| import com.android.ddmlib.AdbCommandRejectedException; |
| import com.android.ddmlib.CollectingOutputReceiver; |
| import com.android.ddmlib.IDevice; |
| import com.android.ddmlib.InstallException; |
| import com.android.ddmlib.ShellCommandUnresponsiveException; |
| import com.android.ddmlib.TimeoutException; |
| import com.facebook.buck.command.Build; |
| import com.facebook.buck.event.LogEvent; |
| import com.facebook.buck.rules.BuildRule; |
| import com.facebook.buck.rules.DependencyGraph; |
| import com.facebook.buck.rules.InstallableBuildRule; |
| import com.facebook.buck.step.ExecutionContext; |
| import com.facebook.buck.util.AndroidManifestReader; |
| import com.facebook.buck.util.DefaultAndroidManifestReader; |
| import com.google.common.annotations.VisibleForTesting; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.util.List; |
| import java.util.UUID; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Command so a user can build and install an APK. |
| */ |
| public class InstallCommand extends UninstallSupportCommandRunner<InstallCommandOptions> { |
| |
| protected InstallCommand(CommandRunnerParams params) { |
| super(params); |
| } |
| |
| @Override |
| InstallCommandOptions createOptions(BuckConfig buckConfig) { |
| return new InstallCommandOptions(buckConfig); |
| } |
| |
| @Override |
| int runCommandWithOptionsInternal(InstallCommandOptions options) throws IOException { |
| // Make sure that only one build target is specified. |
| if (options.getArguments().size() != 1) { |
| getStdErr().println("Must specify exactly one android_binary() or apk_genrule() rule."); |
| return 1; |
| } |
| |
| // Build the specified target. |
| BuildCommand buildCommand = new BuildCommand(getCommandRunnerParams()); |
| int exitCode = buildCommand.runCommandWithOptions(options); |
| if (exitCode != 0) { |
| return exitCode; |
| } |
| |
| // Get the build rule that was built. Verify that it is an android_binary() rule. |
| Build build = buildCommand.getBuild(); |
| DependencyGraph graph = build.getDependencyGraph(); |
| BuildRule buildRule = graph.findBuildRuleByTarget(buildCommand.getBuildTargets().get(0)); |
| if (!(buildRule instanceof InstallableBuildRule)) { |
| console.printBuildFailure(String.format( |
| "Specified rule %s must be of type android_binary() or apk_genrule() but was %s().\n", |
| buildRule.getFullyQualifiedName(), |
| buildRule.getType().getName())); |
| return 1; |
| } |
| InstallableBuildRule installableBuildRule = (InstallableBuildRule)buildRule; |
| |
| // Uninstall the app first, if requested. |
| if (options.shouldUninstallFirst()) { |
| String packageName = tryToExtractPackageNameFromManifest(installableBuildRule); |
| uninstallApk(packageName, |
| options.adbOptions(), |
| options.targetDeviceOptions(), |
| options.uninstallOptions(), |
| build.getExecutionContext()); |
| // Perhaps the app wasn't installed to begin with, shouldn't stop us. |
| } |
| |
| if (!installApk(installableBuildRule, options, build.getExecutionContext())) { |
| return 1; |
| } |
| |
| // We've installed the application successfully. |
| // Is either of --activity or --run present? |
| if (options.shouldStartActivity()) { |
| exitCode = startActivity(installableBuildRule, options.getActivityToStart(), options, |
| build.getExecutionContext()); |
| if (exitCode != 0) { |
| return exitCode; |
| } |
| } |
| |
| return exitCode; |
| } |
| |
| private int startActivity(InstallableBuildRule androidBinaryRule, String activity, |
| InstallCommandOptions options, ExecutionContext context) throws IOException { |
| |
| // Might need the package name and activities from the AndroidManifest. |
| AndroidManifestReader reader = DefaultAndroidManifestReader.forPath( |
| androidBinaryRule.getManifest()); |
| |
| 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)); |
| |
| getBuckEventBus().post(StartActivityEvent.started(androidBinaryRule.getBuildTarget(), |
| activityToRun)); |
| boolean success = adbCall(options.adbOptions(), |
| options.targetDeviceOptions(), |
| context, |
| new AdbCallable() { |
| @Override |
| public boolean call(IDevice device) throws Exception { |
| String err = deviceStartActivity(device, activityToRun); |
| if (err != null) { |
| console.printBuildFailure(err); |
| return false; |
| } else { |
| return true; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return "start activity"; |
| } |
| }); |
| getBuckEventBus().post(StartActivityEvent.finished(androidBinaryRule.getBuildTarget(), |
| activityToRun, |
| success)); |
| |
| return success ? 0 : 1; |
| |
| } |
| |
| @VisibleForTesting |
| String deviceStartActivity(IDevice device, String activityToRun) { |
| try { |
| ErrorParsingReceiver receiver = new ErrorParsingReceiver() { |
| @Override |
| 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; |
| } |
| }; |
| device.executeShellCommand( |
| String.format("am start -n %s", activityToRun), |
| receiver, |
| INSTALL_TIMEOUT); |
| return receiver.getErrorMessage(); |
| } catch (Exception e) { |
| return e.toString(); |
| } |
| } |
| |
| |
| @Override |
| String getUsageIntro() { |
| return "Specify an android_binary() rule whose APK should be installed"; |
| } |
| |
| /** |
| * Install apk on all matching devices. This functions performs device |
| * filtering based on three possible arguments: |
| * |
| * -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 |
| * |
| * 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. |
| */ |
| @VisibleForTesting |
| boolean installApk(InstallableBuildRule buildRule, |
| InstallCommandOptions options, |
| ExecutionContext context) { |
| getBuckEventBus().post(InstallEvent.started(buildRule.getBuildTarget())); |
| |
| final File apk = new File(buildRule.getApkPath()); |
| final boolean installViaSd = options.shouldInstallViaSd(); |
| boolean success = adbCall(options.adbOptions(), |
| options.targetDeviceOptions(), |
| context, |
| new AdbCallable() { |
| @Override |
| public boolean call(IDevice device) throws Exception { |
| return installApkOnDevice(device, apk, installViaSd); |
| } |
| |
| @Override |
| public String toString() { |
| return "install apk"; |
| } |
| }); |
| getBuckEventBus().post(InstallEvent.finished(buildRule.getBuildTarget(), success)); |
| |
| return success; |
| } |
| |
| |
| /** |
| * Installs apk on specific device. Reports success or failure to console. |
| */ |
| @VisibleForTesting |
| @SuppressWarnings("PMD.PrematureDeclaration") |
| boolean installApkOnDevice(IDevice device, File apk, boolean installViaSd) { |
| 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 + ")"; |
| } |
| } |
| |
| getBuckEventBus().post(LogEvent.info("Installing apk on %s.", name)); |
| try { |
| String reason = null; |
| if (installViaSd) { |
| reason = deviceInstallPackageViaSd(device, apk.getAbsolutePath()); |
| } else { |
| reason = 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; |
| } |
| } |
| |
| /** |
| * Installs apk on device, copying apk to external storage first. |
| */ |
| 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 |
| String reason = device.installRemotePackage(remotePackage, true); |
| // Delete temporary file |
| device.removeRemotePackage(remotePackage); |
| return reason; |
| } catch (Throwable t) { |
| return String.valueOf(t.getMessage()); |
| } |
| } |
| |
| /** |
| * Retrieves external storage location (SD card) from device. |
| */ |
| private String deviceGetExternalStorage(IDevice device) throws TimeoutException, |
| AdbCommandRejectedException, ShellCommandUnresponsiveException, IOException { |
| CollectingOutputReceiver receiver = new CollectingOutputReceiver(); |
| device.executeShellCommand("echo $EXTERNAL_STORAGE", receiver, GETPROP_TIMEOUT); |
| String value = receiver.getOutput().trim(); |
| if (value.isEmpty()) { |
| return null; |
| } |
| return value; |
| } |
| |
| } |