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