| /* |
| * 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.launch; |
| |
| 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.Client; |
| import com.android.ddmlib.ClientData; |
| import com.android.ddmlib.Device; |
| import com.android.ddmlib.IDevice; |
| import com.android.ddmlib.Log; |
| import com.android.ddmlib.MultiLineReceiver; |
| import com.android.ddmlib.SyncService; |
| import com.android.ddmlib.SyncService.SyncResult; |
| import com.android.ide.eclipse.adt.AdtPlugin; |
| import com.android.ide.eclipse.adt.launch.DelayedLaunchInfo.InstallRetryMode; |
| import com.android.ide.eclipse.adt.launch.DeviceChooserDialog.DeviceChooserResponse; |
| import com.android.ide.eclipse.adt.project.ProjectHelper; |
| import com.android.ide.eclipse.adt.sdk.Sdk; |
| import com.android.ide.eclipse.common.project.AndroidManifestParser; |
| import com.android.sdklib.IAndroidTarget; |
| import com.android.sdklib.SdkManager; |
| import com.android.sdklib.avd.AvdManager; |
| import com.android.sdklib.avd.AvdManager.AvdInfo; |
| |
| 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.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 java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.Map.Entry; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * 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#mDevice} 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(); |
| |
| |
| |
| /** |
| * Output receiver for "pm install package.apk" command line. |
| */ |
| private static final class InstallReceiver extends MultiLineReceiver { |
| |
| private static final String SUCCESS_OUTPUT = "Success"; //$NON-NLS-1$ |
| private static final Pattern FAILURE_PATTERN = Pattern.compile("Failure\\s+\\[(.*)\\]"); //$NON-NLS-1$ |
| |
| private String mSuccess = null; |
| |
| public InstallReceiver() { |
| } |
| |
| @Override |
| public void processNewLines(String[] lines) { |
| for (String line : lines) { |
| if (line.length() > 0) { |
| if (line.startsWith(SUCCESS_OUTPUT)) { |
| mSuccess = null; |
| } else { |
| Matcher m = FAILURE_PATTERN.matcher(line); |
| if (m.matches()) { |
| mSuccess = m.group(1); |
| } |
| } |
| } |
| } |
| } |
| |
| public boolean isCancelled() { |
| return false; |
| } |
| |
| public String getSuccess() { |
| return mSuccess; |
| } |
| } |
| |
| |
| /** 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); |
| |
| 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 |
| * @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) { |
| // get the launch manager |
| ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager(); |
| |
| // now get the config type for our particular android type. |
| ILaunchConfigurationType configType = manager.getLaunchConfigurationType( |
| LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID); |
| |
| 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.generateUniqueLaunchConfigurationNameFrom(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); |
| |
| // 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(AdtPlugin.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 = AndroidManifestParser.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 debuggable the debuggable value of the app, or null if not set. |
| * @param requiredApiVersionNumber the api version required by the app, or |
| * {@link AndroidManifestParser#INVALID_MIN_SDK} 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, Boolean debuggable, int 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, |
| 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(); |
| |
| // get the project target |
| final IAndroidTarget projectTarget = currentSdk.getTarget(project); |
| |
| // FIXME: check errors on missing sdk, AVD manager, or project target. |
| |
| // device chooser response object. |
| final DeviceChooserResponse response = new DeviceChooserResponse(); |
| |
| /* |
| * Launch logic: |
| * - Manually 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. |
| */ |
| |
| if (config.mTargetMode == AndroidLaunchConfiguration.AUTO_TARGET_MODE) { |
| // if we are in automatic target mode, we need to find the current devices |
| IDevice[] devices = AndroidDebugBridge.getBridge().getDevices(); |
| |
| // 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); |
| if (projectTarget.isCompatibleBaseFor(preferredAvd.getTarget()) == false) { |
| preferredAvd = null; |
| |
| AdtPlugin.printErrorToConsole(project, String.format( |
| "Preferred AVD '%1$s' is not compatible with the project target '%2$s'. Looking for a compatible AVD...", |
| config.mAvdName, projectTarget.getName())); |
| } |
| } |
| |
| if (preferredAvd != null) { |
| // look for a matching device |
| 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. |
| 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); |
| if (info != null && projectTarget.isCompatibleBaseFor(info.getTarget())) { |
| compatibleRunningAvds.put(d, info); |
| } |
| } else { |
| if (projectTarget.isPlatform()) { // means this can run on any device as long |
| // as api level is high enough |
| String apiString = d.getProperty(SdkManager.PROP_VERSION_SDK); |
| try { |
| int apiNumber = Integer.parseInt(apiString); |
| if (apiNumber >= projectTarget.getApiVersionNumber()) { |
| // device is compatible with project |
| compatibleRunningAvds.put(d, null); |
| continue; |
| } |
| } catch (NumberFormatException e) { |
| // do nothing, we'll consider it a non compatible device below. |
| } |
| } |
| 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[] avds = avdManager.getAvds(); |
| AvdInfo defaultAvd = null; |
| for (AvdInfo avd : avds) { |
| if (projectTarget.isCompatibleBaseFor(avd.getTarget())) { |
| if (defaultAvd == null || |
| avd.getTarget().getApiVersionNumber() < |
| defaultAvd.getTarget().getApiVersionNumber()) { |
| defaultAvd = avd; |
| } |
| } |
| } |
| |
| 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 { |
| // FIXME: ask the user if he wants to create a AVD. |
| // we found no compatible AVD. |
| AdtPlugin.printErrorToConsole(project, String.format( |
| "Failed to find a AVD compatible with target '%1$s'. Launch aborted.", |
| projectTarget.getName())); |
| stopLaunch(launchInfo); |
| 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); |
| } |
| |
| // bring up the device chooser. |
| AdtPlugin.getDisplay().asyncExec(new Runnable() { |
| 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.getDisplay().getActiveShell(), |
| response, launchInfo.getPackageName(), projectTarget); |
| if (dialog.open() == Dialog.OK) { |
| AndroidLaunchController.this.continueLaunch(response, project, launch, |
| launchInfo, config); |
| } 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); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * 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. |
| */ |
| void continueLaunch(final DeviceChooserResponse response, final IProject project, |
| final AndroidLaunch launch, final DelayedLaunchInfo launchInfo, |
| final AndroidLaunchConfiguration config) { |
| |
| // Since this is called from the UI thread we spawn a new thread |
| // to finish the launch. |
| new Thread() { |
| @Override |
| public void run() { |
| 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()); |
| } |
| } |
| }.start(); |
| } |
| |
| /** |
| * 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 deviceApiVersionName = device.getProperty(IDevice.PROP_BUILD_VERSION); |
| String value = device.getProperty(IDevice.PROP_BUILD_VERSION_NUMBER); |
| int deviceApiVersionNumber = AndroidManifestParser.INVALID_MIN_SDK; |
| try { |
| deviceApiVersionNumber = Integer.parseInt(value); |
| } catch (NumberFormatException e) { |
| // pass, we'll keep the deviceVersionNumber value at 0. |
| } |
| |
| if (launchInfo.getRequiredApiVersionNumber() == AndroidManifestParser.INVALID_MIN_SDK) { |
| // warn the 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 (deviceApiVersionName == null || |
| deviceApiVersionNumber == AndroidManifestParser.INVALID_MIN_SDK) { |
| 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)", deviceApiVersionNumber, |
| deviceApiVersionName)); |
| } |
| } else { // app requires a specific API level |
| if (deviceApiVersionName == null || |
| deviceApiVersionNumber == AndroidManifestParser.INVALID_MIN_SDK) { |
| AdtPlugin.printToConsole(launchInfo.getProject(), |
| "WARNING: Unknown device API version!"); |
| } else if (deviceApiVersionNumber < launchInfo.getRequiredApiVersionNumber()) { |
| String msg = String.format( |
| "ERROR: Application requires API version %1$d. Device API version is %2$d (Android %3$s).", |
| launchInfo.getRequiredApiVersionNumber(), deviceApiVersionNumber, |
| deviceApiVersionName); |
| AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); |
| |
| // abort the launch |
| return false; |
| } |
| } |
| |
| // 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! |
| if (launchInfo.getDebuggable() == null) { |
| String message1 = String.format( |
| "Device '%1$s' requires that applications explicitely declare themselves as debuggable in their manifest.", |
| device.getSerialNumber()); |
| String message2 = String.format("Application '%1$s' does not have the attribute 'debuggable' set to TRUE in its manifest and cannot be debugged.", |
| launchInfo.getPackageName()); |
| AdtPlugin.printErrorToConsole(launchInfo.getProject(), message1, message2); |
| |
| // 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); |
| } else 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) { |
| // API level check |
| if (checkBuildInfo(launchInfo, device) == false) { |
| AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!"); |
| stopLaunch(launchInfo); |
| return false; |
| } |
| |
| // sync the app |
| if (syncApp(launchInfo, device) == false) { |
| AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!"); |
| stopLaunch(launchInfo); |
| return false; |
| } |
| |
| // launch the app |
| launchApp(launchInfo, device); |
| |
| 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 syncApp(DelayedLaunchInfo launchInfo, IDevice device) { |
| SyncService sync = device.getSyncService(); |
| if (sync != null) { |
| IPath path = launchInfo.getPackageFile().getLocation(); |
| String message = String.format("Uploading %1$s onto device '%2$s'", |
| path.lastSegment(), device.getSerialNumber()); |
| AdtPlugin.printToConsole(launchInfo.getProject(), message); |
| |
| String osLocalPath = path.toOSString(); |
| String apkName = launchInfo.getPackageFile().getName(); |
| String remotePath = "/data/local/tmp/" + apkName; //$NON-NLS-1$ |
| |
| SyncResult result = sync.pushFile(osLocalPath, remotePath, |
| SyncService.getNullProgressMonitor()); |
| |
| if (result.getCode() != SyncService.RESULT_OK) { |
| String msg = String.format("Failed to upload %1$s on '%2$s': %3$s", |
| apkName, device.getSerialNumber(), result.getMessage()); |
| AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); |
| return false; |
| } |
| |
| // Now that the package is uploaded, we can install it properly. |
| // This will check that there isn't another apk declaring the same package, or |
| // that another install used a different key. |
| boolean installResult = installPackage(launchInfo, remotePath, device); |
| |
| // now we delete the app we sync'ed |
| try { |
| device.executeShellCommand("rm " + remotePath, new MultiLineReceiver() { //$NON-NLS-1$ |
| @Override |
| public void processNewLines(String[] lines) { |
| // pass |
| } |
| public boolean isCancelled() { |
| return false; |
| } |
| }); |
| } catch (IOException e) { |
| AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format( |
| "Failed to delete temporary package: %1$s", e.getMessage())); |
| return false; |
| } |
| |
| return installResult; |
| } |
| |
| String msg = String.format( |
| "Failed to upload %1$s on device '%2$s': Unable to open sync connection!", |
| launchInfo.getPackageFile().getName(), device.getSerialNumber()); |
| AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); |
| |
| return false; |
| } |
| |
| /** |
| * Installs the application package that was pushed to a temporary location on the device. |
| * @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 { |
| String result = doInstall(launchInfo, remotePath, device, false /* 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 (IOException e) { |
| // do nothing, we'll return false |
| } |
| |
| 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 IOException |
| */ |
| private boolean checkInstallResult(String result, IDevice device, DelayedLaunchInfo launchInfo, |
| String remotePath, InstallRetryMode retryMode) throws IOException { |
| if (result == null) { |
| AdtPlugin.printToConsole(launchInfo.getProject(), "Success!"); |
| return true; |
| } else if (result.equals("INSTALL_FAILED_ALREADY_EXISTS")) { //$NON-NLS-1$ |
| 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")) { |
| 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 IOException |
| */ |
| @SuppressWarnings("unused") |
| private String doUninstall(IDevice device, DelayedLaunchInfo launchInfo) throws IOException { |
| InstallReceiver receiver = new InstallReceiver(); |
| try { |
| device.executeShellCommand("pm uninstall " + launchInfo.getPackageName(), //$NON-NLS-1$ |
| receiver); |
| } catch (IOException e) { |
| String msg = String.format( |
| "Failed to uninstall %1$s: %2$s", launchInfo.getPackageName(), e.getMessage()); |
| AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); |
| throw e; |
| } |
| |
| return receiver.getSuccess(); |
| } |
| |
| /** |
| * Performs the installation of an application whose package has been uploaded on the device. |
| * <p/>Before doing it, if the application is already running on the device, it is killed. |
| * @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 IOException |
| */ |
| private String doInstall(DelayedLaunchInfo launchInfo, final String remotePath, |
| final IDevice device, boolean reinstall) throws IOException { |
| // kill running application |
| Client application = device.getClient(launchInfo.getPackageName()); |
| if (application != null) { |
| application.kill(); |
| } |
| |
| InstallReceiver receiver = new InstallReceiver(); |
| try { |
| String cmd = String.format( |
| reinstall ? "pm install -r \"%1$s\"" : "pm install \"%1$s\"", //$NON-NLS-1$ //$NON-NLS-2$ |
| remotePath); //$NON-NLS-1$ //$NON-NLS-2$ |
| device.executeShellCommand(cmd, receiver); |
| } catch (IOException e) { |
| String msg = String.format( |
| "Failed to install %1$s on device '%2$s': %3$s", |
| launchInfo.getPackageFile().getName(), device.getSerialNumber(), |
| e.getMessage()); |
| AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); |
| throw e; |
| } |
| |
| return receiver.getSuccess(); |
| } |
| |
| /** |
| * 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 |
| */ |
| public void launchApp(final DelayedLaunchInfo info, IDevice device) { |
| if (info.isDebugMode()) { |
| synchronized (sListLock) { |
| if (mWaitingForDebuggerApplications.contains(info) == false) { |
| mWaitingForDebuggerApplications.add(info); |
| } |
| } |
| } |
| if (info.getLaunchAction().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 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>(); |
| |
| list.add(AdtPlugin.getOsAbsoluteEmulator()); |
| 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.getDisplay().getActiveShell(), |
| "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 |
| */ |
| 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) |
| */ |
| 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(Device) |
| */ |
| public void deviceConnected(Device 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( |
| AdtPlugin.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(Device) |
| */ |
| @SuppressWarnings("unchecked") |
| public void deviceDisconnected(Device 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(Device, int) |
| */ |
| public void deviceChanged(Device 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_INTEREST}, {@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) |
| */ |
| 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(AdtPlugin.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() == ClientData.DEBUGGER_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_INTEREST) == Client.CHANGE_DEBUGGER_INTEREST) { |
| ClientData clientData = client.getClientData(); |
| String applicationName = client.getClientData().getClientDescription(); |
| if (clientData.getDebuggerConnectionStatus() == ClientData.DEBUGGER_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.getPackageName())) { |
| // 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.getPackageName(), 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 ouput 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) |
| */ |
| public void stopLaunch(DelayedLaunchInfo launchInfo) { |
| launchInfo.getLaunch().stopLaunch(); |
| synchronized (sListLock) { |
| mWaitingForReadyEmulatorList.remove(launchInfo); |
| mWaitingForDebuggerApplications.remove(launchInfo); |
| } |
| } |
| } |
| |