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