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