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