/* * 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.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.AndroidDebugBridge.IClientChangeListener; import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; import com.android.ddmlib.SyncService.SyncResult; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.launch.AndroidLaunchConfiguration.TargetMode; 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.ApkInstallManager; 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.ide.eclipse.common.project.BaseProjectHelper; import com.android.prefs.AndroidLocation.AndroidLocationException; 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.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 java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; 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#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(); /** * 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.getValue()); // 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 packageName the Android package name of the app * @param debugPackageName the Android package name to debug * @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, String debugPackageName, 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, 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(); } 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 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 == TargetMode.AUTO) { // 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, true /*validAvdOnly*/); 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, true /*validAvdOnly*/); 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.getValidAvds(); 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 an 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; } /** * 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(), 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) { 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; } // if the installation succeeded, we register it. if (installResult) { ApkInstallManager.getInstance().registerInstallation( launchInfo.getProject(), device); } 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; } /** * 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 AndroidManifestParser manifestParser; try { manifestParser = AndroidManifestParser.parse( androidProject, null /* errorListener */, true /* gatherData */, false /* markErrors */); } catch (CoreException e) { AdtPlugin.printErrorToConsole( launchInfo.getProject(), String.format("Error parsing manifest of %s", androidProject.getElementName())); 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(), manifestParser.getPackage(), manifestParser.getPackage(), launchInfo.getLaunchAction(), apk, manifestParser.getDebuggable(), manifestParser.getApiLevelRequirement(), launchInfo.getLaunch(), launchInfo.getMonitor()); // Add to the list dependencies.add(delayedLaunchInfo); } return dependencies; } /** * 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. * * @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 { 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.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 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); } } }