aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java1872
1 files changed, 1872 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java
new file mode 100644
index 000000000..a95ed6882
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java
@@ -0,0 +1,1872 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
+ *
+ * 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.ide.eclipse.adt.internal.launch;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.CanceledException;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.ClientData.DebuggerStatus;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.InstallException;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.TimeoutException;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.actions.AvdManagerAction;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration.TargetMode;
+import com.android.ide.eclipse.adt.internal.launch.DelayedLaunchInfo.InstallRetryMode;
+import com.android.ide.eclipse.adt.internal.launch.DeviceChooserDialog.DeviceChooserResponse;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.ApkInstallManager;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.avd.AvdManager;
+import com.android.utils.NullLogger;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.debug.core.DebugPlugin;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationType;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.core.model.IDebugTarget;
+import org.eclipse.debug.ui.DebugUITools;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+import org.eclipse.jdt.launching.IVMConnector;
+import org.eclipse.jdt.launching.JavaRuntime;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Controls the launch of Android application either on a device or on the
+ * emulator. If an emulator is already running, this class will attempt to reuse
+ * it.
+ */
+public final class AndroidLaunchController implements IDebugBridgeChangeListener,
+ IDeviceChangeListener, IClientChangeListener, ILaunchController {
+
+ private static final String FLAG_AVD = "-avd"; //$NON-NLS-1$
+ private static final String FLAG_NETDELAY = "-netdelay"; //$NON-NLS-1$
+ private static final String FLAG_NETSPEED = "-netspeed"; //$NON-NLS-1$
+ private static final String FLAG_WIPE_DATA = "-wipe-data"; //$NON-NLS-1$
+ private static final String FLAG_NO_BOOT_ANIM = "-no-boot-anim"; //$NON-NLS-1$
+
+ /**
+ * Map to store {@link ILaunchConfiguration} objects that must be launched as simple connection
+ * to running application. The integer is the port on which to connect.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private static final HashMap<ILaunchConfiguration, Integer> sRunningAppMap =
+ new HashMap<ILaunchConfiguration, Integer>();
+
+ private static final Object sListLock = sRunningAppMap;
+
+ /**
+ * List of {@link DelayedLaunchInfo} waiting for an emulator to connect.
+ * <p>Once an emulator has connected, {@link DelayedLaunchInfo#getDevice()} is set and the
+ * DelayedLaunchInfo object is moved to
+ * {@link AndroidLaunchController#mWaitingForReadyEmulatorList}.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private final ArrayList<DelayedLaunchInfo> mWaitingForEmulatorLaunches =
+ new ArrayList<DelayedLaunchInfo>();
+
+ /**
+ * List of application waiting to be launched on a device/emulator.<br>
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ * */
+ private final ArrayList<DelayedLaunchInfo> mWaitingForReadyEmulatorList =
+ new ArrayList<DelayedLaunchInfo>();
+
+ /**
+ * Application waiting to show up as waiting for debugger.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private final ArrayList<DelayedLaunchInfo> mWaitingForDebuggerApplications =
+ new ArrayList<DelayedLaunchInfo>();
+
+ /**
+ * List of clients that have appeared as waiting for debugger before their name was available.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private final ArrayList<Client> mUnknownClientsWaitingForDebugger = new ArrayList<Client>();
+
+ /** static instance for singleton */
+ private static AndroidLaunchController sThis = new AndroidLaunchController();
+
+ /** private constructor to enforce singleton */
+ private AndroidLaunchController() {
+ AndroidDebugBridge.addDebugBridgeChangeListener(this);
+ AndroidDebugBridge.addDeviceChangeListener(this);
+ AndroidDebugBridge.addClientChangeListener(this);
+ }
+
+ /**
+ * Returns the singleton reference.
+ */
+ public static AndroidLaunchController getInstance() {
+ return sThis;
+ }
+
+
+ /**
+ * Launches a remote java debugging session on an already running application
+ * @param project The project of the application to debug.
+ * @param debugPort The port to connect the debugger to.
+ */
+ public static void debugRunningApp(IProject project, int debugPort) {
+ // get an existing or new launch configuration
+ ILaunchConfiguration config = AndroidLaunchController.getLaunchConfig(project,
+ LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID);
+
+ if (config != null) {
+ setPortLaunchConfigAssociation(config, debugPort);
+
+ // and launch
+ DebugUITools.launch(config, ILaunchManager.DEBUG_MODE);
+ }
+ }
+
+ /**
+ * Returns an {@link ILaunchConfiguration} for the specified {@link IProject}.
+ * @param project the project
+ * @param launchTypeId launch delegate type id
+ * @return a new or already existing <code>ILaunchConfiguration</code> or null if there was
+ * an error when creating a new one.
+ */
+ public static ILaunchConfiguration getLaunchConfig(IProject project, String launchTypeId) {
+ // get the launch manager
+ ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
+
+ // now get the config type for our particular android type.
+ ILaunchConfigurationType configType = manager.getLaunchConfigurationType(launchTypeId);
+
+ String name = project.getName();
+
+ // search for an existing launch configuration
+ ILaunchConfiguration config = findConfig(manager, configType, name);
+
+ // test if we found one or not
+ if (config == null) {
+ // Didn't find a matching config, so we make one.
+ // It'll be made in the "working copy" object first.
+ ILaunchConfigurationWorkingCopy wc = null;
+
+ try {
+ // make the working copy object
+ wc = configType.newInstance(null,
+ manager.generateLaunchConfigurationName(name));
+
+ // set the project name
+ wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name);
+
+ // set the launch mode to default.
+ wc.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION,
+ LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION);
+
+ // set default target mode
+ wc.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
+ LaunchConfigDelegate.DEFAULT_TARGET_MODE.toString());
+
+ // default AVD: None
+ wc.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String) null);
+
+ // set the default network speed
+ wc.setAttribute(LaunchConfigDelegate.ATTR_SPEED,
+ LaunchConfigDelegate.DEFAULT_SPEED);
+
+ // and delay
+ wc.setAttribute(LaunchConfigDelegate.ATTR_DELAY,
+ LaunchConfigDelegate.DEFAULT_DELAY);
+
+ // default wipe data mode
+ wc.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA,
+ LaunchConfigDelegate.DEFAULT_WIPE_DATA);
+
+ // default disable boot animation option
+ wc.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM,
+ LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM);
+
+ // set default emulator options
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+ String emuOptions = store.getString(AdtPrefs.PREFS_EMU_OPTIONS);
+ wc.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE, emuOptions);
+
+ // map the config and the project
+ wc.setMappedResources(getResourcesToMap(project));
+
+ // save the working copy to get the launch config object which we return.
+ return wc.doSave();
+
+ } catch (CoreException e) {
+ String msg = String.format(
+ "Failed to create a Launch config for project '%1$s': %2$s",
+ project.getName(), e.getMessage());
+ AdtPlugin.printErrorToConsole(project, msg);
+
+ // no launch!
+ return null;
+ }
+ }
+
+ return config;
+ }
+
+ /**
+ * Returns the list of resources to map to a Launch Configuration.
+ * @param project the project associated to the launch configuration.
+ */
+ public static IResource[] getResourcesToMap(IProject project) {
+ ArrayList<IResource> array = new ArrayList<IResource>(2);
+ array.add(project);
+
+ IFile manifest = ProjectHelper.getManifest(project);
+ if (manifest != null) {
+ array.add(manifest);
+ }
+
+ return array.toArray(new IResource[array.size()]);
+ }
+
+ /**
+ * Launches an android app on the device or emulator
+ *
+ * @param project The project we're launching
+ * @param mode the mode in which to launch, one of the mode constants
+ * defined by <code>ILaunchManager</code> - <code>RUN_MODE</code> or
+ * <code>DEBUG_MODE</code>.
+ * @param apk the resource to the apk to launch.
+ * @param packageName the Android package name of the app
+ * @param debugPackageName the Android package name to debug
+ * @param debuggable the debuggable value of the app's manifest, or null if not set.
+ * @param requiredApiVersionNumber the api version required by the app, or null if none.
+ * @param launchAction the action to perform after app sync
+ * @param config the launch configuration
+ * @param launch the launch object
+ */
+ public void launch(final IProject project, String mode, IFile apk,
+ String packageName, String debugPackageName, Boolean debuggable,
+ String requiredApiVersionNumber, final IAndroidLaunchAction launchAction,
+ final AndroidLaunchConfiguration config, final AndroidLaunch launch,
+ IProgressMonitor monitor) {
+
+ String message = String.format("Performing %1$s", launchAction.getLaunchDescription());
+ AdtPlugin.printToConsole(project, message);
+
+ // create the launch info
+ final DelayedLaunchInfo launchInfo = new DelayedLaunchInfo(project, packageName,
+ debugPackageName, launchAction, apk, debuggable, requiredApiVersionNumber, launch,
+ monitor);
+
+ // set the debug mode
+ launchInfo.setDebugMode(mode.equals(ILaunchManager.DEBUG_MODE));
+
+ // get the SDK
+ Sdk currentSdk = Sdk.getCurrent();
+ AvdManager avdManager = currentSdk.getAvdManager();
+
+ // reload the AVDs to make sure we are up to date
+ try {
+ avdManager.reloadAvds(NullLogger.getLogger());
+ } catch (AndroidLocationException e1) {
+ // this happens if the AVD Manager failed to find the folder in which the AVDs are
+ // stored. This is unlikely to happen, but if it does, we should force to go manual
+ // to allow using physical devices.
+ config.mTargetMode = TargetMode.MANUAL;
+ }
+
+ // get the sdk against which the project is built
+ IAndroidTarget projectTarget = currentSdk.getTarget(project);
+
+ // get the min required android version
+ ManifestInfo mi = ManifestInfo.get(project);
+ final int minApiLevel = mi.getMinSdkVersion();
+ final String minApiCodeName = mi.getMinSdkCodeName();
+ final AndroidVersion minApiVersion = new AndroidVersion(minApiLevel, minApiCodeName);
+
+ // FIXME: check errors on missing sdk, AVD manager, or project target.
+
+ // device chooser response object.
+ final DeviceChooserResponse response = new DeviceChooserResponse();
+
+ /*
+ * Launch logic:
+ * - Use Last Launched Device/AVD set.
+ * If user requested to use same device for future launches, and the last launched
+ * device/avd is still present, then simply launch on the same device/avd.
+ * - Manual Mode
+ * Always display a UI that lets a user see the current running emulators/devices.
+ * The UI must show which devices are compatibles, and allow launching new emulators
+ * with compatible (and not yet running) AVD.
+ * - Automatic Way
+ * * Preferred AVD set.
+ * If Preferred AVD is not running: launch it.
+ * Launch the application on the preferred AVD.
+ * * No preferred AVD.
+ * Count the number of compatible emulators/devices.
+ * If != 1, display a UI similar to manual mode.
+ * If == 1, launch the application on this AVD/device.
+ * - Launch on multiple devices:
+ * From the currently active devices & emulators, filter out those that cannot run
+ * the app (by api level), and launch on all the others.
+ */
+ IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
+ if (config.mReuseLastUsedDevice) {
+ // check to see if the last used device is still online
+ IDevice lastUsedDevice = getDeviceIfOnline(config.mLastUsedDevice,
+ devices);
+ if (lastUsedDevice != null) {
+ response.setDeviceToUse(lastUsedDevice);
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+ }
+
+ if (config.mTargetMode == TargetMode.AUTO) {
+ // first check if we have a preferred AVD name, and if it actually exists, and is valid
+ // (ie able to run the project).
+ // We need to check this in case the AVD was recreated with a different target that is
+ // not compatible.
+ AvdInfo preferredAvd = null;
+ if (config.mAvdName != null) {
+ preferredAvd = avdManager.getAvd(config.mAvdName, true /*validAvdOnly*/);
+ }
+
+ if (preferredAvd != null) {
+ IAndroidTarget preferredAvdTarget = preferredAvd.getTarget();
+ if (preferredAvdTarget != null
+ && !preferredAvdTarget.getVersion().canRun(minApiVersion)) {
+ preferredAvd = null;
+
+ AdtPlugin.printErrorToConsole(project, String.format(
+ "Preferred AVD '%1$s' (API Level: %2$d) cannot run application with minApi %3$s. Looking for a compatible AVD...",
+ config.mAvdName,
+ preferredAvdTarget.getVersion().getApiLevel(),
+ minApiVersion));
+ }
+ }
+
+ if (preferredAvd != null) {
+ // We have a preferred avd that can actually run the application.
+ // Now see if the AVD is running, and if so use it, otherwise launch it.
+
+ for (IDevice d : devices) {
+ String deviceAvd = d.getAvdName();
+ if (deviceAvd != null && deviceAvd.equals(config.mAvdName)) {
+ response.setDeviceToUse(d);
+
+ AdtPlugin.printToConsole(project, String.format(
+ "Automatic Target Mode: Preferred AVD '%1$s' is available on emulator '%2$s'",
+ config.mAvdName, d));
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+ }
+
+ // at this point we have a valid preferred AVD that is not running.
+ // We need to start it.
+ response.setAvdToLaunch(preferredAvd);
+
+ AdtPlugin.printToConsole(project, String.format(
+ "Automatic Target Mode: Preferred AVD '%1$s' is not available. Launching new emulator.",
+ config.mAvdName));
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+
+ // no (valid) preferred AVD? look for one.
+
+ // If the API level requested in the manifest is lower than the current project
+ // target, when we will iterate devices/avds later ideally we will want to find
+ // a device/avd which target is as close to the manifest as possible (instead of
+ // a device which target is the same as the project's target) and use it as the
+ // new default.
+
+ if (minApiCodeName != null && minApiLevel < projectTarget.getVersion().getApiLevel()) {
+ int maxDist = projectTarget.getVersion().getApiLevel() - minApiLevel;
+ IAndroidTarget candidate = null;
+
+ for (IAndroidTarget target : currentSdk.getTargets()) {
+ if (target.canRunOn(projectTarget)) {
+ int currDist = target.getVersion().getApiLevel() - minApiLevel;
+ if (currDist >= 0 && currDist < maxDist) {
+ maxDist = currDist;
+ candidate = target;
+ if (maxDist == 0) {
+ // Found a perfect match
+ break;
+ }
+ }
+ }
+ }
+
+ if (candidate != null) {
+ // We found a better SDK target candidate, that is closer to the
+ // API level from minSdkVersion than the one currently used by the
+ // project. Below (in the for...devices loop) we'll try to find
+ // a device/AVD for it.
+ projectTarget = candidate;
+ }
+ }
+
+ HashMap<IDevice, AvdInfo> compatibleRunningAvds = new HashMap<IDevice, AvdInfo>();
+ boolean hasDevice = false; // if there's 1+ device running, we may force manual mode,
+ // as we cannot always detect proper compatibility with
+ // devices. This is the case if the project target is not
+ // a standard platform
+ for (IDevice d : devices) {
+ String deviceAvd = d.getAvdName();
+ if (deviceAvd != null) { // physical devices return null.
+ AvdInfo info = avdManager.getAvd(deviceAvd, true /*validAvdOnly*/);
+ if (AvdCompatibility.canRun(info, projectTarget, minApiVersion)
+ == AvdCompatibility.Compatibility.YES) {
+ compatibleRunningAvds.put(d, info);
+ }
+ } else {
+ if (projectTarget.isPlatform()) { // means this can run on any device as long
+ // as api level is high enough
+ AndroidVersion deviceVersion = Sdk.getDeviceVersion(d);
+ // the deviceVersion may be null if it wasn't yet queried (device just
+ // plugged in or emulator just booting up.
+ if (deviceVersion != null &&
+ deviceVersion.canRun(projectTarget.getVersion())) {
+ // device is compatible with project
+ compatibleRunningAvds.put(d, null);
+ continue;
+ }
+ } else {
+ // for non project platform, we can't be sure if a device can
+ // run an application or not, since we don't query the device
+ // for the list of optional libraries that it supports.
+ }
+ hasDevice = true;
+ }
+ }
+
+ // depending on the number of devices, we'll simulate an automatic choice
+ // from the device chooser or simply show up the device chooser.
+ if (hasDevice == false && compatibleRunningAvds.size() == 0) {
+ // if zero emulators/devices, we launch an emulator.
+ // We need to figure out which AVD first.
+
+ // we are going to take the closest AVD. ie a compatible AVD that has the API level
+ // closest to the project target.
+ AvdInfo defaultAvd = findMatchingAvd(avdManager, projectTarget, minApiVersion);
+
+ if (defaultAvd != null) {
+ response.setAvdToLaunch(defaultAvd);
+
+ AdtPlugin.printToConsole(project, String.format(
+ "Automatic Target Mode: launching new emulator with compatible AVD '%1$s'",
+ defaultAvd.getName()));
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ } else {
+ AdtPlugin.printToConsole(project, String.format(
+ "Failed to find an AVD compatible with target '%1$s'.",
+ projectTarget.getName()));
+
+ final Display display = AdtPlugin.getDisplay();
+ final boolean[] searchAgain = new boolean[] { false };
+ // ask the user to create a new one.
+ display.syncExec(new Runnable() {
+ @Override
+ public void run() {
+ Shell shell = display.getActiveShell();
+ if (MessageDialog.openQuestion(shell, "Android AVD Error",
+ "No compatible targets were found. Do you wish to add a new Android Virtual Device?")) {
+ AvdManagerAction action = new AvdManagerAction();
+ action.run(null /*action*/);
+ searchAgain[0] = true;
+ }
+ }
+ });
+ if (searchAgain[0]) {
+ // attempt to reload the AVDs and find one compatible.
+ defaultAvd = findMatchingAvd(avdManager, projectTarget, minApiVersion);
+
+ if (defaultAvd == null) {
+ AdtPlugin.printErrorToConsole(project, String.format(
+ "Still no compatible AVDs with target '%1$s': Aborting launch.",
+ projectTarget.getName()));
+ stopLaunch(launchInfo);
+ } else {
+ response.setAvdToLaunch(defaultAvd);
+
+ AdtPlugin.printToConsole(project, String.format(
+ "Launching new emulator with compatible AVD '%1$s'",
+ defaultAvd.getName()));
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+ }
+ }
+ } else if (hasDevice == false && compatibleRunningAvds.size() == 1) {
+ Entry<IDevice, AvdInfo> e = compatibleRunningAvds.entrySet().iterator().next();
+ response.setDeviceToUse(e.getKey());
+
+ // get the AvdInfo, if null, the device is a physical device.
+ AvdInfo avdInfo = e.getValue();
+ if (avdInfo != null) {
+ message = String.format("Automatic Target Mode: using existing emulator '%1$s' running compatible AVD '%2$s'",
+ response.getDeviceToUse(), e.getValue().getName());
+ } else {
+ message = String.format("Automatic Target Mode: using device '%1$s'",
+ response.getDeviceToUse());
+ }
+ AdtPlugin.printToConsole(project, message);
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+
+ // if more than one device, we'll bring up the DeviceChooser dialog below.
+ if (compatibleRunningAvds.size() >= 2) {
+ message = "Automatic Target Mode: Several compatible targets. Please select a target device.";
+ } else if (hasDevice) {
+ message = "Automatic Target Mode: Unable to detect device compatibility. Please select a target device.";
+ }
+
+ AdtPlugin.printToConsole(project, message);
+ } else if ((config.mTargetMode == TargetMode.ALL_DEVICES_AND_EMULATORS
+ || config.mTargetMode == TargetMode.ALL_DEVICES
+ || config.mTargetMode == TargetMode.ALL_EMULATORS)
+ && ILaunchManager.RUN_MODE.equals(mode)) {
+ // if running on multiple devices, identify all compatible devices
+ boolean includeDevices = config.mTargetMode != TargetMode.ALL_EMULATORS;
+ boolean includeAvds = config.mTargetMode != TargetMode.ALL_DEVICES;
+ Collection<IDevice> compatibleDevices = findCompatibleDevices(devices,
+ minApiVersion, includeDevices, includeAvds);
+ if (compatibleDevices.size() == 0) {
+ AdtPlugin.printErrorToConsole(project,
+ "No active compatible AVD's or devices found. "
+ + "Relaunch this configuration after connecting a device or starting an AVD.");
+ stopLaunch(launchInfo);
+ } else {
+ multiLaunch(launchInfo, compatibleDevices);
+ }
+ return;
+ }
+
+ // bring up the device chooser.
+ final IAndroidTarget desiredProjectTarget = projectTarget;
+ final AtomicBoolean continueLaunch = new AtomicBoolean(false);
+ AdtPlugin.getDisplay().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // open the chooser dialog. It'll fill 'response' with the device to use
+ // or the AVD to launch.
+ DeviceChooserDialog dialog = new DeviceChooserDialog(
+ AdtPlugin.getShell(),
+ response, launchInfo.getPackageName(),
+ desiredProjectTarget, minApiVersion,
+ config.mReuseLastUsedDevice);
+ if (dialog.open() == Dialog.OK) {
+ updateLaunchConfigWithLastUsedDevice(launch.getLaunchConfiguration(),
+ response);
+ continueLaunch.set(true);
+ } else {
+ AdtPlugin.printErrorToConsole(project, "Launch canceled!");
+ stopLaunch(launchInfo);
+ return;
+ }
+ } catch (Exception e) {
+ // there seems to be some case where the shell will be null. (might be
+ // an OS X bug). Because of this the creation of the dialog will throw
+ // and IllegalArg exception interrupting the launch with no user feedback.
+ // So we trap all the exception and display something.
+ String msg = e.getMessage();
+ if (msg == null) {
+ msg = e.getClass().getCanonicalName();
+ }
+ AdtPlugin.printErrorToConsole(project,
+ String.format("Error during launch: %s", msg));
+ stopLaunch(launchInfo);
+ }
+ }
+ });
+
+ if (continueLaunch.get()) {
+ continueLaunch(response, project, launch, launchInfo, config);
+ }
+ }
+
+ /**
+ * Returns devices that can run a app of provided API level.
+ * @param devices list of devices to filter from
+ * @param requiredVersion minimum required API that should be supported
+ * @param includeDevices include physical devices in the filtered list
+ * @param includeAvds include emulators in the filtered list
+ * @return set of compatible devices, may be an empty set
+ */
+ private Collection<IDevice> findCompatibleDevices(IDevice[] devices,
+ AndroidVersion requiredVersion, boolean includeDevices, boolean includeAvds) {
+ Set<IDevice> compatibleDevices = new HashSet<IDevice>(devices.length);
+ AvdManager avdManager = Sdk.getCurrent().getAvdManager();
+ for (IDevice d: devices) {
+ boolean isEmulator = d.isEmulator();
+ boolean canRun = false;
+
+ if (isEmulator) {
+ if (!includeAvds) {
+ continue;
+ }
+
+ AvdInfo avdInfo = avdManager.getAvd(d.getAvdName(), true);
+ if (avdInfo != null && avdInfo.getTarget() != null) {
+ canRun = avdInfo.getTarget().getVersion().canRun(requiredVersion);
+ }
+ } else {
+ if (!includeDevices) {
+ continue;
+ }
+
+ AndroidVersion deviceVersion = Sdk.getDeviceVersion(d);
+ if (deviceVersion != null) {
+ canRun = deviceVersion.canRun(requiredVersion);
+ }
+ }
+
+ if (canRun) {
+ compatibleDevices.add(d);
+ }
+ }
+
+ return compatibleDevices;
+ }
+
+ /**
+ * Find a matching AVD.
+ * @param minApiVersion
+ */
+ private AvdInfo findMatchingAvd(AvdManager avdManager, final IAndroidTarget projectTarget,
+ AndroidVersion minApiVersion) {
+ AvdInfo[] avds = avdManager.getValidAvds();
+ AvdInfo bestAvd = null;
+ for (AvdInfo avd : avds) {
+ if (AvdCompatibility.canRun(avd, projectTarget, minApiVersion)
+ == AvdCompatibility.Compatibility.YES) {
+ // at this point we can ignore the code name issue since
+ // AvdCompatibility.canRun() will already have filtered out the non compatible AVDs.
+ if (bestAvd == null ||
+ avd.getTarget().getVersion().getApiLevel() <
+ bestAvd.getTarget().getVersion().getApiLevel()) {
+ bestAvd = avd;
+ }
+ }
+ }
+ return bestAvd;
+ }
+
+ /**
+ * Continues the launch based on the DeviceChooser response.
+ * @param response the device chooser response
+ * @param project The project being launched
+ * @param launch The eclipse launch info
+ * @param launchInfo The {@link DelayedLaunchInfo}
+ * @param config The config needed to start a new emulator.
+ */
+ private void continueLaunch(final DeviceChooserResponse response, final IProject project,
+ final AndroidLaunch launch, final DelayedLaunchInfo launchInfo,
+ final AndroidLaunchConfiguration config) {
+ if (response.getAvdToLaunch() != null) {
+ // there was no selected device, we start a new emulator.
+ synchronized (sListLock) {
+ AvdInfo info = response.getAvdToLaunch();
+ mWaitingForEmulatorLaunches.add(launchInfo);
+ AdtPlugin.printToConsole(project, String.format(
+ "Launching a new emulator with Virtual Device '%1$s'",
+ info.getName()));
+ boolean status = launchEmulator(config, info);
+
+ if (status == false) {
+ // launching the emulator failed!
+ AdtPlugin.displayError("Emulator Launch",
+ "Couldn't launch the emulator! Make sure the SDK directory is properly setup and the emulator is not missing.");
+
+ // stop the launch and return
+ mWaitingForEmulatorLaunches.remove(launchInfo);
+ AdtPlugin.printErrorToConsole(project, "Launch canceled!");
+ stopLaunch(launchInfo);
+ return;
+ }
+
+ return;
+ }
+ } else if (response.getDeviceToUse() != null) {
+ launchInfo.setDevice(response.getDeviceToUse());
+ simpleLaunch(launchInfo, launchInfo.getDevice());
+ }
+ }
+
+ /**
+ * Queries for a debugger port for a specific {@link ILaunchConfiguration}.
+ * <p/>
+ * If the configuration and a debugger port where added through
+ * {@link #setPortLaunchConfigAssociation(ILaunchConfiguration, int)}, then this method
+ * will return the debugger port, and remove the configuration from the list.
+ * @param launchConfig the {@link ILaunchConfiguration}
+ * @return the debugger port or {@link LaunchConfigDelegate#INVALID_DEBUG_PORT} if the
+ * configuration was not setup.
+ */
+ static int getPortForConfig(ILaunchConfiguration launchConfig) {
+ synchronized (sListLock) {
+ Integer port = sRunningAppMap.get(launchConfig);
+ if (port != null) {
+ sRunningAppMap.remove(launchConfig);
+ return port;
+ }
+ }
+
+ return LaunchConfigDelegate.INVALID_DEBUG_PORT;
+ }
+
+ /**
+ * Set a {@link ILaunchConfiguration} and its associated debug port, in the list of
+ * launch config to connect directly to a running app instead of doing full launch (sync,
+ * launch, and connect to).
+ * @param launchConfig the {@link ILaunchConfiguration} object.
+ * @param port The debugger port to connect to.
+ */
+ private static void setPortLaunchConfigAssociation(ILaunchConfiguration launchConfig,
+ int port) {
+ synchronized (sListLock) {
+ sRunningAppMap.put(launchConfig, port);
+ }
+ }
+
+ /**
+ * Checks the build information, and returns whether the launch should continue.
+ * <p/>The value tested are:
+ * <ul>
+ * <li>Minimum API version requested by the application. If the target device does not match,
+ * the launch is canceled.</li>
+ * <li>Debuggable attribute of the application and whether or not the device requires it. If
+ * the device requires it and it is not set in the manifest, the launch will be forced to
+ * "release" mode instead of "debug"</li>
+ * <ul>
+ */
+ private boolean checkBuildInfo(DelayedLaunchInfo launchInfo, IDevice device) {
+ if (device != null) {
+ // check the app required API level versus the target device API level
+
+ String deviceVersion = device.getProperty(IDevice.PROP_BUILD_VERSION);
+ String deviceApiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL);
+ String deviceCodeName = device.getProperty(IDevice.PROP_BUILD_CODENAME);
+
+ int deviceApiLevel = -1;
+ try {
+ deviceApiLevel = Integer.parseInt(deviceApiLevelString);
+ } catch (NumberFormatException e) {
+ // pass, we'll keep the apiLevel value at -1.
+ }
+
+ String requiredApiString = launchInfo.getRequiredApiVersionNumber();
+ if (requiredApiString != null) {
+ int requiredApi = -1;
+ try {
+ requiredApi = Integer.parseInt(requiredApiString);
+ } catch (NumberFormatException e) {
+ // pass, we'll keep requiredApi value at -1.
+ }
+
+ if (requiredApi == -1) {
+ // this means the manifest uses a codename for minSdkVersion
+ // check that the device is using the same codename
+ if (requiredApiString.equals(deviceCodeName) == false) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format(
+ "ERROR: Application requires a device running '%1$s'!",
+ requiredApiString));
+ return false;
+ }
+ } else {
+ // app requires a specific API level
+ if (deviceApiLevel == -1) {
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ "WARNING: Unknown device API version!");
+ } else if (deviceApiLevel < requiredApi) {
+ String msg = String.format(
+ "ERROR: Application requires API version %1$d. Device API version is %2$d (Android %3$s).",
+ requiredApi, deviceApiLevel, deviceVersion);
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);
+
+ // abort the launch
+ return false;
+ }
+ }
+ } else {
+ // warn the application API level requirement is not set.
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "WARNING: Application does not specify an API level requirement!");
+
+ // and display the target device API level (if known)
+ if (deviceApiLevel == -1) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "WARNING: Unknown device API version!");
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format(
+ "Device API version is %1$d (Android %2$s)", deviceApiLevel,
+ deviceVersion));
+ }
+ }
+
+ // now checks that the device/app can be debugged (if needed)
+ if (device.isEmulator() == false && launchInfo.isDebugMode()) {
+ String debuggableDevice = device.getProperty(IDevice.PROP_DEBUGGABLE);
+ if (debuggableDevice != null && debuggableDevice.equals("0")) { //$NON-NLS-1$
+ // the device is "secure" and requires apps to declare themselves as debuggable!
+ // launchInfo.getDebuggable() will return null if the manifest doesn't declare
+ // anything. In this case this is fine since the build system does insert
+ // debuggable=true. The only case to look for is if false is manually set
+ // in the manifest.
+ if (launchInfo.getDebuggable() == Boolean.FALSE) {
+ String message = String.format("Application '%1$s' has its 'debuggable' attribute set to FALSE and cannot be debugged.",
+ launchInfo.getPackageName());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), message);
+
+ // because am -D does not check for ro.debuggable and the
+ // 'debuggable' attribute, it is important we do not use the -D option
+ // in this case or the app will wait for a debugger forever and never
+ // really launch.
+ launchInfo.setDebugMode(false);
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Do a simple launch on the specified device, attempting to sync the new
+ * package, and then launching the application. Failed sync/launch will
+ * stop the current AndroidLaunch and return false;
+ * @param launchInfo
+ * @param device
+ * @return true if succeed
+ */
+ private boolean simpleLaunch(DelayedLaunchInfo launchInfo, IDevice device) {
+ if (!doPreLaunchActions(launchInfo, device)) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!");
+ stopLaunch(launchInfo);
+ return false;
+ }
+
+ // launch the app
+ launchApp(launchInfo, device);
+
+ return true;
+ }
+
+ private boolean doPreLaunchActions(DelayedLaunchInfo launchInfo, IDevice device) {
+ // API level check
+ if (!checkBuildInfo(launchInfo, device)) {
+ return false;
+ }
+
+ // sync app
+ if (!syncApp(launchInfo, device)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void multiLaunch(DelayedLaunchInfo launchInfo, Collection<IDevice> devices) {
+ for (IDevice d: devices) {
+ boolean success = doPreLaunchActions(launchInfo, d);
+ if (!success) {
+ String deviceName = d.isEmulator() ? d.getAvdName() : d.getSerialNumber();
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Launch failed on device: " + deviceName);
+ continue;
+ }
+ }
+
+ doLaunchAction(launchInfo, devices);
+
+ // multiple launches are only supported for run configuration, so we can terminate
+ // the launch itself
+ stopLaunch(launchInfo);
+ }
+
+ /**
+ * If needed, syncs the application and all its dependencies on the device/emulator.
+ *
+ * @param launchInfo The Launch information object.
+ * @param device the device on which to sync the application
+ * @return true if the install succeeded.
+ */
+ private boolean syncApp(DelayedLaunchInfo launchInfo, IDevice device) {
+ boolean alreadyInstalled = ApkInstallManager.getInstance().isApplicationInstalled(
+ launchInfo.getProject(), launchInfo.getPackageName(), device);
+
+ if (alreadyInstalled) {
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ "Application already deployed. No need to reinstall.");
+ } else {
+ if (doSyncApp(launchInfo, device) == false) {
+ return false;
+ }
+ }
+
+ // The app is now installed, now try the dependent projects
+ for (DelayedLaunchInfo dependentLaunchInfo : getDependenciesLaunchInfo(launchInfo)) {
+ String msg = String.format("Project dependency found, installing: %s",
+ dependentLaunchInfo.getProject().getName());
+ AdtPlugin.printToConsole(launchInfo.getProject(), msg);
+ if (syncApp(dependentLaunchInfo, device) == false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Syncs the application on the device/emulator.
+ *
+ * @param launchInfo The Launch information object.
+ * @param device the device on which to sync the application
+ * @return true if the install succeeded.
+ */
+ private boolean doSyncApp(DelayedLaunchInfo launchInfo, IDevice device) {
+ IPath path = launchInfo.getPackageFile().getLocation();
+ String fileName = path.lastSegment();
+ try {
+ String message = String.format("Uploading %1$s onto device '%2$s'",
+ fileName, device.getSerialNumber());
+ AdtPlugin.printToConsole(launchInfo.getProject(), message);
+
+ String remotePackagePath = device.syncPackageToDevice(path.toOSString());
+ boolean installResult = installPackage(launchInfo, remotePackagePath, device);
+ device.removeRemotePackage(remotePackagePath);
+
+ // if the installation succeeded, we register it.
+ if (installResult) {
+ ApkInstallManager.getInstance().registerInstallation(
+ launchInfo.getProject(), launchInfo.getPackageName(), device);
+ }
+ return installResult;
+ }
+ catch (IOException e) {
+ String msg = String.format("Failed to install %1$s on device '%2$s': %3$s", fileName,
+ device.getSerialNumber(), e.getMessage());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
+ } catch (TimeoutException e) {
+ String msg = String.format("Failed to install %1$s on device '%2$s': timeout", fileName,
+ device.getSerialNumber());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);
+ } catch (AdbCommandRejectedException e) {
+ String msg = String.format(
+ "Failed to install %1$s on device '%2$s': adb rejected install command with: %3$s",
+ fileName, device.getSerialNumber(), e.getMessage());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
+ } catch (CanceledException e) {
+ if (e.wasCanceled()) {
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ String.format("Install of %1$s canceled", fileName));
+ } else {
+ String msg = String.format("Failed to install %1$s on device '%2$s': %3$s",
+ fileName, device.getSerialNumber(), e.getMessage());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * For the current launchInfo, create additional DelayedLaunchInfo that should be used to
+ * sync APKs that we are dependent on to the device.
+ *
+ * @param launchInfo the original launch info that we want to find the
+ * @return a list of DelayedLaunchInfo (may be empty if no dependencies were found or error)
+ */
+ public List<DelayedLaunchInfo> getDependenciesLaunchInfo(DelayedLaunchInfo launchInfo) {
+ List<DelayedLaunchInfo> dependencies = new ArrayList<DelayedLaunchInfo>();
+
+ // Convert to equivalent JavaProject
+ IJavaProject javaProject;
+ try {
+ //assuming this is an Android (and Java) project since it is attached to the launchInfo.
+ javaProject = BaseProjectHelper.getJavaProject(launchInfo.getProject());
+ } catch (CoreException e) {
+ // return empty dependencies
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
+ return dependencies;
+ }
+
+ // Get all projects that this depends on
+ List<IJavaProject> androidProjectList;
+ try {
+ androidProjectList = ProjectHelper.getAndroidProjectDependencies(javaProject);
+ } catch (JavaModelException e) {
+ // return empty dependencies
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
+ return dependencies;
+ }
+
+ // for each project, parse manifest and create launch information
+ for (IJavaProject androidProject : androidProjectList) {
+ // Parse the Manifest to get various required information
+ // copied from LaunchConfigDelegate
+ ManifestData manifestData = AndroidManifestHelper.parseForData(
+ androidProject.getProject());
+
+ if (manifestData == null) {
+ continue;
+ }
+
+ // Get the APK location (can return null)
+ IFile apk = ProjectHelper.getApplicationPackage(androidProject.getProject());
+ if (apk == null) {
+ // getApplicationPackage will have logged an error message
+ continue;
+ }
+
+ // Create new launchInfo as an hybrid between parent and dependency information
+ DelayedLaunchInfo delayedLaunchInfo = new DelayedLaunchInfo(
+ androidProject.getProject(),
+ manifestData.getPackage(),
+ manifestData.getPackage(),
+ launchInfo.getLaunchAction(),
+ apk,
+ manifestData.getDebuggable(),
+ manifestData.getMinSdkVersionString(),
+ launchInfo.getLaunch(),
+ launchInfo.getMonitor());
+
+ // Add to the list
+ dependencies.add(delayedLaunchInfo);
+ }
+
+ return dependencies;
+ }
+
+ /**
+ * Installs the application package on the device, and handles return result
+ * @param launchInfo The launch information
+ * @param remotePath The remote path of the package.
+ * @param device The device on which the launch is done.
+ */
+ private boolean installPackage(DelayedLaunchInfo launchInfo, final String remotePath,
+ final IDevice device) {
+ String message = String.format("Installing %1$s...", launchInfo.getPackageFile().getName());
+ AdtPlugin.printToConsole(launchInfo.getProject(), message);
+ try {
+ // try a reinstall first, because the most common case is the app is already installed
+ String result = doInstall(launchInfo, remotePath, device, true /* reinstall */);
+
+ /* For now we force to retry the install (after uninstalling) because there's no
+ * other way around it: adb install does not want to update a package w/o uninstalling
+ * the old one first!
+ */
+ return checkInstallResult(result, device, launchInfo, remotePath,
+ InstallRetryMode.ALWAYS);
+ } catch (Exception e) {
+ String msg = String.format(
+ "Failed to install %1$s on device '%2$s!",
+ launchInfo.getPackageFile().getName(), device.getSerialNumber());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e.getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks the result of an installation, and takes optional actions based on it.
+ * @param result the result string from the installation
+ * @param device the device on which the installation occured.
+ * @param launchInfo the {@link DelayedLaunchInfo}
+ * @param remotePath the temporary path of the package on the device
+ * @param retryMode indicates what to do in case, a package already exists.
+ * @return <code>true<code> if success, <code>false</code> otherwise.
+ * @throws InstallException
+ */
+ private boolean checkInstallResult(String result, IDevice device, DelayedLaunchInfo launchInfo,
+ String remotePath, InstallRetryMode retryMode) throws InstallException {
+ if (result == null) {
+ AdtPlugin.printToConsole(launchInfo.getProject(), "Success!");
+ return true;
+ }
+ else if (result.equals("INSTALL_FAILED_ALREADY_EXISTS")) { //$NON-NLS-1$
+ // this should never happen, since reinstall mode is used on the first attempt
+ if (retryMode == InstallRetryMode.PROMPT) {
+ boolean prompt = AdtPlugin.displayPrompt("Application Install",
+ "A previous installation needs to be uninstalled before the new package can be installed.\nDo you want to uninstall?");
+ if (prompt) {
+ retryMode = InstallRetryMode.ALWAYS;
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Installation error! The package already exists.");
+ return false;
+ }
+ }
+
+ if (retryMode == InstallRetryMode.ALWAYS) {
+ /*
+ * TODO: create a UI that gives the dev the choice to:
+ * - clean uninstall on launch
+ * - full uninstall if application exists.
+ * - soft uninstall if application exists (keeps the app data around).
+ * - always ask (choice of soft-reinstall, full reinstall)
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "Application already exists, uninstalling...");
+ String res = doUninstall(device, launchInfo);
+ if (res == null) {
+ AdtPlugin.printToConsole(launchInfo.mProject, "Success!");
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ String.format("Failed to uninstall: %1$s", res));
+ return false;
+ }
+ */
+
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ "Application already exists. Attempting to re-install instead...");
+ String res = doInstall(launchInfo, remotePath, device, true /* reinstall */ );
+ return checkInstallResult(res, device, launchInfo, remotePath,
+ InstallRetryMode.NEVER);
+ }
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Installation error! The package already exists.");
+ } else if (result.equals("INSTALL_FAILED_INVALID_APK")) { //$NON-NLS-1$
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Installation failed due to invalid APK file!",
+ "Please check logcat output for more details.");
+ } else if (result.equals("INSTALL_FAILED_INVALID_URI")) { //$NON-NLS-1$
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Installation failed due to invalid URI!",
+ "Please check logcat output for more details.");
+ } else if (result.equals("INSTALL_FAILED_COULDNT_COPY")) { //$NON-NLS-1$
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format("Installation failed: Could not copy %1$s to its final location!",
+ launchInfo.getPackageFile().getName()),
+ "Please check logcat output for more details.");
+ } else if (result.equals("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES")) { //$NON-NLS-1$
+ if (retryMode != InstallRetryMode.NEVER) {
+ boolean prompt = AdtPlugin.displayPrompt("Application Install",
+ "Re-installation failed due to different application signatures. You must perform a full uninstall of the application. WARNING: This will remove the application data!\nDo you want to uninstall?");
+ if (prompt) {
+ doUninstall(device, launchInfo);
+ String res = doInstall(launchInfo, remotePath, device, false);
+ return checkInstallResult(res, device, launchInfo, remotePath,
+ InstallRetryMode.NEVER);
+ }
+ }
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Re-installation failed due to different application signatures.",
+ "You must perform a full uninstall of the application. WARNING: This will remove the application data!",
+ String.format("Please execute 'adb uninstall %1$s' in a shell.", launchInfo.getPackageName()));
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format("Installation error: %1$s", result),
+ "Please check logcat output for more details.");
+ }
+
+ return false;
+ }
+
+ /**
+ * Performs the uninstallation of an application.
+ * @param device the device on which to install the application.
+ * @param launchInfo the {@link DelayedLaunchInfo}.
+ * @return a {@link String} with an error code, or <code>null</code> if success.
+ * @throws InstallException if the installation failed.
+ */
+ private String doUninstall(IDevice device, DelayedLaunchInfo launchInfo)
+ throws InstallException {
+ try {
+ return device.uninstallPackage(launchInfo.getPackageName());
+ } catch (InstallException e) {
+ String msg = String.format(
+ "Failed to uninstall %1$s: %2$s", launchInfo.getPackageName(), e.getMessage());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);
+ throw e;
+ }
+ }
+
+ /**
+ * Performs the installation of an application whose package has been uploaded on the device.
+ *
+ * @param launchInfo the {@link DelayedLaunchInfo}.
+ * @param remotePath the path of the application package in the device tmp folder.
+ * @param device the device on which to install the application.
+ * @param reinstall
+ * @return a {@link String} with an error code, or <code>null</code> if success.
+ * @throws InstallException if the uninstallation failed.
+ */
+ private String doInstall(DelayedLaunchInfo launchInfo, final String remotePath,
+ final IDevice device, boolean reinstall) throws InstallException {
+ return device.installRemotePackage(remotePath, reinstall);
+ }
+
+ /**
+ * launches an application on a device or emulator
+ *
+ * @param info the {@link DelayedLaunchInfo} that indicates the launch action
+ * @param device the device or emulator to launch the application on
+ */
+ @Override
+ public void launchApp(final DelayedLaunchInfo info, IDevice device) {
+ if (info.isDebugMode()) {
+ synchronized (sListLock) {
+ if (mWaitingForDebuggerApplications.contains(info) == false) {
+ mWaitingForDebuggerApplications.add(info);
+ }
+ }
+ }
+ if (doLaunchAction(info, device)) {
+ // if the app is not a debug app, we need to do some clean up, as
+ // the process is done!
+ if (info.isDebugMode() == false) {
+ // stop the launch object, since there's no debug, and it can't
+ // provide any control over the app
+ stopLaunch(info);
+ }
+ } else {
+ // something went wrong or no further launch action needed
+ // lets stop the Launch
+ stopLaunch(info);
+ }
+ }
+
+ private boolean doLaunchAction(final DelayedLaunchInfo info, Collection<IDevice> devices) {
+ boolean result = info.getLaunchAction().doLaunchAction(info, devices);
+
+ // Monitor the logcat output on the launched device to notify
+ // the user if any significant error occurs that is visible from logcat
+ for (IDevice d : devices) {
+ DdmsPlugin.getDefault().startLogCatMonitor(d);
+ }
+
+ return result;
+ }
+
+ private boolean doLaunchAction(final DelayedLaunchInfo info, IDevice device) {
+ return doLaunchAction(info, Collections.singletonList(device));
+ }
+
+ private boolean launchEmulator(AndroidLaunchConfiguration config, AvdInfo avdToLaunch) {
+
+ // split the custom command line in segments
+ ArrayList<String> customArgs = new ArrayList<String>();
+ boolean hasWipeData = false;
+ if (config.mEmulatorCommandLine != null && config.mEmulatorCommandLine.length() > 0) {
+ String[] segments = config.mEmulatorCommandLine.split("\\s+"); //$NON-NLS-1$
+
+ // we need to remove the empty strings
+ for (String s : segments) {
+ if (s.length() > 0) {
+ customArgs.add(s);
+ if (!hasWipeData && s.equals(FLAG_WIPE_DATA)) {
+ hasWipeData = true;
+ }
+ }
+ }
+ }
+
+ boolean needsWipeData = config.mWipeData && !hasWipeData;
+ if (needsWipeData) {
+ if (!AdtPlugin.displayPrompt("Android Launch", "Are you sure you want to wipe all user data when starting this emulator?")) {
+ needsWipeData = false;
+ }
+ }
+
+ // build the command line based on the available parameters.
+ ArrayList<String> list = new ArrayList<String>();
+
+ String path = AdtPlugin.getOsAbsoluteEmulator();
+
+ list.add(path);
+
+ list.add(FLAG_AVD);
+ list.add(avdToLaunch.getName());
+
+ if (config.mNetworkSpeed != null) {
+ list.add(FLAG_NETSPEED);
+ list.add(config.mNetworkSpeed);
+ }
+
+ if (config.mNetworkDelay != null) {
+ list.add(FLAG_NETDELAY);
+ list.add(config.mNetworkDelay);
+ }
+
+ if (needsWipeData) {
+ list.add(FLAG_WIPE_DATA);
+ }
+
+ if (config.mNoBootAnim) {
+ list.add(FLAG_NO_BOOT_ANIM);
+ }
+
+ list.addAll(customArgs);
+
+ // convert the list into an array for the call to exec.
+ String[] command = list.toArray(new String[list.size()]);
+
+ // launch the emulator
+ try {
+ Process process = Runtime.getRuntime().exec(command);
+ grabEmulatorOutput(process);
+ } catch (IOException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Looks for and returns an existing {@link ILaunchConfiguration} object for a
+ * specified project.
+ * @param manager The {@link ILaunchManager}.
+ * @param type The {@link ILaunchConfigurationType}.
+ * @param projectName The name of the project
+ * @return an existing <code>ILaunchConfiguration</code> object matching the project, or
+ * <code>null</code>.
+ */
+ private static ILaunchConfiguration findConfig(ILaunchManager manager,
+ ILaunchConfigurationType type, String projectName) {
+ try {
+ ILaunchConfiguration[] configs = manager.getLaunchConfigurations(type);
+
+ for (ILaunchConfiguration config : configs) {
+ if (config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ "").equals(projectName)) { //$NON-NLS-1$
+ return config;
+ }
+ }
+ } catch (CoreException e) {
+ MessageDialog.openError(AdtPlugin.getShell(),
+ "Launch Error", e.getStatus().getMessage());
+ }
+
+ // didn't find anything that matches. Return null
+ return null;
+ }
+
+
+ /**
+ * Connects a remote debugger on the specified port.
+ * @param debugPort The port to connect the debugger to
+ * @param launch The associated AndroidLaunch object.
+ * @param monitor A Progress monitor
+ * @return false if cancelled by the monitor
+ * @throws CoreException
+ */
+ @SuppressWarnings("deprecation")
+ public static boolean connectRemoteDebugger(int debugPort,
+ AndroidLaunch launch, IProgressMonitor monitor)
+ throws CoreException {
+ // get some default parameters.
+ int connectTimeout = JavaRuntime.getPreferences().getInt(JavaRuntime.PREF_CONNECT_TIMEOUT);
+
+ HashMap<String, String> newMap = new HashMap<String, String>();
+
+ newMap.put("hostname", "localhost"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ newMap.put("port", Integer.toString(debugPort)); //$NON-NLS-1$
+
+ newMap.put("timeout", Integer.toString(connectTimeout));
+
+ // get the default VM connector
+ IVMConnector connector = JavaRuntime.getDefaultVMConnector();
+
+ // connect to remote VM
+ connector.connect(newMap, monitor, launch);
+
+ // check for cancellation
+ if (monitor.isCanceled()) {
+ IDebugTarget[] debugTargets = launch.getDebugTargets();
+ for (IDebugTarget target : debugTargets) {
+ if (target.canDisconnect()) {
+ target.disconnect();
+ }
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Launch a new thread that connects a remote debugger on the specified port.
+ * @param debugPort The port to connect the debugger to
+ * @param androidLaunch The associated AndroidLaunch object.
+ * @param monitor A Progress monitor
+ * @see #connectRemoteDebugger(int, AndroidLaunch, IProgressMonitor)
+ */
+ public static void launchRemoteDebugger(final int debugPort, final AndroidLaunch androidLaunch,
+ final IProgressMonitor monitor) {
+ new Thread("Debugger connection") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ try {
+ connectRemoteDebugger(debugPort, androidLaunch, monitor);
+ } catch (CoreException e) {
+ androidLaunch.stopLaunch();
+ }
+ monitor.done();
+ }
+ }.start();
+ }
+
+ /**
+ * Sent when a new {@link AndroidDebugBridge} is started.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param bridge the new {@link AndroidDebugBridge} object.
+ *
+ * @see IDebugBridgeChangeListener#bridgeChanged(AndroidDebugBridge)
+ */
+ @Override
+ public void bridgeChanged(AndroidDebugBridge bridge) {
+ // The adb server has changed. We cancel any pending launches.
+ String message = "adb server change: cancelling '%1$s'!";
+ synchronized (sListLock) {
+ for (DelayedLaunchInfo launchInfo : mWaitingForReadyEmulatorList) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format(message, launchInfo.getLaunchAction().getLaunchDescription()));
+ stopLaunch(launchInfo);
+ }
+ for (DelayedLaunchInfo launchInfo : mWaitingForDebuggerApplications) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format(message,
+ launchInfo.getLaunchAction().getLaunchDescription()));
+ stopLaunch(launchInfo);
+ }
+
+ mWaitingForReadyEmulatorList.clear();
+ mWaitingForDebuggerApplications.clear();
+ }
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceConnected(IDevice)
+ */
+ @Override
+ public void deviceConnected(IDevice device) {
+ synchronized (sListLock) {
+ // look if there's an app waiting for a device
+ if (mWaitingForEmulatorLaunches.size() > 0) {
+ // get/remove first launch item from the list
+ // FIXME: what if we have multiple launches waiting?
+ DelayedLaunchInfo launchInfo = mWaitingForEmulatorLaunches.get(0);
+ mWaitingForEmulatorLaunches.remove(0);
+
+ // give the launch item its device for later use.
+ launchInfo.setDevice(device);
+
+ // and move it to the other list
+ mWaitingForReadyEmulatorList.add(launchInfo);
+
+ // and tell the user about it
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ String.format("New emulator found: %1$s", device.getSerialNumber()));
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ String.format("Waiting for HOME ('%1$s') to be launched...",
+ AdtPlugin.getDefault().getPreferenceStore().getString(
+ AdtPrefs.PREFS_HOME_PACKAGE)));
+ }
+ }
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceDisconnected(IDevice)
+ */
+ @Override
+ public void deviceDisconnected(IDevice device) {
+ // any pending launch on this device must be canceled.
+ String message = "%1$s disconnected! Cancelling '%2$s'!";
+ synchronized (sListLock) {
+ ArrayList<DelayedLaunchInfo> copyList =
+ (ArrayList<DelayedLaunchInfo>) mWaitingForReadyEmulatorList.clone();
+ for (DelayedLaunchInfo launchInfo : copyList) {
+ if (launchInfo.getDevice() == device) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format(message, device.getSerialNumber(),
+ launchInfo.getLaunchAction().getLaunchDescription()));
+ stopLaunch(launchInfo);
+ }
+ }
+ copyList = (ArrayList<DelayedLaunchInfo>) mWaitingForDebuggerApplications.clone();
+ for (DelayedLaunchInfo launchInfo : copyList) {
+ if (launchInfo.getDevice() == device) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format(message, device.getSerialNumber(),
+ launchInfo.getLaunchAction().getLaunchDescription()));
+ stopLaunch(launchInfo);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sent when a device data changed, or when clients are started/terminated on the device.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the device that was updated.
+ * @param changeMask the mask indicating what changed.
+ *
+ * @see IDeviceChangeListener#deviceChanged(IDevice, int)
+ */
+ @Override
+ public void deviceChanged(IDevice device, int changeMask) {
+ // We could check if any starting device we care about is now ready, but we can wait for
+ // its home app to show up, so...
+ }
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ @Override
+ public void clientChanged(final Client client, int changeMask) {
+ boolean connectDebugger = false;
+ if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) {
+ String applicationName = client.getClientData().getClientDescription();
+ if (applicationName != null) {
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+ String home = store.getString(AdtPrefs.PREFS_HOME_PACKAGE);
+
+ if (home.equals(applicationName)) {
+
+ // looks like home is up, get its device
+ IDevice device = client.getDevice();
+
+ // look for application waiting for home
+ synchronized (sListLock) {
+ for (int i = 0; i < mWaitingForReadyEmulatorList.size(); ) {
+ DelayedLaunchInfo launchInfo = mWaitingForReadyEmulatorList.get(i);
+ if (launchInfo.getDevice() == device) {
+ // it's match, remove from the list
+ mWaitingForReadyEmulatorList.remove(i);
+
+ // We couldn't check earlier the API level of the device
+ // (it's asynchronous when the device boot, and usually
+ // deviceConnected is called before it's queried for its build info)
+ // so we check now
+ if (checkBuildInfo(launchInfo, device) == false) {
+ // device is not the proper API!
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Launch canceled!");
+ stopLaunch(launchInfo);
+ return;
+ }
+
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ String.format("HOME is up on device '%1$s'",
+ device.getSerialNumber()));
+
+ // attempt to sync the new package onto the device.
+ if (syncApp(launchInfo, device)) {
+ // application package is sync'ed, lets attempt to launch it.
+ launchApp(launchInfo, device);
+ } else {
+ // failure! Cancel and return
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Launch canceled!");
+ stopLaunch(launchInfo);
+ }
+
+ break;
+ } else {
+ i++;
+ }
+ }
+ }
+ }
+
+ // check if it's already waiting for a debugger, and if so we connect to it.
+ if (client.getClientData().getDebuggerConnectionStatus() == DebuggerStatus.WAITING) {
+ // search for this client in the list;
+ synchronized (sListLock) {
+ int index = mUnknownClientsWaitingForDebugger.indexOf(client);
+ if (index != -1) {
+ connectDebugger = true;
+ mUnknownClientsWaitingForDebugger.remove(client);
+ }
+ }
+ }
+ }
+ }
+
+ // if it's not home, it could be an app that is now in debugger mode that we're waiting for
+ // lets check it
+
+ if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) == Client.CHANGE_DEBUGGER_STATUS) {
+ ClientData clientData = client.getClientData();
+ String applicationName = client.getClientData().getClientDescription();
+ if (clientData.getDebuggerConnectionStatus() == DebuggerStatus.WAITING) {
+ // Get the application name, and make sure its valid.
+ if (applicationName == null) {
+ // looks like we don't have the client yet, so we keep it around for when its
+ // name becomes available.
+ synchronized (sListLock) {
+ mUnknownClientsWaitingForDebugger.add(client);
+ }
+ return;
+ } else {
+ connectDebugger = true;
+ }
+ }
+ }
+
+ if (connectDebugger) {
+ Log.d("adt", "Debugging " + client);
+ // now check it against the apps waiting for a debugger
+ String applicationName = client.getClientData().getClientDescription();
+ Log.d("adt", "App Name: " + applicationName);
+ synchronized (sListLock) {
+ for (int i = 0; i < mWaitingForDebuggerApplications.size(); ) {
+ final DelayedLaunchInfo launchInfo = mWaitingForDebuggerApplications.get(i);
+ if (client.getDevice() == launchInfo.getDevice() &&
+ applicationName.equals(launchInfo.getDebugPackageName())) {
+ // this is a match. We remove the launch info from the list
+ mWaitingForDebuggerApplications.remove(i);
+
+ // and connect the debugger.
+ String msg = String.format(
+ "Attempting to connect debugger to '%1$s' on port %2$d",
+ launchInfo.getDebugPackageName(), client.getDebuggerListenPort());
+ AdtPlugin.printToConsole(launchInfo.getProject(), msg);
+
+ new Thread("Debugger Connection") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ try {
+ if (connectRemoteDebugger(
+ client.getDebuggerListenPort(),
+ launchInfo.getLaunch(),
+ launchInfo.getMonitor()) == false) {
+ return;
+ }
+ } catch (CoreException e) {
+ // well something went wrong.
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format("Launch error: %s", e.getMessage()));
+ // stop the launch
+ stopLaunch(launchInfo);
+ }
+
+ launchInfo.getMonitor().done();
+ }
+ }.start();
+
+ // we're done processing this client.
+ return;
+
+ } else {
+ i++;
+ }
+ }
+ }
+
+ // if we get here, we haven't found an app that we were launching, so we look
+ // for opened android projects that contains the app asking for a debugger.
+ // If we find one, we automatically connect to it.
+ IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName);
+
+ if (project != null) {
+ debugRunningApp(project, client.getDebuggerListenPort());
+ }
+ }
+ }
+
+ /**
+ * Get the stderr/stdout outputs of a process and return when the process is done.
+ * Both <b>must</b> be read or the process will block on windows.
+ * @param process The process to get the output from
+ */
+ private void grabEmulatorOutput(final Process process) {
+ // read the lines as they come. if null is returned, it's
+ // because the process finished
+ new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ // create a buffer to read the stderr output
+ InputStreamReader is = new InputStreamReader(process.getErrorStream());
+ BufferedReader errReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = errReader.readLine();
+ if (line != null) {
+ AdtPlugin.printErrorToConsole("Emulator", line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ }.start();
+
+ new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ InputStreamReader is = new InputStreamReader(process.getInputStream());
+ BufferedReader outReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = outReader.readLine();
+ if (line != null) {
+ AdtPlugin.printToConsole("Emulator", line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ }.start();
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ide.eclipse.adt.launch.ILaunchController#stopLaunch(com.android.ide.eclipse.adt.launch.AndroidLaunchController.DelayedLaunchInfo)
+ */
+ @Override
+ public void stopLaunch(DelayedLaunchInfo launchInfo) {
+ launchInfo.getLaunch().stopLaunch();
+ synchronized (sListLock) {
+ mWaitingForReadyEmulatorList.remove(launchInfo);
+ mWaitingForDebuggerApplications.remove(launchInfo);
+ }
+ }
+
+ public static void updateLaunchConfigWithLastUsedDevice(
+ ILaunchConfiguration launchConfiguration, DeviceChooserResponse response) {
+ try {
+ boolean configModified = false;
+ boolean reuse = launchConfiguration.getAttribute(
+ LaunchConfigDelegate.ATTR_REUSE_LAST_USED_DEVICE, false);
+ String serial = launchConfiguration.getAttribute(
+ LaunchConfigDelegate.ATTR_LAST_USED_DEVICE, (String)null);
+
+ ILaunchConfigurationWorkingCopy wc = launchConfiguration.getWorkingCopy();
+ if (reuse != response.useDeviceForFutureLaunches()) {
+ reuse = response.useDeviceForFutureLaunches();
+ wc.setAttribute(LaunchConfigDelegate.ATTR_REUSE_LAST_USED_DEVICE, reuse);
+ configModified = true;
+ }
+
+ if (reuse) {
+ String selected = getSerial(response);
+ if (selected != null && !selected.equalsIgnoreCase(serial)) {
+ wc.setAttribute(LaunchConfigDelegate.ATTR_LAST_USED_DEVICE, selected);
+ configModified = true;
+ }
+ }
+
+ if (configModified) {
+ wc.doSave();
+ }
+ } catch (CoreException e) {
+ // in such a case, users just won't see this setting take effect
+ return;
+ }
+ }
+
+ private static String getSerial(DeviceChooserResponse response) {
+ AvdInfo avd = response.getAvdToLaunch();
+ return (avd != null) ? avd.getName() : response.getDeviceToUse().getSerialNumber();
+ }
+
+ @Nullable
+ public static IDevice getDeviceIfOnline(@Nullable String serial,
+ @NonNull IDevice[] onlineDevices) {
+ if (serial == null) {
+ return null;
+ }
+
+ for (IDevice device : onlineDevices) {
+ if (serial.equals(device.getAvdName()) ||
+ serial.equals(device.getSerialNumber())) {
+ return device;
+ }
+ }
+
+ return null;
+ }
+}