1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Eclipse Public License, Version 1.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.eclipse.org/org/documents/epl-v10.php 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.ide.eclipse.adt.internal.launch; 18 19 import com.android.ddmlib.AdbCommandRejectedException; 20 import com.android.ddmlib.AndroidDebugBridge; 21 import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; 22 import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener; 23 import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener; 24 import com.android.ddmlib.CanceledException; 25 import com.android.ddmlib.Client; 26 import com.android.ddmlib.ClientData; 27 import com.android.ddmlib.ClientData.DebuggerStatus; 28 import com.android.ddmlib.IDevice; 29 import com.android.ddmlib.InstallException; 30 import com.android.ddmlib.Log; 31 import com.android.ddmlib.TimeoutException; 32 import com.android.ide.eclipse.adt.AdtPlugin; 33 import com.android.ide.eclipse.adt.internal.actions.AvdManagerAction; 34 import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration.TargetMode; 35 import com.android.ide.eclipse.adt.internal.launch.DelayedLaunchInfo.InstallRetryMode; 36 import com.android.ide.eclipse.adt.internal.launch.DeviceChooserDialog.DeviceChooserResponse; 37 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 38 import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper; 39 import com.android.ide.eclipse.adt.internal.project.ApkInstallManager; 40 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 41 import com.android.ide.eclipse.adt.internal.project.ProjectHelper; 42 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 43 import com.android.ide.eclipse.ddms.DdmsPlugin; 44 import com.android.prefs.AndroidLocation.AndroidLocationException; 45 import com.android.sdklib.AndroidVersion; 46 import com.android.sdklib.IAndroidTarget; 47 import com.android.sdklib.NullSdkLog; 48 import com.android.sdklib.internal.avd.AvdInfo; 49 import com.android.sdklib.internal.avd.AvdManager; 50 import com.android.sdklib.xml.ManifestData; 51 52 import org.eclipse.core.resources.IFile; 53 import org.eclipse.core.resources.IProject; 54 import org.eclipse.core.resources.IResource; 55 import org.eclipse.core.runtime.CoreException; 56 import org.eclipse.core.runtime.IPath; 57 import org.eclipse.core.runtime.IProgressMonitor; 58 import org.eclipse.debug.core.DebugPlugin; 59 import org.eclipse.debug.core.ILaunchConfiguration; 60 import org.eclipse.debug.core.ILaunchConfigurationType; 61 import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; 62 import org.eclipse.debug.core.ILaunchManager; 63 import org.eclipse.debug.core.model.IDebugTarget; 64 import org.eclipse.debug.ui.DebugUITools; 65 import org.eclipse.jdt.core.IJavaProject; 66 import org.eclipse.jdt.core.JavaModelException; 67 import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; 68 import org.eclipse.jdt.launching.IVMConnector; 69 import org.eclipse.jdt.launching.JavaRuntime; 70 import org.eclipse.jface.dialogs.Dialog; 71 import org.eclipse.jface.dialogs.MessageDialog; 72 import org.eclipse.jface.preference.IPreferenceStore; 73 import org.eclipse.swt.widgets.Display; 74 import org.eclipse.swt.widgets.Shell; 75 76 import java.io.BufferedReader; 77 import java.io.IOException; 78 import java.io.InputStreamReader; 79 import java.util.ArrayList; 80 import java.util.HashMap; 81 import java.util.List; 82 import java.util.Map; 83 import java.util.Map.Entry; 84 import java.util.concurrent.atomic.AtomicBoolean; 85 86 /** 87 * Controls the launch of Android application either on a device or on the 88 * emulator. If an emulator is already running, this class will attempt to reuse 89 * it. 90 */ 91 public final class AndroidLaunchController implements IDebugBridgeChangeListener, 92 IDeviceChangeListener, IClientChangeListener, ILaunchController { 93 94 private static final String FLAG_AVD = "-avd"; //$NON-NLS-1$ 95 private static final String FLAG_NETDELAY = "-netdelay"; //$NON-NLS-1$ 96 private static final String FLAG_NETSPEED = "-netspeed"; //$NON-NLS-1$ 97 private static final String FLAG_WIPE_DATA = "-wipe-data"; //$NON-NLS-1$ 98 private static final String FLAG_NO_BOOT_ANIM = "-no-boot-anim"; //$NON-NLS-1$ 99 100 /** 101 * Map to store {@link ILaunchConfiguration} objects that must be launched as simple connection 102 * to running application. The integer is the port on which to connect. 103 * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b> 104 */ 105 private static final HashMap<ILaunchConfiguration, Integer> sRunningAppMap = 106 new HashMap<ILaunchConfiguration, Integer>(); 107 108 private static final Object sListLock = sRunningAppMap; 109 110 /** 111 * List of {@link DelayedLaunchInfo} waiting for an emulator to connect. 112 * <p>Once an emulator has connected, {@link DelayedLaunchInfo#getDevice()} is set and the 113 * DelayedLaunchInfo object is moved to 114 * {@link AndroidLaunchController#mWaitingForReadyEmulatorList}. 115 * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b> 116 */ 117 private final ArrayList<DelayedLaunchInfo> mWaitingForEmulatorLaunches = 118 new ArrayList<DelayedLaunchInfo>(); 119 120 /** 121 * List of application waiting to be launched on a device/emulator.<br> 122 * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b> 123 * */ 124 private final ArrayList<DelayedLaunchInfo> mWaitingForReadyEmulatorList = 125 new ArrayList<DelayedLaunchInfo>(); 126 127 /** 128 * Application waiting to show up as waiting for debugger. 129 * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b> 130 */ 131 private final ArrayList<DelayedLaunchInfo> mWaitingForDebuggerApplications = 132 new ArrayList<DelayedLaunchInfo>(); 133 134 /** 135 * List of clients that have appeared as waiting for debugger before their name was available. 136 * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b> 137 */ 138 private final ArrayList<Client> mUnknownClientsWaitingForDebugger = new ArrayList<Client>(); 139 140 /** A map of launch config name to device used for that launch config. */ 141 private static final Map<String, String> sDeviceUsedForLaunch = new HashMap<String, String>(); 142 143 /** static instance for singleton */ 144 private static AndroidLaunchController sThis = new AndroidLaunchController(); 145 146 /** private constructor to enforce singleton */ 147 private AndroidLaunchController() { 148 AndroidDebugBridge.addDebugBridgeChangeListener(this); 149 AndroidDebugBridge.addDeviceChangeListener(this); 150 AndroidDebugBridge.addClientChangeListener(this); 151 } 152 153 /** 154 * Returns the singleton reference. 155 */ 156 public static AndroidLaunchController getInstance() { 157 return sThis; 158 } 159 160 161 /** 162 * Launches a remote java debugging session on an already running application 163 * @param project The project of the application to debug. 164 * @param debugPort The port to connect the debugger to. 165 */ 166 public static void debugRunningApp(IProject project, int debugPort) { 167 // get an existing or new launch configuration 168 ILaunchConfiguration config = AndroidLaunchController.getLaunchConfig(project); 169 170 if (config != null) { 171 setPortLaunchConfigAssociation(config, debugPort); 172 173 // and launch 174 DebugUITools.launch(config, ILaunchManager.DEBUG_MODE); 175 } 176 } 177 178 /** 179 * Returns an {@link ILaunchConfiguration} for the specified {@link IProject}. 180 * @param project the project 181 * @return a new or already existing <code>ILaunchConfiguration</code> or null if there was 182 * an error when creating a new one. 183 */ 184 public static ILaunchConfiguration getLaunchConfig(IProject project) { 185 // get the launch manager 186 ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager(); 187 188 // now get the config type for our particular android type. 189 ILaunchConfigurationType configType = manager.getLaunchConfigurationType( 190 LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID); 191 192 String name = project.getName(); 193 194 // search for an existing launch configuration 195 ILaunchConfiguration config = findConfig(manager, configType, name); 196 197 // test if we found one or not 198 if (config == null) { 199 // Didn't find a matching config, so we make one. 200 // It'll be made in the "working copy" object first. 201 ILaunchConfigurationWorkingCopy wc = null; 202 203 try { 204 // make the working copy object 205 wc = configType.newInstance(null, 206 manager.generateUniqueLaunchConfigurationNameFrom(name)); 207 208 // set the project name 209 wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name); 210 211 // set the launch mode to default. 212 wc.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION, 213 LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION); 214 215 // set default target mode 216 wc.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, 217 LaunchConfigDelegate.DEFAULT_TARGET_MODE.getValue()); 218 219 // default AVD: None 220 wc.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String) null); 221 222 // set the default network speed 223 wc.setAttribute(LaunchConfigDelegate.ATTR_SPEED, 224 LaunchConfigDelegate.DEFAULT_SPEED); 225 226 // and delay 227 wc.setAttribute(LaunchConfigDelegate.ATTR_DELAY, 228 LaunchConfigDelegate.DEFAULT_DELAY); 229 230 // default wipe data mode 231 wc.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, 232 LaunchConfigDelegate.DEFAULT_WIPE_DATA); 233 234 // default disable boot animation option 235 wc.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM, 236 LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM); 237 238 // set default emulator options 239 IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); 240 String emuOptions = store.getString(AdtPrefs.PREFS_EMU_OPTIONS); 241 wc.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE, emuOptions); 242 243 // map the config and the project 244 wc.setMappedResources(getResourcesToMap(project)); 245 246 // save the working copy to get the launch config object which we return. 247 return wc.doSave(); 248 249 } catch (CoreException e) { 250 String msg = String.format( 251 "Failed to create a Launch config for project '%1$s': %2$s", 252 project.getName(), e.getMessage()); 253 AdtPlugin.printErrorToConsole(project, msg); 254 255 // no launch! 256 return null; 257 } 258 } 259 260 return config; 261 } 262 263 /** 264 * Returns the list of resources to map to a Launch Configuration. 265 * @param project the project associated to the launch configuration. 266 */ 267 public static IResource[] getResourcesToMap(IProject project) { 268 ArrayList<IResource> array = new ArrayList<IResource>(2); 269 array.add(project); 270 271 IFile manifest = ProjectHelper.getManifest(project); 272 if (manifest != null) { 273 array.add(manifest); 274 } 275 276 return array.toArray(new IResource[array.size()]); 277 } 278 279 /** 280 * Launches an android app on the device or emulator 281 * 282 * @param project The project we're launching 283 * @param mode the mode in which to launch, one of the mode constants 284 * defined by <code>ILaunchManager</code> - <code>RUN_MODE</code> or 285 * <code>DEBUG_MODE</code>. 286 * @param apk the resource to the apk to launch. 287 * @param packageName the Android package name of the app 288 * @param debugPackageName the Android package name to debug 289 * @param debuggable the debuggable value of the app's manifest, or null if not set. 290 * @param requiredApiVersionNumber the api version required by the app, or null if none. 291 * @param launchAction the action to perform after app sync 292 * @param config the launch configuration 293 * @param launch the launch object 294 */ 295 public void launch(final IProject project, String mode, IFile apk, 296 String packageName, String debugPackageName, Boolean debuggable, 297 String requiredApiVersionNumber, final IAndroidLaunchAction launchAction, 298 final AndroidLaunchConfiguration config, final AndroidLaunch launch, 299 IProgressMonitor monitor) { 300 301 String message = String.format("Performing %1$s", launchAction.getLaunchDescription()); 302 AdtPlugin.printToConsole(project, message); 303 304 // create the launch info 305 final DelayedLaunchInfo launchInfo = new DelayedLaunchInfo(project, packageName, 306 debugPackageName, launchAction, apk, debuggable, requiredApiVersionNumber, launch, 307 monitor); 308 309 // set the debug mode 310 launchInfo.setDebugMode(mode.equals(ILaunchManager.DEBUG_MODE)); 311 312 // get the SDK 313 Sdk currentSdk = Sdk.getCurrent(); 314 AvdManager avdManager = currentSdk.getAvdManager(); 315 316 // reload the AVDs to make sure we are up to date 317 try { 318 avdManager.reloadAvds(NullSdkLog.getLogger()); 319 } catch (AndroidLocationException e1) { 320 // this happens if the AVD Manager failed to find the folder in which the AVDs are 321 // stored. This is unlikely to happen, but if it does, we should force to go manual 322 // to allow using physical devices. 323 config.mTargetMode = TargetMode.MANUAL; 324 } 325 326 // get the project target 327 IAndroidTarget projectTarget = currentSdk.getTarget(project); 328 329 // FIXME: check errors on missing sdk, AVD manager, or project target. 330 331 // device chooser response object. 332 final DeviceChooserResponse response = new DeviceChooserResponse(); 333 334 /* 335 * Launch logic: 336 * - Use Last Launched Device/AVD set. 337 * If user requested to use same device for future launches, and the last launched 338 * device/avd is still present, then simply launch on the same device/avd. 339 * - Manually Mode 340 * Always display a UI that lets a user see the current running emulators/devices. 341 * The UI must show which devices are compatibles, and allow launching new emulators 342 * with compatible (and not yet running) AVD. 343 * - Automatic Way 344 * * Preferred AVD set. 345 * If Preferred AVD is not running: launch it. 346 * Launch the application on the preferred AVD. 347 * * No preferred AVD. 348 * Count the number of compatible emulators/devices. 349 * If != 1, display a UI similar to manual mode. 350 * If == 1, launch the application on this AVD/device. 351 */ 352 IDevice[] devices = AndroidDebugBridge.getBridge().getDevices(); 353 IDevice deviceUsedInLastLaunch = getDeviceUsedForLastLaunch(devices, 354 launch.getLaunchConfiguration().getName()); 355 if (deviceUsedInLastLaunch != null) { 356 response.setDeviceToUse(deviceUsedInLastLaunch); 357 continueLaunch(response, project, launch, launchInfo, config); 358 return; 359 } 360 361 if (config.mTargetMode == TargetMode.AUTO) { 362 // first check if we have a preferred AVD name, and if it actually exists, and is valid 363 // (ie able to run the project). 364 // We need to check this in case the AVD was recreated with a different target that is 365 // not compatible. 366 AvdInfo preferredAvd = null; 367 if (config.mAvdName != null) { 368 preferredAvd = avdManager.getAvd(config.mAvdName, true /*validAvdOnly*/); 369 if (projectTarget.canRunOn(preferredAvd.getTarget()) == false) { 370 preferredAvd = null; 371 372 AdtPlugin.printErrorToConsole(project, String.format( 373 "Preferred AVD '%1$s' is not compatible with the project target '%2$s'. Looking for a compatible AVD...", 374 config.mAvdName, projectTarget.getName())); 375 } 376 } 377 378 if (preferredAvd != null) { 379 // look for a matching device 380 381 for (IDevice d : devices) { 382 String deviceAvd = d.getAvdName(); 383 if (deviceAvd != null && deviceAvd.equals(config.mAvdName)) { 384 response.setDeviceToUse(d); 385 386 AdtPlugin.printToConsole(project, String.format( 387 "Automatic Target Mode: Preferred AVD '%1$s' is available on emulator '%2$s'", 388 config.mAvdName, d)); 389 390 continueLaunch(response, project, launch, launchInfo, config); 391 return; 392 } 393 } 394 395 // at this point we have a valid preferred AVD that is not running. 396 // We need to start it. 397 response.setAvdToLaunch(preferredAvd); 398 399 AdtPlugin.printToConsole(project, String.format( 400 "Automatic Target Mode: Preferred AVD '%1$s' is not available. Launching new emulator.", 401 config.mAvdName)); 402 403 continueLaunch(response, project, launch, launchInfo, config); 404 return; 405 } 406 407 // no (valid) preferred AVD? look for one. 408 409 // If the API level requested in the manifest is lower than the current project 410 // target, when we will iterate devices/avds later ideally we will want to find 411 // a device/avd which target is as close to the manifest as possible (instead of 412 // a device which target is the same as the project's target) and use it as the 413 // new default. 414 415 int reqApiLevel = 0; 416 try { 417 reqApiLevel = Integer.parseInt(requiredApiVersionNumber); 418 419 if (reqApiLevel > 0 && reqApiLevel < projectTarget.getVersion().getApiLevel()) { 420 int maxDist = projectTarget.getVersion().getApiLevel() - reqApiLevel; 421 IAndroidTarget candidate = null; 422 423 for (IAndroidTarget target : currentSdk.getTargets()) { 424 if (target.canRunOn(projectTarget)) { 425 int currDist = target.getVersion().getApiLevel() - reqApiLevel; 426 if (currDist >= 0 && currDist < maxDist) { 427 maxDist = currDist; 428 candidate = target; 429 if (maxDist == 0) { 430 // Found a perfect match 431 break; 432 } 433 } 434 } 435 } 436 437 if (candidate != null) { 438 // We found a better SDK target candidate, that is closer to the 439 // API level from minSdkVersion than the one currently used by the 440 // project. Below (in the for...devices loop) we'll try to find 441 // a device/AVD for it. 442 projectTarget = candidate; 443 } 444 } 445 } catch (NumberFormatException e) { 446 // pass 447 } 448 449 HashMap<IDevice, AvdInfo> compatibleRunningAvds = new HashMap<IDevice, AvdInfo>(); 450 boolean hasDevice = false; // if there's 1+ device running, we may force manual mode, 451 // as we cannot always detect proper compatibility with 452 // devices. This is the case if the project target is not 453 // a standard platform 454 for (IDevice d : devices) { 455 String deviceAvd = d.getAvdName(); 456 if (deviceAvd != null) { // physical devices return null. 457 AvdInfo info = avdManager.getAvd(deviceAvd, true /*validAvdOnly*/); 458 if (info != null && projectTarget.canRunOn(info.getTarget())) { 459 compatibleRunningAvds.put(d, info); 460 } 461 } else { 462 if (projectTarget.isPlatform()) { // means this can run on any device as long 463 // as api level is high enough 464 AndroidVersion deviceVersion = Sdk.getDeviceVersion(d); 465 // the deviceVersion may be null if it wasn't yet queried (device just 466 // plugged in or emulator just booting up. 467 if (deviceVersion != null && 468 deviceVersion.canRun(projectTarget.getVersion())) { 469 // device is compatible with project 470 compatibleRunningAvds.put(d, null); 471 continue; 472 } 473 } else { 474 // for non project platform, we can't be sure if a device can 475 // run an application or not, since we don't query the device 476 // for the list of optional libraries that it supports. 477 } 478 hasDevice = true; 479 } 480 } 481 482 // depending on the number of devices, we'll simulate an automatic choice 483 // from the device chooser or simply show up the device chooser. 484 if (hasDevice == false && compatibleRunningAvds.size() == 0) { 485 // if zero emulators/devices, we launch an emulator. 486 // We need to figure out which AVD first. 487 488 // we are going to take the closest AVD. ie a compatible AVD that has the API level 489 // closest to the project target. 490 AvdInfo defaultAvd = findMatchingAvd(avdManager, projectTarget); 491 492 if (defaultAvd != null) { 493 response.setAvdToLaunch(defaultAvd); 494 495 AdtPlugin.printToConsole(project, String.format( 496 "Automatic Target Mode: launching new emulator with compatible AVD '%1$s'", 497 defaultAvd.getName())); 498 499 continueLaunch(response, project, launch, launchInfo, config); 500 return; 501 } else { 502 AdtPlugin.printToConsole(project, String.format( 503 "Failed to find an AVD compatible with target '%1$s'.", 504 projectTarget.getName())); 505 506 final Display display = AdtPlugin.getDisplay(); 507 final boolean[] searchAgain = new boolean[] { false }; 508 // ask the user to create a new one. 509 display.syncExec(new Runnable() { 510 @Override 511 public void run() { 512 Shell shell = display.getActiveShell(); 513 if (MessageDialog.openQuestion(shell, "Android AVD Error", 514 "No compatible targets were found. Do you wish to a add new Android Virtual Device?")) { 515 AvdManagerAction action = new AvdManagerAction(); 516 action.run(null /*action*/); 517 searchAgain[0] = true; 518 } 519 } 520 }); 521 if (searchAgain[0]) { 522 // attempt to reload the AVDs and find one compatible. 523 defaultAvd = findMatchingAvd(avdManager, projectTarget); 524 525 if (defaultAvd == null) { 526 AdtPlugin.printErrorToConsole(project, String.format( 527 "Still no compatible AVDs with target '%1$s': Aborting launch.", 528 projectTarget.getName())); 529 stopLaunch(launchInfo); 530 } else { 531 response.setAvdToLaunch(defaultAvd); 532 533 AdtPlugin.printToConsole(project, String.format( 534 "Launching new emulator with compatible AVD '%1$s'", 535 defaultAvd.getName())); 536 537 continueLaunch(response, project, launch, launchInfo, config); 538 return; 539 } 540 } 541 } 542 } else if (hasDevice == false && compatibleRunningAvds.size() == 1) { 543 Entry<IDevice, AvdInfo> e = compatibleRunningAvds.entrySet().iterator().next(); 544 response.setDeviceToUse(e.getKey()); 545 546 // get the AvdInfo, if null, the device is a physical device. 547 AvdInfo avdInfo = e.getValue(); 548 if (avdInfo != null) { 549 message = String.format("Automatic Target Mode: using existing emulator '%1$s' running compatible AVD '%2$s'", 550 response.getDeviceToUse(), e.getValue().getName()); 551 } else { 552 message = String.format("Automatic Target Mode: using device '%1$s'", 553 response.getDeviceToUse()); 554 } 555 AdtPlugin.printToConsole(project, message); 556 557 continueLaunch(response, project, launch, launchInfo, config); 558 return; 559 } 560 561 // if more than one device, we'll bring up the DeviceChooser dialog below. 562 if (compatibleRunningAvds.size() >= 2) { 563 message = "Automatic Target Mode: Several compatible targets. Please select a target device."; 564 } else if (hasDevice) { 565 message = "Automatic Target Mode: Unable to detect device compatibility. Please select a target device."; 566 } 567 568 AdtPlugin.printToConsole(project, message); 569 } 570 571 // bring up the device chooser. 572 final IAndroidTarget desiredProjectTarget = projectTarget; 573 final AtomicBoolean continueLaunch = new AtomicBoolean(false); 574 AdtPlugin.getDisplay().syncExec(new Runnable() { 575 @Override 576 public void run() { 577 try { 578 // open the chooser dialog. It'll fill 'response' with the device to use 579 // or the AVD to launch. 580 DeviceChooserDialog dialog = new DeviceChooserDialog( 581 AdtPlugin.getDisplay().getActiveShell(), 582 response, launchInfo.getPackageName(), desiredProjectTarget); 583 if (dialog.open() == Dialog.OK) { 584 updateLaunchOnSameDeviceState(response, 585 launch.getLaunchConfiguration().getName()); 586 continueLaunch.set(true); 587 } else { 588 AdtPlugin.printErrorToConsole(project, "Launch canceled!"); 589 stopLaunch(launchInfo); 590 return; 591 } 592 } catch (Exception e) { 593 // there seems to be some case where the shell will be null. (might be 594 // an OS X bug). Because of this the creation of the dialog will throw 595 // and IllegalArg exception interrupting the launch with no user feedback. 596 // So we trap all the exception and display something. 597 String msg = e.getMessage(); 598 if (msg == null) { 599 msg = e.getClass().getCanonicalName(); 600 } 601 AdtPlugin.printErrorToConsole(project, 602 String.format("Error during launch: %s", msg)); 603 stopLaunch(launchInfo); 604 } 605 } 606 }); 607 608 if (continueLaunch.get()) { 609 continueLaunch(response, project, launch, launchInfo, config); 610 } 611 } 612 613 private IDevice getDeviceUsedForLastLaunch(IDevice[] devices, 614 String launchConfigName) { 615 String deviceName = sDeviceUsedForLaunch.get(launchConfigName); 616 if (deviceName == null) { 617 return null; 618 } 619 620 for (IDevice device : devices) { 621 if (deviceName.equals(device.getAvdName()) || 622 deviceName.equals(device.getSerialNumber())) { 623 return device; 624 } 625 } 626 627 return null; 628 } 629 630 private void updateLaunchOnSameDeviceState(DeviceChooserResponse response, 631 String launchConfigName) { 632 if (!response.useDeviceForFutureLaunches()) { 633 return; 634 } 635 636 AvdInfo avd = response.getAvdToLaunch(); 637 String device = null; 638 if (avd != null) { 639 device = avd.getName(); 640 } else { 641 device = response.getDeviceToUse().getSerialNumber(); 642 } 643 644 sDeviceUsedForLaunch.put(launchConfigName, device); 645 } 646 647 /** 648 * Find a matching AVD. 649 */ 650 private AvdInfo findMatchingAvd(AvdManager avdManager, final IAndroidTarget projectTarget) { 651 AvdInfo[] avds = avdManager.getValidAvds(); 652 AvdInfo defaultAvd = null; 653 for (AvdInfo avd : avds) { 654 if (projectTarget.canRunOn(avd.getTarget())) { 655 // at this point we can ignore the code name issue since 656 // IAndroidTarget.canRunOn() will already have filtered the non 657 // compatible AVDs. 658 if (defaultAvd == null || 659 avd.getTarget().getVersion().getApiLevel() < 660 defaultAvd.getTarget().getVersion().getApiLevel()) { 661 defaultAvd = avd; 662 } 663 } 664 } 665 return defaultAvd; 666 } 667 668 /** 669 * Continues the launch based on the DeviceChooser response. 670 * @param response the device chooser response 671 * @param project The project being launched 672 * @param launch The eclipse launch info 673 * @param launchInfo The {@link DelayedLaunchInfo} 674 * @param config The config needed to start a new emulator. 675 */ 676 private void continueLaunch(final DeviceChooserResponse response, final IProject project, 677 final AndroidLaunch launch, final DelayedLaunchInfo launchInfo, 678 final AndroidLaunchConfiguration config) { 679 if (response.getAvdToLaunch() != null) { 680 // there was no selected device, we start a new emulator. 681 synchronized (sListLock) { 682 AvdInfo info = response.getAvdToLaunch(); 683 mWaitingForEmulatorLaunches.add(launchInfo); 684 AdtPlugin.printToConsole(project, String.format( 685 "Launching a new emulator with Virtual Device '%1$s'", 686 info.getName())); 687 boolean status = launchEmulator(config, info); 688 689 if (status == false) { 690 // launching the emulator failed! 691 AdtPlugin.displayError("Emulator Launch", 692 "Couldn't launch the emulator! Make sure the SDK directory is properly setup and the emulator is not missing."); 693 694 // stop the launch and return 695 mWaitingForEmulatorLaunches.remove(launchInfo); 696 AdtPlugin.printErrorToConsole(project, "Launch canceled!"); 697 stopLaunch(launchInfo); 698 return; 699 } 700 701 return; 702 } 703 } else if (response.getDeviceToUse() != null) { 704 launchInfo.setDevice(response.getDeviceToUse()); 705 simpleLaunch(launchInfo, launchInfo.getDevice()); 706 } 707 } 708 709 /** 710 * Queries for a debugger port for a specific {@link ILaunchConfiguration}. 711 * <p/> 712 * If the configuration and a debugger port where added through 713 * {@link #setPortLaunchConfigAssociation(ILaunchConfiguration, int)}, then this method 714 * will return the debugger port, and remove the configuration from the list. 715 * @param launchConfig the {@link ILaunchConfiguration} 716 * @return the debugger port or {@link LaunchConfigDelegate#INVALID_DEBUG_PORT} if the 717 * configuration was not setup. 718 */ 719 static int getPortForConfig(ILaunchConfiguration launchConfig) { 720 synchronized (sListLock) { 721 Integer port = sRunningAppMap.get(launchConfig); 722 if (port != null) { 723 sRunningAppMap.remove(launchConfig); 724 return port; 725 } 726 } 727 728 return LaunchConfigDelegate.INVALID_DEBUG_PORT; 729 } 730 731 /** 732 * Set a {@link ILaunchConfiguration} and its associated debug port, in the list of 733 * launch config to connect directly to a running app instead of doing full launch (sync, 734 * launch, and connect to). 735 * @param launchConfig the {@link ILaunchConfiguration} object. 736 * @param port The debugger port to connect to. 737 */ 738 private static void setPortLaunchConfigAssociation(ILaunchConfiguration launchConfig, 739 int port) { 740 synchronized (sListLock) { 741 sRunningAppMap.put(launchConfig, port); 742 } 743 } 744 745 /** 746 * Checks the build information, and returns whether the launch should continue. 747 * <p/>The value tested are: 748 * <ul> 749 * <li>Minimum API version requested by the application. If the target device does not match, 750 * the launch is canceled.</li> 751 * <li>Debuggable attribute of the application and whether or not the device requires it. If 752 * the device requires it and it is not set in the manifest, the launch will be forced to 753 * "release" mode instead of "debug"</li> 754 * <ul> 755 */ 756 private boolean checkBuildInfo(DelayedLaunchInfo launchInfo, IDevice device) { 757 if (device != null) { 758 // check the app required API level versus the target device API level 759 760 String deviceVersion = device.getProperty(IDevice.PROP_BUILD_VERSION); 761 String deviceApiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL); 762 String deviceCodeName = device.getProperty(IDevice.PROP_BUILD_CODENAME); 763 764 int deviceApiLevel = -1; 765 try { 766 deviceApiLevel = Integer.parseInt(deviceApiLevelString); 767 } catch (NumberFormatException e) { 768 // pass, we'll keep the apiLevel value at -1. 769 } 770 771 String requiredApiString = launchInfo.getRequiredApiVersionNumber(); 772 if (requiredApiString != null) { 773 int requiredApi = -1; 774 try { 775 requiredApi = Integer.parseInt(requiredApiString); 776 } catch (NumberFormatException e) { 777 // pass, we'll keep requiredApi value at -1. 778 } 779 780 if (requiredApi == -1) { 781 // this means the manifest uses a codename for minSdkVersion 782 // check that the device is using the same codename 783 if (requiredApiString.equals(deviceCodeName) == false) { 784 AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format( 785 "ERROR: Application requires a device running '%1$s'!", 786 requiredApiString)); 787 return false; 788 } 789 } else { 790 // app requires a specific API level 791 if (deviceApiLevel == -1) { 792 AdtPlugin.printToConsole(launchInfo.getProject(), 793 "WARNING: Unknown device API version!"); 794 } else if (deviceApiLevel < requiredApi) { 795 String msg = String.format( 796 "ERROR: Application requires API version %1$d. Device API version is %2$d (Android %3$s).", 797 requiredApi, deviceApiLevel, deviceVersion); 798 AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); 799 800 // abort the launch 801 return false; 802 } 803 } 804 } else { 805 // warn the application API level requirement is not set. 806 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 807 "WARNING: Application does not specify an API level requirement!"); 808 809 // and display the target device API level (if known) 810 if (deviceApiLevel == -1) { 811 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 812 "WARNING: Unknown device API version!"); 813 } else { 814 AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format( 815 "Device API version is %1$d (Android %2$s)", deviceApiLevel, 816 deviceVersion)); 817 } 818 } 819 820 // now checks that the device/app can be debugged (if needed) 821 if (device.isEmulator() == false && launchInfo.isDebugMode()) { 822 String debuggableDevice = device.getProperty(IDevice.PROP_DEBUGGABLE); 823 if (debuggableDevice != null && debuggableDevice.equals("0")) { //$NON-NLS-1$ 824 // the device is "secure" and requires apps to declare themselves as debuggable! 825 // launchInfo.getDebuggable() will return null if the manifest doesn't declare 826 // anything. In this case this is fine since the build system does insert 827 // debuggable=true. The only case to look for is if false is manually set 828 // in the manifest. 829 if (launchInfo.getDebuggable() == Boolean.FALSE) { 830 String message = String.format("Application '%1$s' has its 'debuggable' attribute set to FALSE and cannot be debugged.", 831 launchInfo.getPackageName()); 832 AdtPlugin.printErrorToConsole(launchInfo.getProject(), message); 833 834 // because am -D does not check for ro.debuggable and the 835 // 'debuggable' attribute, it is important we do not use the -D option 836 // in this case or the app will wait for a debugger forever and never 837 // really launch. 838 launchInfo.setDebugMode(false); 839 } 840 } 841 } 842 } 843 844 return true; 845 } 846 847 /** 848 * Do a simple launch on the specified device, attempting to sync the new 849 * package, and then launching the application. Failed sync/launch will 850 * stop the current AndroidLaunch and return false; 851 * @param launchInfo 852 * @param device 853 * @return true if succeed 854 */ 855 private boolean simpleLaunch(DelayedLaunchInfo launchInfo, IDevice device) { 856 // API level check 857 if (checkBuildInfo(launchInfo, device) == false) { 858 AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!"); 859 stopLaunch(launchInfo); 860 return false; 861 } 862 863 // sync the app 864 if (syncApp(launchInfo, device) == false) { 865 AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!"); 866 stopLaunch(launchInfo); 867 return false; 868 } 869 870 // launch the app 871 launchApp(launchInfo, device); 872 873 return true; 874 } 875 876 877 /** 878 * If needed, syncs the application and all its dependencies on the device/emulator. 879 * 880 * @param launchInfo The Launch information object. 881 * @param device the device on which to sync the application 882 * @return true if the install succeeded. 883 */ 884 private boolean syncApp(DelayedLaunchInfo launchInfo, IDevice device) { 885 boolean alreadyInstalled = ApkInstallManager.getInstance().isApplicationInstalled( 886 launchInfo.getProject(), launchInfo.getPackageName(), device); 887 888 if (alreadyInstalled) { 889 AdtPlugin.printToConsole(launchInfo.getProject(), 890 "Application already deployed. No need to reinstall."); 891 } else { 892 if (doSyncApp(launchInfo, device) == false) { 893 return false; 894 } 895 } 896 897 // The app is now installed, now try the dependent projects 898 for (DelayedLaunchInfo dependentLaunchInfo : getDependenciesLaunchInfo(launchInfo)) { 899 String msg = String.format("Project dependency found, installing: %s", 900 dependentLaunchInfo.getProject().getName()); 901 AdtPlugin.printToConsole(launchInfo.getProject(), msg); 902 if (syncApp(dependentLaunchInfo, device) == false) { 903 return false; 904 } 905 } 906 907 return true; 908 } 909 910 /** 911 * Syncs the application on the device/emulator. 912 * 913 * @param launchInfo The Launch information object. 914 * @param device the device on which to sync the application 915 * @return true if the install succeeded. 916 */ 917 private boolean doSyncApp(DelayedLaunchInfo launchInfo, IDevice device) { 918 IPath path = launchInfo.getPackageFile().getLocation(); 919 String fileName = path.lastSegment(); 920 try { 921 String message = String.format("Uploading %1$s onto device '%2$s'", 922 fileName, device.getSerialNumber()); 923 AdtPlugin.printToConsole(launchInfo.getProject(), message); 924 925 String remotePackagePath = device.syncPackageToDevice(path.toOSString()); 926 boolean installResult = installPackage(launchInfo, remotePackagePath, device); 927 device.removeRemotePackage(remotePackagePath); 928 929 // if the installation succeeded, we register it. 930 if (installResult) { 931 ApkInstallManager.getInstance().registerInstallation( 932 launchInfo.getProject(), launchInfo.getPackageName(), device); 933 } 934 return installResult; 935 } 936 catch (IOException e) { 937 String msg = String.format("Failed to install %1$s on device '%2$s': %3$s", fileName, 938 device.getSerialNumber(), e.getMessage()); 939 AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e); 940 } catch (TimeoutException e) { 941 String msg = String.format("Failed to install %1$s on device '%2$s': timeout", fileName, 942 device.getSerialNumber()); 943 AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); 944 } catch (AdbCommandRejectedException e) { 945 String msg = String.format( 946 "Failed to install %1$s on device '%2$s': adb rejected install command with: %3$s", 947 fileName, device.getSerialNumber(), e.getMessage()); 948 AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e); 949 } catch (CanceledException e) { 950 if (e.wasCanceled()) { 951 AdtPlugin.printToConsole(launchInfo.getProject(), 952 String.format("Install of %1$s canceled", fileName)); 953 } else { 954 String msg = String.format("Failed to install %1$s on device '%2$s': %3$s", 955 fileName, device.getSerialNumber(), e.getMessage()); 956 AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e); 957 } 958 } 959 960 return false; 961 } 962 963 /** 964 * For the current launchInfo, create additional DelayedLaunchInfo that should be used to 965 * sync APKs that we are dependent on to the device. 966 * 967 * @param launchInfo the original launch info that we want to find the 968 * @return a list of DelayedLaunchInfo (may be empty if no dependencies were found or error) 969 */ 970 public List<DelayedLaunchInfo> getDependenciesLaunchInfo(DelayedLaunchInfo launchInfo) { 971 List<DelayedLaunchInfo> dependencies = new ArrayList<DelayedLaunchInfo>(); 972 973 // Convert to equivalent JavaProject 974 IJavaProject javaProject; 975 try { 976 //assuming this is an Android (and Java) project since it is attached to the launchInfo. 977 javaProject = BaseProjectHelper.getJavaProject(launchInfo.getProject()); 978 } catch (CoreException e) { 979 // return empty dependencies 980 AdtPlugin.printErrorToConsole(launchInfo.getProject(), e); 981 return dependencies; 982 } 983 984 // Get all projects that this depends on 985 List<IJavaProject> androidProjectList; 986 try { 987 androidProjectList = ProjectHelper.getAndroidProjectDependencies(javaProject); 988 } catch (JavaModelException e) { 989 // return empty dependencies 990 AdtPlugin.printErrorToConsole(launchInfo.getProject(), e); 991 return dependencies; 992 } 993 994 // for each project, parse manifest and create launch information 995 for (IJavaProject androidProject : androidProjectList) { 996 // Parse the Manifest to get various required information 997 // copied from LaunchConfigDelegate 998 ManifestData manifestData = AndroidManifestHelper.parseForData( 999 androidProject.getProject()); 1000 1001 if (manifestData == null) { 1002 continue; 1003 } 1004 1005 // Get the APK location (can return null) 1006 IFile apk = ProjectHelper.getApplicationPackage(androidProject.getProject()); 1007 if (apk == null) { 1008 // getApplicationPackage will have logged an error message 1009 continue; 1010 } 1011 1012 // Create new launchInfo as an hybrid between parent and dependency information 1013 DelayedLaunchInfo delayedLaunchInfo = new DelayedLaunchInfo( 1014 androidProject.getProject(), 1015 manifestData.getPackage(), 1016 manifestData.getPackage(), 1017 launchInfo.getLaunchAction(), 1018 apk, 1019 manifestData.getDebuggable(), 1020 manifestData.getMinSdkVersionString(), 1021 launchInfo.getLaunch(), 1022 launchInfo.getMonitor()); 1023 1024 // Add to the list 1025 dependencies.add(delayedLaunchInfo); 1026 } 1027 1028 return dependencies; 1029 } 1030 1031 /** 1032 * Installs the application package on the device, and handles return result 1033 * @param launchInfo The launch information 1034 * @param remotePath The remote path of the package. 1035 * @param device The device on which the launch is done. 1036 */ 1037 private boolean installPackage(DelayedLaunchInfo launchInfo, final String remotePath, 1038 final IDevice device) { 1039 String message = String.format("Installing %1$s...", launchInfo.getPackageFile().getName()); 1040 AdtPlugin.printToConsole(launchInfo.getProject(), message); 1041 try { 1042 // try a reinstall first, because the most common case is the app is already installed 1043 String result = doInstall(launchInfo, remotePath, device, true /* reinstall */); 1044 1045 /* For now we force to retry the install (after uninstalling) because there's no 1046 * other way around it: adb install does not want to update a package w/o uninstalling 1047 * the old one first! 1048 */ 1049 return checkInstallResult(result, device, launchInfo, remotePath, 1050 InstallRetryMode.ALWAYS); 1051 } catch (Exception e) { 1052 String msg = String.format( 1053 "Failed to install %1$s on device '%2$s!", 1054 launchInfo.getPackageFile().getName(), device.getSerialNumber()); 1055 AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e.getMessage()); 1056 } 1057 1058 return false; 1059 } 1060 1061 /** 1062 * Checks the result of an installation, and takes optional actions based on it. 1063 * @param result the result string from the installation 1064 * @param device the device on which the installation occured. 1065 * @param launchInfo the {@link DelayedLaunchInfo} 1066 * @param remotePath the temporary path of the package on the device 1067 * @param retryMode indicates what to do in case, a package already exists. 1068 * @return <code>true<code> if success, <code>false</code> otherwise. 1069 * @throws InstallException 1070 */ 1071 private boolean checkInstallResult(String result, IDevice device, DelayedLaunchInfo launchInfo, 1072 String remotePath, InstallRetryMode retryMode) throws InstallException { 1073 if (result == null) { 1074 AdtPlugin.printToConsole(launchInfo.getProject(), "Success!"); 1075 return true; 1076 } 1077 else if (result.equals("INSTALL_FAILED_ALREADY_EXISTS")) { //$NON-NLS-1$ 1078 // this should never happen, since reinstall mode is used on the first attempt 1079 if (retryMode == InstallRetryMode.PROMPT) { 1080 boolean prompt = AdtPlugin.displayPrompt("Application Install", 1081 "A previous installation needs to be uninstalled before the new package can be installed.\nDo you want to uninstall?"); 1082 if (prompt) { 1083 retryMode = InstallRetryMode.ALWAYS; 1084 } else { 1085 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1086 "Installation error! The package already exists."); 1087 return false; 1088 } 1089 } 1090 1091 if (retryMode == InstallRetryMode.ALWAYS) { 1092 /* 1093 * TODO: create a UI that gives the dev the choice to: 1094 * - clean uninstall on launch 1095 * - full uninstall if application exists. 1096 * - soft uninstall if application exists (keeps the app data around). 1097 * - always ask (choice of soft-reinstall, full reinstall) 1098 AdtPlugin.printErrorToConsole(launchInfo.mProject, 1099 "Application already exists, uninstalling..."); 1100 String res = doUninstall(device, launchInfo); 1101 if (res == null) { 1102 AdtPlugin.printToConsole(launchInfo.mProject, "Success!"); 1103 } else { 1104 AdtPlugin.printErrorToConsole(launchInfo.mProject, 1105 String.format("Failed to uninstall: %1$s", res)); 1106 return false; 1107 } 1108 */ 1109 1110 AdtPlugin.printToConsole(launchInfo.getProject(), 1111 "Application already exists. Attempting to re-install instead..."); 1112 String res = doInstall(launchInfo, remotePath, device, true /* reinstall */ ); 1113 return checkInstallResult(res, device, launchInfo, remotePath, 1114 InstallRetryMode.NEVER); 1115 } 1116 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1117 "Installation error! The package already exists."); 1118 } else if (result.equals("INSTALL_FAILED_INVALID_APK")) { //$NON-NLS-1$ 1119 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1120 "Installation failed due to invalid APK file!", 1121 "Please check logcat output for more details."); 1122 } else if (result.equals("INSTALL_FAILED_INVALID_URI")) { //$NON-NLS-1$ 1123 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1124 "Installation failed due to invalid URI!", 1125 "Please check logcat output for more details."); 1126 } else if (result.equals("INSTALL_FAILED_COULDNT_COPY")) { //$NON-NLS-1$ 1127 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1128 String.format("Installation failed: Could not copy %1$s to its final location!", 1129 launchInfo.getPackageFile().getName()), 1130 "Please check logcat output for more details."); 1131 } else if (result.equals("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES")) { 1132 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1133 "Re-installation failed due to different application signatures.", 1134 "You must perform a full uninstall of the application. WARNING: This will remove the application data!", 1135 String.format("Please execute 'adb uninstall %1$s' in a shell.", launchInfo.getPackageName())); 1136 } else { 1137 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1138 String.format("Installation error: %1$s", result), 1139 "Please check logcat output for more details."); 1140 } 1141 1142 return false; 1143 } 1144 1145 /** 1146 * Performs the uninstallation of an application. 1147 * @param device the device on which to install the application. 1148 * @param launchInfo the {@link DelayedLaunchInfo}. 1149 * @return a {@link String} with an error code, or <code>null</code> if success. 1150 * @throws InstallException if the installation failed. 1151 */ 1152 @SuppressWarnings("unused") 1153 private String doUninstall(IDevice device, DelayedLaunchInfo launchInfo) 1154 throws InstallException { 1155 try { 1156 return device.uninstallPackage(launchInfo.getPackageName()); 1157 } catch (InstallException e) { 1158 String msg = String.format( 1159 "Failed to uninstall %1$s: %2$s", launchInfo.getPackageName(), e.getMessage()); 1160 AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); 1161 throw e; 1162 } 1163 } 1164 1165 /** 1166 * Performs the installation of an application whose package has been uploaded on the device. 1167 * 1168 * @param launchInfo the {@link DelayedLaunchInfo}. 1169 * @param remotePath the path of the application package in the device tmp folder. 1170 * @param device the device on which to install the application. 1171 * @param reinstall 1172 * @return a {@link String} with an error code, or <code>null</code> if success. 1173 * @throws InstallException if the uninstallation failed. 1174 */ 1175 private String doInstall(DelayedLaunchInfo launchInfo, final String remotePath, 1176 final IDevice device, boolean reinstall) throws InstallException { 1177 return device.installRemotePackage(remotePath, reinstall); 1178 } 1179 1180 /** 1181 * launches an application on a device or emulator 1182 * 1183 * @param info the {@link DelayedLaunchInfo} that indicates the launch action 1184 * @param device the device or emulator to launch the application on 1185 */ 1186 @Override 1187 public void launchApp(final DelayedLaunchInfo info, IDevice device) { 1188 if (info.isDebugMode()) { 1189 synchronized (sListLock) { 1190 if (mWaitingForDebuggerApplications.contains(info) == false) { 1191 mWaitingForDebuggerApplications.add(info); 1192 } 1193 } 1194 } 1195 if (info.getLaunchAction().doLaunchAction(info, device)) { 1196 // if the app is not a debug app, we need to do some clean up, as 1197 // the process is done! 1198 if (info.isDebugMode() == false) { 1199 // stop the launch object, since there's no debug, and it can't 1200 // provide any control over the app 1201 stopLaunch(info); 1202 } 1203 } else { 1204 // something went wrong or no further launch action needed 1205 // lets stop the Launch 1206 stopLaunch(info); 1207 } 1208 1209 // Monitor the logcat output on the launched device to notify 1210 // the user if any significant error occurs that is visible from logcat 1211 DdmsPlugin.getDefault().startLogCatMonitor(device); 1212 } 1213 1214 private boolean launchEmulator(AndroidLaunchConfiguration config, AvdInfo avdToLaunch) { 1215 1216 // split the custom command line in segments 1217 ArrayList<String> customArgs = new ArrayList<String>(); 1218 boolean hasWipeData = false; 1219 if (config.mEmulatorCommandLine != null && config.mEmulatorCommandLine.length() > 0) { 1220 String[] segments = config.mEmulatorCommandLine.split("\\s+"); //$NON-NLS-1$ 1221 1222 // we need to remove the empty strings 1223 for (String s : segments) { 1224 if (s.length() > 0) { 1225 customArgs.add(s); 1226 if (!hasWipeData && s.equals(FLAG_WIPE_DATA)) { 1227 hasWipeData = true; 1228 } 1229 } 1230 } 1231 } 1232 1233 boolean needsWipeData = config.mWipeData && !hasWipeData; 1234 if (needsWipeData) { 1235 if (!AdtPlugin.displayPrompt("Android Launch", "Are you sure you want to wipe all user data when starting this emulator?")) { 1236 needsWipeData = false; 1237 } 1238 } 1239 1240 // build the command line based on the available parameters. 1241 ArrayList<String> list = new ArrayList<String>(); 1242 1243 String path = AdtPlugin.getOsAbsoluteEmulator(); 1244 1245 list.add(path); 1246 1247 list.add(FLAG_AVD); 1248 list.add(avdToLaunch.getName()); 1249 1250 if (config.mNetworkSpeed != null) { 1251 list.add(FLAG_NETSPEED); 1252 list.add(config.mNetworkSpeed); 1253 } 1254 1255 if (config.mNetworkDelay != null) { 1256 list.add(FLAG_NETDELAY); 1257 list.add(config.mNetworkDelay); 1258 } 1259 1260 if (needsWipeData) { 1261 list.add(FLAG_WIPE_DATA); 1262 } 1263 1264 if (config.mNoBootAnim) { 1265 list.add(FLAG_NO_BOOT_ANIM); 1266 } 1267 1268 list.addAll(customArgs); 1269 1270 // convert the list into an array for the call to exec. 1271 String[] command = list.toArray(new String[list.size()]); 1272 1273 // launch the emulator 1274 try { 1275 Process process = Runtime.getRuntime().exec(command); 1276 grabEmulatorOutput(process); 1277 } catch (IOException e) { 1278 return false; 1279 } 1280 1281 return true; 1282 } 1283 1284 /** 1285 * Looks for and returns an existing {@link ILaunchConfiguration} object for a 1286 * specified project. 1287 * @param manager The {@link ILaunchManager}. 1288 * @param type The {@link ILaunchConfigurationType}. 1289 * @param projectName The name of the project 1290 * @return an existing <code>ILaunchConfiguration</code> object matching the project, or 1291 * <code>null</code>. 1292 */ 1293 private static ILaunchConfiguration findConfig(ILaunchManager manager, 1294 ILaunchConfigurationType type, String projectName) { 1295 try { 1296 ILaunchConfiguration[] configs = manager.getLaunchConfigurations(type); 1297 1298 for (ILaunchConfiguration config : configs) { 1299 if (config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, 1300 "").equals(projectName)) { //$NON-NLS-1$ 1301 return config; 1302 } 1303 } 1304 } catch (CoreException e) { 1305 MessageDialog.openError(AdtPlugin.getDisplay().getActiveShell(), 1306 "Launch Error", e.getStatus().getMessage()); 1307 } 1308 1309 // didn't find anything that matches. Return null 1310 return null; 1311 1312 } 1313 1314 1315 /** 1316 * Connects a remote debugger on the specified port. 1317 * @param debugPort The port to connect the debugger to 1318 * @param launch The associated AndroidLaunch object. 1319 * @param monitor A Progress monitor 1320 * @return false if cancelled by the monitor 1321 * @throws CoreException 1322 */ 1323 @SuppressWarnings("deprecation") 1324 public static boolean connectRemoteDebugger(int debugPort, 1325 AndroidLaunch launch, IProgressMonitor monitor) 1326 throws CoreException { 1327 // get some default parameters. 1328 int connectTimeout = JavaRuntime.getPreferences().getInt(JavaRuntime.PREF_CONNECT_TIMEOUT); 1329 1330 HashMap<String, String> newMap = new HashMap<String, String>(); 1331 1332 newMap.put("hostname", "localhost"); //$NON-NLS-1$ //$NON-NLS-2$ 1333 1334 newMap.put("port", Integer.toString(debugPort)); //$NON-NLS-1$ 1335 1336 newMap.put("timeout", Integer.toString(connectTimeout)); 1337 1338 // get the default VM connector 1339 IVMConnector connector = JavaRuntime.getDefaultVMConnector(); 1340 1341 // connect to remote VM 1342 connector.connect(newMap, monitor, launch); 1343 1344 // check for cancellation 1345 if (monitor.isCanceled()) { 1346 IDebugTarget[] debugTargets = launch.getDebugTargets(); 1347 for (IDebugTarget target : debugTargets) { 1348 if (target.canDisconnect()) { 1349 target.disconnect(); 1350 } 1351 } 1352 return false; 1353 } 1354 1355 return true; 1356 } 1357 1358 /** 1359 * Launch a new thread that connects a remote debugger on the specified port. 1360 * @param debugPort The port to connect the debugger to 1361 * @param androidLaunch The associated AndroidLaunch object. 1362 * @param monitor A Progress monitor 1363 * @see #connectRemoteDebugger(int, AndroidLaunch, IProgressMonitor) 1364 */ 1365 public static void launchRemoteDebugger(final int debugPort, final AndroidLaunch androidLaunch, 1366 final IProgressMonitor monitor) { 1367 new Thread("Debugger connection") { //$NON-NLS-1$ 1368 @Override 1369 public void run() { 1370 try { 1371 connectRemoteDebugger(debugPort, androidLaunch, monitor); 1372 } catch (CoreException e) { 1373 androidLaunch.stopLaunch(); 1374 } 1375 monitor.done(); 1376 } 1377 }.start(); 1378 } 1379 1380 /** 1381 * Sent when a new {@link AndroidDebugBridge} is started. 1382 * <p/> 1383 * This is sent from a non UI thread. 1384 * @param bridge the new {@link AndroidDebugBridge} object. 1385 * 1386 * @see IDebugBridgeChangeListener#bridgeChanged(AndroidDebugBridge) 1387 */ 1388 @Override 1389 public void bridgeChanged(AndroidDebugBridge bridge) { 1390 // The adb server has changed. We cancel any pending launches. 1391 String message = "adb server change: cancelling '%1$s'!"; 1392 synchronized (sListLock) { 1393 for (DelayedLaunchInfo launchInfo : mWaitingForReadyEmulatorList) { 1394 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1395 String.format(message, launchInfo.getLaunchAction().getLaunchDescription())); 1396 stopLaunch(launchInfo); 1397 } 1398 for (DelayedLaunchInfo launchInfo : mWaitingForDebuggerApplications) { 1399 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1400 String.format(message, 1401 launchInfo.getLaunchAction().getLaunchDescription())); 1402 stopLaunch(launchInfo); 1403 } 1404 1405 mWaitingForReadyEmulatorList.clear(); 1406 mWaitingForDebuggerApplications.clear(); 1407 } 1408 } 1409 1410 /** 1411 * Sent when the a device is connected to the {@link AndroidDebugBridge}. 1412 * <p/> 1413 * This is sent from a non UI thread. 1414 * @param device the new device. 1415 * 1416 * @see IDeviceChangeListener#deviceConnected(IDevice) 1417 */ 1418 @Override 1419 public void deviceConnected(IDevice device) { 1420 synchronized (sListLock) { 1421 // look if there's an app waiting for a device 1422 if (mWaitingForEmulatorLaunches.size() > 0) { 1423 // get/remove first launch item from the list 1424 // FIXME: what if we have multiple launches waiting? 1425 DelayedLaunchInfo launchInfo = mWaitingForEmulatorLaunches.get(0); 1426 mWaitingForEmulatorLaunches.remove(0); 1427 1428 // give the launch item its device for later use. 1429 launchInfo.setDevice(device); 1430 1431 // and move it to the other list 1432 mWaitingForReadyEmulatorList.add(launchInfo); 1433 1434 // and tell the user about it 1435 AdtPlugin.printToConsole(launchInfo.getProject(), 1436 String.format("New emulator found: %1$s", device.getSerialNumber())); 1437 AdtPlugin.printToConsole(launchInfo.getProject(), 1438 String.format("Waiting for HOME ('%1$s') to be launched...", 1439 AdtPlugin.getDefault().getPreferenceStore().getString( 1440 AdtPrefs.PREFS_HOME_PACKAGE))); 1441 } 1442 } 1443 } 1444 1445 /** 1446 * Sent when the a device is connected to the {@link AndroidDebugBridge}. 1447 * <p/> 1448 * This is sent from a non UI thread. 1449 * @param device the new device. 1450 * 1451 * @see IDeviceChangeListener#deviceDisconnected(IDevice) 1452 */ 1453 @Override 1454 @SuppressWarnings("unchecked") 1455 public void deviceDisconnected(IDevice device) { 1456 // any pending launch on this device must be canceled. 1457 String message = "%1$s disconnected! Cancelling '%2$s'!"; 1458 synchronized (sListLock) { 1459 ArrayList<DelayedLaunchInfo> copyList = 1460 (ArrayList<DelayedLaunchInfo>) mWaitingForReadyEmulatorList.clone(); 1461 for (DelayedLaunchInfo launchInfo : copyList) { 1462 if (launchInfo.getDevice() == device) { 1463 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1464 String.format(message, device.getSerialNumber(), 1465 launchInfo.getLaunchAction().getLaunchDescription())); 1466 stopLaunch(launchInfo); 1467 } 1468 } 1469 copyList = (ArrayList<DelayedLaunchInfo>) mWaitingForDebuggerApplications.clone(); 1470 for (DelayedLaunchInfo launchInfo : copyList) { 1471 if (launchInfo.getDevice() == device) { 1472 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1473 String.format(message, device.getSerialNumber(), 1474 launchInfo.getLaunchAction().getLaunchDescription())); 1475 stopLaunch(launchInfo); 1476 } 1477 } 1478 } 1479 } 1480 1481 /** 1482 * Sent when a device data changed, or when clients are started/terminated on the device. 1483 * <p/> 1484 * This is sent from a non UI thread. 1485 * @param device the device that was updated. 1486 * @param changeMask the mask indicating what changed. 1487 * 1488 * @see IDeviceChangeListener#deviceChanged(IDevice, int) 1489 */ 1490 @Override 1491 public void deviceChanged(IDevice device, int changeMask) { 1492 // We could check if any starting device we care about is now ready, but we can wait for 1493 // its home app to show up, so... 1494 } 1495 1496 /** 1497 * Sent when an existing client information changed. 1498 * <p/> 1499 * This is sent from a non UI thread. 1500 * @param client the updated client. 1501 * @param changeMask the bit mask describing the changed properties. It can contain 1502 * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} 1503 * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, 1504 * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, 1505 * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} 1506 * 1507 * @see IClientChangeListener#clientChanged(Client, int) 1508 */ 1509 @Override 1510 public void clientChanged(final Client client, int changeMask) { 1511 boolean connectDebugger = false; 1512 if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) { 1513 String applicationName = client.getClientData().getClientDescription(); 1514 if (applicationName != null) { 1515 IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); 1516 String home = store.getString(AdtPrefs.PREFS_HOME_PACKAGE); 1517 1518 if (home.equals(applicationName)) { 1519 1520 // looks like home is up, get its device 1521 IDevice device = client.getDevice(); 1522 1523 // look for application waiting for home 1524 synchronized (sListLock) { 1525 for (int i = 0; i < mWaitingForReadyEmulatorList.size(); ) { 1526 DelayedLaunchInfo launchInfo = mWaitingForReadyEmulatorList.get(i); 1527 if (launchInfo.getDevice() == device) { 1528 // it's match, remove from the list 1529 mWaitingForReadyEmulatorList.remove(i); 1530 1531 // We couldn't check earlier the API level of the device 1532 // (it's asynchronous when the device boot, and usually 1533 // deviceConnected is called before it's queried for its build info) 1534 // so we check now 1535 if (checkBuildInfo(launchInfo, device) == false) { 1536 // device is not the proper API! 1537 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1538 "Launch canceled!"); 1539 stopLaunch(launchInfo); 1540 return; 1541 } 1542 1543 AdtPlugin.printToConsole(launchInfo.getProject(), 1544 String.format("HOME is up on device '%1$s'", 1545 device.getSerialNumber())); 1546 1547 // attempt to sync the new package onto the device. 1548 if (syncApp(launchInfo, device)) { 1549 // application package is sync'ed, lets attempt to launch it. 1550 launchApp(launchInfo, device); 1551 } else { 1552 // failure! Cancel and return 1553 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1554 "Launch canceled!"); 1555 stopLaunch(launchInfo); 1556 } 1557 1558 break; 1559 } else { 1560 i++; 1561 } 1562 } 1563 } 1564 } 1565 1566 // check if it's already waiting for a debugger, and if so we connect to it. 1567 if (client.getClientData().getDebuggerConnectionStatus() == DebuggerStatus.WAITING) { 1568 // search for this client in the list; 1569 synchronized (sListLock) { 1570 int index = mUnknownClientsWaitingForDebugger.indexOf(client); 1571 if (index != -1) { 1572 connectDebugger = true; 1573 mUnknownClientsWaitingForDebugger.remove(client); 1574 } 1575 } 1576 } 1577 } 1578 } 1579 1580 // if it's not home, it could be an app that is now in debugger mode that we're waiting for 1581 // lets check it 1582 1583 if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) == Client.CHANGE_DEBUGGER_STATUS) { 1584 ClientData clientData = client.getClientData(); 1585 String applicationName = client.getClientData().getClientDescription(); 1586 if (clientData.getDebuggerConnectionStatus() == DebuggerStatus.WAITING) { 1587 // Get the application name, and make sure its valid. 1588 if (applicationName == null) { 1589 // looks like we don't have the client yet, so we keep it around for when its 1590 // name becomes available. 1591 synchronized (sListLock) { 1592 mUnknownClientsWaitingForDebugger.add(client); 1593 } 1594 return; 1595 } else { 1596 connectDebugger = true; 1597 } 1598 } 1599 } 1600 1601 if (connectDebugger) { 1602 Log.d("adt", "Debugging " + client); 1603 // now check it against the apps waiting for a debugger 1604 String applicationName = client.getClientData().getClientDescription(); 1605 Log.d("adt", "App Name: " + applicationName); 1606 synchronized (sListLock) { 1607 for (int i = 0; i < mWaitingForDebuggerApplications.size(); ) { 1608 final DelayedLaunchInfo launchInfo = mWaitingForDebuggerApplications.get(i); 1609 if (client.getDevice() == launchInfo.getDevice() && 1610 applicationName.equals(launchInfo.getDebugPackageName())) { 1611 // this is a match. We remove the launch info from the list 1612 mWaitingForDebuggerApplications.remove(i); 1613 1614 // and connect the debugger. 1615 String msg = String.format( 1616 "Attempting to connect debugger to '%1$s' on port %2$d", 1617 launchInfo.getDebugPackageName(), client.getDebuggerListenPort()); 1618 AdtPlugin.printToConsole(launchInfo.getProject(), msg); 1619 1620 new Thread("Debugger Connection") { //$NON-NLS-1$ 1621 @Override 1622 public void run() { 1623 try { 1624 if (connectRemoteDebugger( 1625 client.getDebuggerListenPort(), 1626 launchInfo.getLaunch(), 1627 launchInfo.getMonitor()) == false) { 1628 return; 1629 } 1630 } catch (CoreException e) { 1631 // well something went wrong. 1632 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1633 String.format("Launch error: %s", e.getMessage())); 1634 // stop the launch 1635 stopLaunch(launchInfo); 1636 } 1637 1638 launchInfo.getMonitor().done(); 1639 } 1640 }.start(); 1641 1642 // we're done processing this client. 1643 return; 1644 1645 } else { 1646 i++; 1647 } 1648 } 1649 } 1650 1651 // if we get here, we haven't found an app that we were launching, so we look 1652 // for opened android projects that contains the app asking for a debugger. 1653 // If we find one, we automatically connect to it. 1654 IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName); 1655 1656 if (project != null) { 1657 debugRunningApp(project, client.getDebuggerListenPort()); 1658 } 1659 } 1660 } 1661 1662 /** 1663 * Get the stderr/stdout outputs of a process and return when the process is done. 1664 * Both <b>must</b> be read or the process will block on windows. 1665 * @param process The process to get the output from 1666 */ 1667 private void grabEmulatorOutput(final Process process) { 1668 // read the lines as they come. if null is returned, it's 1669 // because the process finished 1670 new Thread("") { //$NON-NLS-1$ 1671 @Override 1672 public void run() { 1673 // create a buffer to read the stderr output 1674 InputStreamReader is = new InputStreamReader(process.getErrorStream()); 1675 BufferedReader errReader = new BufferedReader(is); 1676 1677 try { 1678 while (true) { 1679 String line = errReader.readLine(); 1680 if (line != null) { 1681 AdtPlugin.printErrorToConsole("Emulator", line); 1682 } else { 1683 break; 1684 } 1685 } 1686 } catch (IOException e) { 1687 // do nothing. 1688 } 1689 } 1690 }.start(); 1691 1692 new Thread("") { //$NON-NLS-1$ 1693 @Override 1694 public void run() { 1695 InputStreamReader is = new InputStreamReader(process.getInputStream()); 1696 BufferedReader outReader = new BufferedReader(is); 1697 1698 try { 1699 while (true) { 1700 String line = outReader.readLine(); 1701 if (line != null) { 1702 AdtPlugin.printToConsole("Emulator", line); 1703 } else { 1704 break; 1705 } 1706 } 1707 } catch (IOException e) { 1708 // do nothing. 1709 } 1710 } 1711 }.start(); 1712 } 1713 1714 /* (non-Javadoc) 1715 * @see com.android.ide.eclipse.adt.launch.ILaunchController#stopLaunch(com.android.ide.eclipse.adt.launch.AndroidLaunchController.DelayedLaunchInfo) 1716 */ 1717 @Override 1718 public void stopLaunch(DelayedLaunchInfo launchInfo) { 1719 launchInfo.getLaunch().stopLaunch(); 1720 synchronized (sListLock) { 1721 mWaitingForReadyEmulatorList.remove(launchInfo); 1722 mWaitingForDebuggerApplications.remove(launchInfo); 1723 } 1724 } 1725 } 1726