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")) { //$NON-NLS-1$ 1198 if (retryMode != InstallRetryMode.NEVER) { 1199 boolean prompt = AdtPlugin.displayPrompt("Application Install", 1200 "Re-installation failed due to different application signatures. You must perform a full uninstall of the application. WARNING: This will remove the application data!\nDo you want to uninstall?"); 1201 if (prompt) { 1202 doUninstall(device, launchInfo); 1203 String res = doInstall(launchInfo, remotePath, device, false); 1204 return checkInstallResult(res, device, launchInfo, remotePath, 1205 InstallRetryMode.NEVER); 1206 } 1207 } 1208 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1209 "Re-installation failed due to different application signatures.", 1210 "You must perform a full uninstall of the application. WARNING: This will remove the application data!", 1211 String.format("Please execute 'adb uninstall %1$s' in a shell.", launchInfo.getPackageName())); 1212 } else { 1213 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1214 String.format("Installation error: %1$s", result), 1215 "Please check logcat output for more details."); 1216 } 1217 1218 return false; 1219 } 1220 1221 /** 1222 * Performs the uninstallation of an application. 1223 * @param device the device on which to install the application. 1224 * @param launchInfo the {@link DelayedLaunchInfo}. 1225 * @return a {@link String} with an error code, or <code>null</code> if success. 1226 * @throws InstallException if the installation failed. 1227 */ 1228 private String doUninstall(IDevice device, DelayedLaunchInfo launchInfo) 1229 throws InstallException { 1230 try { 1231 return device.uninstallPackage(launchInfo.getPackageName()); 1232 } catch (InstallException e) { 1233 String msg = String.format( 1234 "Failed to uninstall %1$s: %2$s", launchInfo.getPackageName(), e.getMessage()); 1235 AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg); 1236 throw e; 1237 } 1238 } 1239 1240 /** 1241 * Performs the installation of an application whose package has been uploaded on the device. 1242 * 1243 * @param launchInfo the {@link DelayedLaunchInfo}. 1244 * @param remotePath the path of the application package in the device tmp folder. 1245 * @param device the device on which to install the application. 1246 * @param reinstall 1247 * @return a {@link String} with an error code, or <code>null</code> if success. 1248 * @throws InstallException if the uninstallation failed. 1249 */ 1250 private String doInstall(DelayedLaunchInfo launchInfo, final String remotePath, 1251 final IDevice device, boolean reinstall) throws InstallException { 1252 return device.installRemotePackage(remotePath, reinstall); 1253 } 1254 1255 /** 1256 * launches an application on a device or emulator 1257 * 1258 * @param info the {@link DelayedLaunchInfo} that indicates the launch action 1259 * @param device the device or emulator to launch the application on 1260 */ 1261 @Override 1262 public void launchApp(final DelayedLaunchInfo info, IDevice device) { 1263 if (info.isDebugMode()) { 1264 synchronized (sListLock) { 1265 if (mWaitingForDebuggerApplications.contains(info) == false) { 1266 mWaitingForDebuggerApplications.add(info); 1267 } 1268 } 1269 } 1270 if (doLaunchAction(info, device)) { 1271 // if the app is not a debug app, we need to do some clean up, as 1272 // the process is done! 1273 if (info.isDebugMode() == false) { 1274 // stop the launch object, since there's no debug, and it can't 1275 // provide any control over the app 1276 stopLaunch(info); 1277 } 1278 } else { 1279 // something went wrong or no further launch action needed 1280 // lets stop the Launch 1281 stopLaunch(info); 1282 } 1283 } 1284 1285 private boolean doLaunchAction(final DelayedLaunchInfo info, Collection<IDevice> devices) { 1286 boolean result = info.getLaunchAction().doLaunchAction(info, devices); 1287 1288 // Monitor the logcat output on the launched device to notify 1289 // the user if any significant error occurs that is visible from logcat 1290 for (IDevice d : devices) { 1291 DdmsPlugin.getDefault().startLogCatMonitor(d); 1292 } 1293 1294 return result; 1295 } 1296 1297 private boolean doLaunchAction(final DelayedLaunchInfo info, IDevice device) { 1298 return doLaunchAction(info, Collections.singletonList(device)); 1299 } 1300 1301 private boolean launchEmulator(AndroidLaunchConfiguration config, AvdInfo avdToLaunch) { 1302 1303 // split the custom command line in segments 1304 ArrayList<String> customArgs = new ArrayList<String>(); 1305 boolean hasWipeData = false; 1306 if (config.mEmulatorCommandLine != null && config.mEmulatorCommandLine.length() > 0) { 1307 String[] segments = config.mEmulatorCommandLine.split("\\s+"); //$NON-NLS-1$ 1308 1309 // we need to remove the empty strings 1310 for (String s : segments) { 1311 if (s.length() > 0) { 1312 customArgs.add(s); 1313 if (!hasWipeData && s.equals(FLAG_WIPE_DATA)) { 1314 hasWipeData = true; 1315 } 1316 } 1317 } 1318 } 1319 1320 boolean needsWipeData = config.mWipeData && !hasWipeData; 1321 if (needsWipeData) { 1322 if (!AdtPlugin.displayPrompt("Android Launch", "Are you sure you want to wipe all user data when starting this emulator?")) { 1323 needsWipeData = false; 1324 } 1325 } 1326 1327 // build the command line based on the available parameters. 1328 ArrayList<String> list = new ArrayList<String>(); 1329 1330 String path = AdtPlugin.getOsAbsoluteEmulator(); 1331 1332 list.add(path); 1333 1334 list.add(FLAG_AVD); 1335 list.add(avdToLaunch.getName()); 1336 1337 if (config.mNetworkSpeed != null) { 1338 list.add(FLAG_NETSPEED); 1339 list.add(config.mNetworkSpeed); 1340 } 1341 1342 if (config.mNetworkDelay != null) { 1343 list.add(FLAG_NETDELAY); 1344 list.add(config.mNetworkDelay); 1345 } 1346 1347 if (needsWipeData) { 1348 list.add(FLAG_WIPE_DATA); 1349 } 1350 1351 if (config.mNoBootAnim) { 1352 list.add(FLAG_NO_BOOT_ANIM); 1353 } 1354 1355 list.addAll(customArgs); 1356 1357 // convert the list into an array for the call to exec. 1358 String[] command = list.toArray(new String[list.size()]); 1359 1360 // launch the emulator 1361 try { 1362 Process process = Runtime.getRuntime().exec(command); 1363 grabEmulatorOutput(process); 1364 } catch (IOException e) { 1365 return false; 1366 } 1367 1368 return true; 1369 } 1370 1371 /** 1372 * Looks for and returns an existing {@link ILaunchConfiguration} object for a 1373 * specified project. 1374 * @param manager The {@link ILaunchManager}. 1375 * @param type The {@link ILaunchConfigurationType}. 1376 * @param projectName The name of the project 1377 * @return an existing <code>ILaunchConfiguration</code> object matching the project, or 1378 * <code>null</code>. 1379 */ 1380 private static ILaunchConfiguration findConfig(ILaunchManager manager, 1381 ILaunchConfigurationType type, String projectName) { 1382 try { 1383 ILaunchConfiguration[] configs = manager.getLaunchConfigurations(type); 1384 1385 for (ILaunchConfiguration config : configs) { 1386 if (config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, 1387 "").equals(projectName)) { //$NON-NLS-1$ 1388 return config; 1389 } 1390 } 1391 } catch (CoreException e) { 1392 MessageDialog.openError(AdtPlugin.getShell(), 1393 "Launch Error", e.getStatus().getMessage()); 1394 } 1395 1396 // didn't find anything that matches. Return null 1397 return null; 1398 } 1399 1400 1401 /** 1402 * Connects a remote debugger on the specified port. 1403 * @param debugPort The port to connect the debugger to 1404 * @param launch The associated AndroidLaunch object. 1405 * @param monitor A Progress monitor 1406 * @return false if cancelled by the monitor 1407 * @throws CoreException 1408 */ 1409 @SuppressWarnings("deprecation") 1410 public static boolean connectRemoteDebugger(int debugPort, 1411 AndroidLaunch launch, IProgressMonitor monitor) 1412 throws CoreException { 1413 // get some default parameters. 1414 int connectTimeout = JavaRuntime.getPreferences().getInt(JavaRuntime.PREF_CONNECT_TIMEOUT); 1415 1416 HashMap<String, String> newMap = new HashMap<String, String>(); 1417 1418 newMap.put("hostname", "localhost"); //$NON-NLS-1$ //$NON-NLS-2$ 1419 1420 newMap.put("port", Integer.toString(debugPort)); //$NON-NLS-1$ 1421 1422 newMap.put("timeout", Integer.toString(connectTimeout)); 1423 1424 // get the default VM connector 1425 IVMConnector connector = JavaRuntime.getDefaultVMConnector(); 1426 1427 // connect to remote VM 1428 connector.connect(newMap, monitor, launch); 1429 1430 // check for cancellation 1431 if (monitor.isCanceled()) { 1432 IDebugTarget[] debugTargets = launch.getDebugTargets(); 1433 for (IDebugTarget target : debugTargets) { 1434 if (target.canDisconnect()) { 1435 target.disconnect(); 1436 } 1437 } 1438 return false; 1439 } 1440 1441 return true; 1442 } 1443 1444 /** 1445 * Launch a new thread that connects a remote debugger on the specified port. 1446 * @param debugPort The port to connect the debugger to 1447 * @param androidLaunch The associated AndroidLaunch object. 1448 * @param monitor A Progress monitor 1449 * @see #connectRemoteDebugger(int, AndroidLaunch, IProgressMonitor) 1450 */ 1451 public static void launchRemoteDebugger(final int debugPort, final AndroidLaunch androidLaunch, 1452 final IProgressMonitor monitor) { 1453 new Thread("Debugger connection") { //$NON-NLS-1$ 1454 @Override 1455 public void run() { 1456 try { 1457 connectRemoteDebugger(debugPort, androidLaunch, monitor); 1458 } catch (CoreException e) { 1459 androidLaunch.stopLaunch(); 1460 } 1461 monitor.done(); 1462 } 1463 }.start(); 1464 } 1465 1466 /** 1467 * Sent when a new {@link AndroidDebugBridge} is started. 1468 * <p/> 1469 * This is sent from a non UI thread. 1470 * @param bridge the new {@link AndroidDebugBridge} object. 1471 * 1472 * @see IDebugBridgeChangeListener#bridgeChanged(AndroidDebugBridge) 1473 */ 1474 @Override 1475 public void bridgeChanged(AndroidDebugBridge bridge) { 1476 // The adb server has changed. We cancel any pending launches. 1477 String message = "adb server change: cancelling '%1$s'!"; 1478 synchronized (sListLock) { 1479 for (DelayedLaunchInfo launchInfo : mWaitingForReadyEmulatorList) { 1480 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1481 String.format(message, launchInfo.getLaunchAction().getLaunchDescription())); 1482 stopLaunch(launchInfo); 1483 } 1484 for (DelayedLaunchInfo launchInfo : mWaitingForDebuggerApplications) { 1485 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1486 String.format(message, 1487 launchInfo.getLaunchAction().getLaunchDescription())); 1488 stopLaunch(launchInfo); 1489 } 1490 1491 mWaitingForReadyEmulatorList.clear(); 1492 mWaitingForDebuggerApplications.clear(); 1493 } 1494 } 1495 1496 /** 1497 * Sent when the a device is connected to the {@link AndroidDebugBridge}. 1498 * <p/> 1499 * This is sent from a non UI thread. 1500 * @param device the new device. 1501 * 1502 * @see IDeviceChangeListener#deviceConnected(IDevice) 1503 */ 1504 @Override 1505 public void deviceConnected(IDevice device) { 1506 synchronized (sListLock) { 1507 // look if there's an app waiting for a device 1508 if (mWaitingForEmulatorLaunches.size() > 0) { 1509 // get/remove first launch item from the list 1510 // FIXME: what if we have multiple launches waiting? 1511 DelayedLaunchInfo launchInfo = mWaitingForEmulatorLaunches.get(0); 1512 mWaitingForEmulatorLaunches.remove(0); 1513 1514 // give the launch item its device for later use. 1515 launchInfo.setDevice(device); 1516 1517 // and move it to the other list 1518 mWaitingForReadyEmulatorList.add(launchInfo); 1519 1520 // and tell the user about it 1521 AdtPlugin.printToConsole(launchInfo.getProject(), 1522 String.format("New emulator found: %1$s", device.getSerialNumber())); 1523 AdtPlugin.printToConsole(launchInfo.getProject(), 1524 String.format("Waiting for HOME ('%1$s') to be launched...", 1525 AdtPlugin.getDefault().getPreferenceStore().getString( 1526 AdtPrefs.PREFS_HOME_PACKAGE))); 1527 } 1528 } 1529 } 1530 1531 /** 1532 * Sent when the a device is connected to the {@link AndroidDebugBridge}. 1533 * <p/> 1534 * This is sent from a non UI thread. 1535 * @param device the new device. 1536 * 1537 * @see IDeviceChangeListener#deviceDisconnected(IDevice) 1538 */ 1539 @Override 1540 public void deviceDisconnected(IDevice device) { 1541 // any pending launch on this device must be canceled. 1542 String message = "%1$s disconnected! Cancelling '%2$s'!"; 1543 synchronized (sListLock) { 1544 ArrayList<DelayedLaunchInfo> copyList = 1545 (ArrayList<DelayedLaunchInfo>) mWaitingForReadyEmulatorList.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 copyList = (ArrayList<DelayedLaunchInfo>) mWaitingForDebuggerApplications.clone(); 1555 for (DelayedLaunchInfo launchInfo : copyList) { 1556 if (launchInfo.getDevice() == device) { 1557 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1558 String.format(message, device.getSerialNumber(), 1559 launchInfo.getLaunchAction().getLaunchDescription())); 1560 stopLaunch(launchInfo); 1561 } 1562 } 1563 } 1564 } 1565 1566 /** 1567 * Sent when a device data changed, or when clients are started/terminated on the device. 1568 * <p/> 1569 * This is sent from a non UI thread. 1570 * @param device the device that was updated. 1571 * @param changeMask the mask indicating what changed. 1572 * 1573 * @see IDeviceChangeListener#deviceChanged(IDevice, int) 1574 */ 1575 @Override 1576 public void deviceChanged(IDevice device, int changeMask) { 1577 // We could check if any starting device we care about is now ready, but we can wait for 1578 // its home app to show up, so... 1579 } 1580 1581 /** 1582 * Sent when an existing client information changed. 1583 * <p/> 1584 * This is sent from a non UI thread. 1585 * @param client the updated client. 1586 * @param changeMask the bit mask describing the changed properties. It can contain 1587 * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME} 1588 * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE}, 1589 * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE}, 1590 * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA} 1591 * 1592 * @see IClientChangeListener#clientChanged(Client, int) 1593 */ 1594 @Override 1595 public void clientChanged(final Client client, int changeMask) { 1596 boolean connectDebugger = false; 1597 if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) { 1598 String applicationName = client.getClientData().getClientDescription(); 1599 if (applicationName != null) { 1600 IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); 1601 String home = store.getString(AdtPrefs.PREFS_HOME_PACKAGE); 1602 1603 if (home.equals(applicationName)) { 1604 1605 // looks like home is up, get its device 1606 IDevice device = client.getDevice(); 1607 1608 // look for application waiting for home 1609 synchronized (sListLock) { 1610 for (int i = 0; i < mWaitingForReadyEmulatorList.size(); ) { 1611 DelayedLaunchInfo launchInfo = mWaitingForReadyEmulatorList.get(i); 1612 if (launchInfo.getDevice() == device) { 1613 // it's match, remove from the list 1614 mWaitingForReadyEmulatorList.remove(i); 1615 1616 // We couldn't check earlier the API level of the device 1617 // (it's asynchronous when the device boot, and usually 1618 // deviceConnected is called before it's queried for its build info) 1619 // so we check now 1620 if (checkBuildInfo(launchInfo, device) == false) { 1621 // device is not the proper API! 1622 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1623 "Launch canceled!"); 1624 stopLaunch(launchInfo); 1625 return; 1626 } 1627 1628 AdtPlugin.printToConsole(launchInfo.getProject(), 1629 String.format("HOME is up on device '%1$s'", 1630 device.getSerialNumber())); 1631 1632 // attempt to sync the new package onto the device. 1633 if (syncApp(launchInfo, device)) { 1634 // application package is sync'ed, lets attempt to launch it. 1635 launchApp(launchInfo, device); 1636 } else { 1637 // failure! Cancel and return 1638 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1639 "Launch canceled!"); 1640 stopLaunch(launchInfo); 1641 } 1642 1643 break; 1644 } else { 1645 i++; 1646 } 1647 } 1648 } 1649 } 1650 1651 // check if it's already waiting for a debugger, and if so we connect to it. 1652 if (client.getClientData().getDebuggerConnectionStatus() == DebuggerStatus.WAITING) { 1653 // search for this client in the list; 1654 synchronized (sListLock) { 1655 int index = mUnknownClientsWaitingForDebugger.indexOf(client); 1656 if (index != -1) { 1657 connectDebugger = true; 1658 mUnknownClientsWaitingForDebugger.remove(client); 1659 } 1660 } 1661 } 1662 } 1663 } 1664 1665 // if it's not home, it could be an app that is now in debugger mode that we're waiting for 1666 // lets check it 1667 1668 if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) == Client.CHANGE_DEBUGGER_STATUS) { 1669 ClientData clientData = client.getClientData(); 1670 String applicationName = client.getClientData().getClientDescription(); 1671 if (clientData.getDebuggerConnectionStatus() == DebuggerStatus.WAITING) { 1672 // Get the application name, and make sure its valid. 1673 if (applicationName == null) { 1674 // looks like we don't have the client yet, so we keep it around for when its 1675 // name becomes available. 1676 synchronized (sListLock) { 1677 mUnknownClientsWaitingForDebugger.add(client); 1678 } 1679 return; 1680 } else { 1681 connectDebugger = true; 1682 } 1683 } 1684 } 1685 1686 if (connectDebugger) { 1687 Log.d("adt", "Debugging " + client); 1688 // now check it against the apps waiting for a debugger 1689 String applicationName = client.getClientData().getClientDescription(); 1690 Log.d("adt", "App Name: " + applicationName); 1691 synchronized (sListLock) { 1692 for (int i = 0; i < mWaitingForDebuggerApplications.size(); ) { 1693 final DelayedLaunchInfo launchInfo = mWaitingForDebuggerApplications.get(i); 1694 if (client.getDevice() == launchInfo.getDevice() && 1695 applicationName.equals(launchInfo.getDebugPackageName())) { 1696 // this is a match. We remove the launch info from the list 1697 mWaitingForDebuggerApplications.remove(i); 1698 1699 // and connect the debugger. 1700 String msg = String.format( 1701 "Attempting to connect debugger to '%1$s' on port %2$d", 1702 launchInfo.getDebugPackageName(), client.getDebuggerListenPort()); 1703 AdtPlugin.printToConsole(launchInfo.getProject(), msg); 1704 1705 new Thread("Debugger Connection") { //$NON-NLS-1$ 1706 @Override 1707 public void run() { 1708 try { 1709 if (connectRemoteDebugger( 1710 client.getDebuggerListenPort(), 1711 launchInfo.getLaunch(), 1712 launchInfo.getMonitor()) == false) { 1713 return; 1714 } 1715 } catch (CoreException e) { 1716 // well something went wrong. 1717 AdtPlugin.printErrorToConsole(launchInfo.getProject(), 1718 String.format("Launch error: %s", e.getMessage())); 1719 // stop the launch 1720 stopLaunch(launchInfo); 1721 } 1722 1723 launchInfo.getMonitor().done(); 1724 } 1725 }.start(); 1726 1727 // we're done processing this client. 1728 return; 1729 1730 } else { 1731 i++; 1732 } 1733 } 1734 } 1735 1736 // if we get here, we haven't found an app that we were launching, so we look 1737 // for opened android projects that contains the app asking for a debugger. 1738 // If we find one, we automatically connect to it. 1739 IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName); 1740 1741 if (project != null) { 1742 debugRunningApp(project, client.getDebuggerListenPort()); 1743 } 1744 } 1745 } 1746 1747 /** 1748 * Get the stderr/stdout outputs of a process and return when the process is done. 1749 * Both <b>must</b> be read or the process will block on windows. 1750 * @param process The process to get the output from 1751 */ 1752 private void grabEmulatorOutput(final Process process) { 1753 // read the lines as they come. if null is returned, it's 1754 // because the process finished 1755 new Thread("") { //$NON-NLS-1$ 1756 @Override 1757 public void run() { 1758 // create a buffer to read the stderr output 1759 InputStreamReader is = new InputStreamReader(process.getErrorStream()); 1760 BufferedReader errReader = new BufferedReader(is); 1761 1762 try { 1763 while (true) { 1764 String line = errReader.readLine(); 1765 if (line != null) { 1766 AdtPlugin.printErrorToConsole("Emulator", line); 1767 } else { 1768 break; 1769 } 1770 } 1771 } catch (IOException e) { 1772 // do nothing. 1773 } 1774 } 1775 }.start(); 1776 1777 new Thread("") { //$NON-NLS-1$ 1778 @Override 1779 public void run() { 1780 InputStreamReader is = new InputStreamReader(process.getInputStream()); 1781 BufferedReader outReader = new BufferedReader(is); 1782 1783 try { 1784 while (true) { 1785 String line = outReader.readLine(); 1786 if (line != null) { 1787 AdtPlugin.printToConsole("Emulator", line); 1788 } else { 1789 break; 1790 } 1791 } 1792 } catch (IOException e) { 1793 // do nothing. 1794 } 1795 } 1796 }.start(); 1797 } 1798 1799 /* (non-Javadoc) 1800 * @see com.android.ide.eclipse.adt.launch.ILaunchController#stopLaunch(com.android.ide.eclipse.adt.launch.AndroidLaunchController.DelayedLaunchInfo) 1801 */ 1802 @Override 1803 public void stopLaunch(DelayedLaunchInfo launchInfo) { 1804 launchInfo.getLaunch().stopLaunch(); 1805 synchronized (sListLock) { 1806 mWaitingForReadyEmulatorList.remove(launchInfo); 1807 mWaitingForDebuggerApplications.remove(launchInfo); 1808 } 1809 } 1810 } 1811