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