aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AMReceiver.java161
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/ActivityLaunchAction.java124
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunch.java57
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchConfiguration.java192
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java1872
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AvdCompatibility.java58
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/DelayedLaunchInfo.java244
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/DeviceChooserDialog.java824
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/EmptyLaunchAction.java49
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/EmulatorConfigTab.java596
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/IAndroidLaunchAction.java43
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/ILaunchController.java40
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/JUnitLaunchConfigDelegate.java156
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigDelegate.java424
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigTabGroup.java42
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchMessages.java60
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchShortcut.java104
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/MainLaunchConfigTab.java494
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchAction.java293
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigDelegate.java286
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigurationTab.java1061
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchShortcut.java56
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitPropertyTester.java131
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitTabGroup.java44
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java159
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidJUnitLaunchInfo.java148
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidTestReference.java65
-rwxr-xr-xeclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java524
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCaseReference.java67
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCollector.java136
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestSuiteReference.java92
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/messages.properties43
32 files changed, 8645 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AMReceiver.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AMReceiver.java
new file mode 100644
index 000000000..ea61945a2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AMReceiver.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.MultiLineReceiver;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+/**
+ * Output receiver for am process (Activity Manager)
+ *
+ * Monitors adb output for am errors, and retries launch as appropriate.
+ */
+public class AMReceiver extends MultiLineReceiver {
+
+ private static final int MAX_ATTEMPT_COUNT = 5;
+ private static final Pattern sAmErrorType = Pattern.compile("Error type (\\d+)"); //$NON-NLS-1$
+
+ private final DelayedLaunchInfo mLaunchInfo;
+ private final IDevice mDevice;
+ private final ILaunchController mLaunchController;
+
+ /**
+ * Basic constructor.
+ *
+ * @param launchInfo the {@link DelayedLaunchInfo} associated with the am process.
+ * @param device the Android device on which the launch is done.
+ * @param launchController the {@link ILaunchController} that is managing the launch
+ */
+ public AMReceiver(DelayedLaunchInfo launchInfo, IDevice device,
+ ILaunchController launchController) {
+ mLaunchInfo = launchInfo;
+ mDevice = device;
+ mLaunchController = launchController;
+ }
+
+ /**
+ * Monitors the am process for error messages. If an error occurs, will reattempt launch up to
+ * <code>MAX_ATTEMPT_COUNT</code> times.
+ *
+ * @param lines a portion of the am output
+ *
+ * @see MultiLineReceiver#processNewLines(String[])
+ */
+ @Override
+ public void processNewLines(String[] lines) {
+ // first we check if one starts with error
+ ArrayList<String> array = new ArrayList<String>();
+ boolean error = false;
+ boolean warning = false;
+ for (String s : lines) {
+ // ignore empty lines.
+ if (s.length() == 0) {
+ continue;
+ }
+
+ // check for errors that output an error type, if the attempt count is still
+ // valid. If not the whole text will be output in the console
+ if (mLaunchInfo.getAttemptCount() < MAX_ATTEMPT_COUNT &&
+ mLaunchInfo.isCancelled() == false) {
+ Matcher m = sAmErrorType.matcher(s);
+ if (m.matches()) {
+ // get the error type
+ int type = Integer.parseInt(m.group(1));
+
+ final int waitTime = 3;
+ String msg;
+
+ switch (type) {
+ case 1:
+ /* Intended fall through */
+ case 2:
+ msg = String.format(
+ "Device not ready. Waiting %1$d seconds before next attempt.",
+ waitTime);
+ break;
+ case 3:
+ msg = String.format(
+ "New package not yet registered with the system. Waiting %1$d seconds before next attempt.",
+ waitTime);
+ break;
+ default:
+ msg = String.format(
+ "Device not ready (%2$d). Waiting %1$d seconds before next attempt.",
+ waitTime, type);
+ break;
+
+ }
+
+ AdtPlugin.printToConsole(mLaunchInfo.getProject(), msg);
+
+ // launch another thread, that waits a bit and attempts another launch
+ new Thread("Delayed Launch attempt") {
+ @Override
+ public void run() {
+ try {
+ sleep(waitTime * 1000);
+ } catch (InterruptedException e) {
+ // ignore
+ }
+
+ mLaunchController.launchApp(mLaunchInfo, mDevice);
+ }
+ }.start();
+
+ // no need to parse the rest
+ return;
+ }
+ }
+
+ // check for error if needed
+ if (error == false && s.startsWith("Error:")) { //$NON-NLS-1$
+ error = true;
+ }
+ if (warning == false && s.startsWith("Warning:")) { //$NON-NLS-1$
+ warning = true;
+ }
+
+ // add the line to the list
+ array.add("ActivityManager: " + s); //$NON-NLS-1$
+ }
+
+ // then we display them in the console
+ if (warning || error) {
+ AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(), array.toArray());
+ } else {
+ AdtPlugin.printToConsole(mLaunchInfo.getProject(), array.toArray());
+ }
+
+ // if error then we cancel the launch, and remove the delayed info
+ if (error) {
+ mLaunchController.stopLaunch(mLaunchInfo);
+ }
+ }
+
+ /**
+ * Returns true if launch has been cancelled
+ */
+ @Override
+ public boolean isCancelled() {
+ return mLaunchInfo.isCancelled();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/ActivityLaunchAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/ActivityLaunchAction.java
new file mode 100644
index 000000000..311b19aa7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/ActivityLaunchAction.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import java.io.IOException;
+import java.util.Collection;
+
+/**
+ * Launches the given activity
+ */
+public class ActivityLaunchAction implements IAndroidLaunchAction {
+
+ private final String mActivity;
+ private final ILaunchController mLaunchController;
+
+ /**
+ * Creates a ActivityLaunchAction
+ *
+ * @param activity fully qualified activity name to launch
+ * @param controller the {@link ILaunchController} that performs launch
+ */
+ public ActivityLaunchAction(String activity, ILaunchController controller) {
+ mActivity = activity;
+ mLaunchController = controller;
+ }
+
+ public boolean doLaunchAction(DelayedLaunchInfo info, IDevice device) {
+ String command = "am start" //$NON-NLS-1$
+ + (info.isDebugMode() ? " -D" //$NON-NLS-1$
+ : "") //$NON-NLS-1$
+ + " -n " //$NON-NLS-1$
+ + info.getPackageName() + "/" //$NON-NLS-1$
+ + mActivity.replaceAll("\\$", "\\\\\\$") //$NON-NLS-1$ //$NON-NLS-2$
+ + " -a android.intent.action.MAIN" //$NON-NLS-1$
+ + " -c android.intent.category.LAUNCHER";
+ try {
+ String msg = String.format("Starting activity %1$s on device %2$s", mActivity,
+ device);
+ AdtPlugin.printToConsole(info.getProject(), msg);
+
+ // In debug mode, we need to add the info to the list of application monitoring
+ // client changes.
+ // increment launch attempt count, to handle retries and timeouts
+ info.incrementAttemptCount();
+
+ // now we actually launch the app.
+ device.executeShellCommand(command, new AMReceiver(info, device, mLaunchController));
+
+ // if the app is not a debug app, we need to do some clean up, as
+ // the process is done!
+ if (info.isDebugMode() == false) {
+ // stop the launch object, since there's no debug, and it can't
+ // provide any control over the app
+ return false;
+ }
+ } catch (TimeoutException e) {
+ AdtPlugin.printErrorToConsole(info.getProject(), "Launch error: timeout");
+ return false;
+ } catch (AdbCommandRejectedException e) {
+ AdtPlugin.printErrorToConsole(info.getProject(), String.format(
+ "Launch error: adb rejected command: %1$s", e.getMessage()));
+ return false;
+ } catch (ShellCommandUnresponsiveException e) {
+ // we didn't get the output but that's ok, just log it
+ AdtPlugin.log(e, "No command output when running: '%1$s' on device %2$s", command,
+ device);
+ } catch (IOException e) {
+ // something went wrong trying to launch the app.
+ // lets stop the Launch
+ AdtPlugin.printErrorToConsole(info.getProject(),
+ String.format("Launch error: %s", e.getMessage()));
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Launches the activity on targeted device
+ *
+ * @param info the {@link DelayedLaunchInfo} that contains launch details
+ * @param devices list of Android devices on which the activity will be launched
+ */
+ @Override
+ public boolean doLaunchAction(DelayedLaunchInfo info, Collection<IDevice> devices) {
+ boolean result = true;
+ for (IDevice d : devices) {
+ // Note that this expression should not short circuit - even if an action fails
+ // on a device, it should still be performed on all other devices.
+ result = doLaunchAction(info, d) && result;
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns a description of the activity being launched
+ *
+ * @see IAndroidLaunchAction#getLaunchDescription()
+ */
+ @Override
+ public String getLaunchDescription() {
+ return String.format("%1$s activity launch", mActivity);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunch.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunch.java
new file mode 100644
index 000000000..8e2c133be
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunch.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import org.eclipse.debug.core.DebugException;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.core.Launch;
+import org.eclipse.debug.core.model.ISourceLocator;
+
+/**
+ * Custom implementation of Launch to allow access to the LaunchManager
+ *
+ */
+public class AndroidLaunch extends Launch {
+
+ /**
+ * Basic constructor does nothing special
+ * @param launchConfiguration
+ * @param mode
+ * @param locator
+ */
+ public AndroidLaunch(ILaunchConfiguration launchConfiguration, String mode,
+ ISourceLocator locator) {
+ super(launchConfiguration, mode, locator);
+ }
+
+ /** Stops the launch, and removes it from the launch manager */
+ public void stopLaunch() {
+ ILaunchManager mgr = getLaunchManager();
+
+ if (canTerminate()) {
+ try {
+ terminate();
+ } catch (DebugException e) {
+ // well looks like we couldn't stop it. nothing else to be
+ // done really
+ }
+ }
+ // remove the launch
+ mgr.removeLaunch(this);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchConfiguration.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchConfiguration.java
new file mode 100644
index 000000000..580bbbee5
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchConfiguration.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.debug.core.ILaunchConfiguration;
+
+/**
+ * Launch configuration data. This stores the result of querying the
+ * {@link ILaunchConfiguration} so that it's only done once.
+ */
+public class AndroidLaunchConfiguration {
+
+ /**
+ * Launch action. See {@link LaunchConfigDelegate#ACTION_DEFAULT},
+ * {@link LaunchConfigDelegate#ACTION_ACTIVITY},
+ * {@link LaunchConfigDelegate#ACTION_DO_NOTHING}
+ */
+ public int mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;
+
+ /** Target selection mode for the configuration. */
+ public enum TargetMode {
+ /** Automatic target selection mode. */
+ AUTO,
+ /** Manual target selection mode. */
+ MANUAL,
+ /** All active devices */
+ ALL_DEVICES,
+ /** All active emulators */
+ ALL_EMULATORS,
+ /** All active devices and emulators */
+ ALL_DEVICES_AND_EMULATORS;
+
+ public static TargetMode getMode(String s) {
+ for (TargetMode m: values()) {
+ if (m.toString().equals(s)) {
+ return m;
+ }
+ }
+
+ throw new IllegalArgumentException(String.format(
+ "Invalid representation (%s) for TargetMode", s));
+ }
+
+ public boolean isMultiDevice() {
+ return this == ALL_DEVICES
+ || this == ALL_EMULATORS
+ || this == ALL_DEVICES_AND_EMULATORS;
+ }
+ }
+
+ /**
+ * Target selection mode.
+ * @see TargetMode
+ */
+ public TargetMode mTargetMode = LaunchConfigDelegate.DEFAULT_TARGET_MODE;
+
+ /**
+ * Indicates whether the emulator should be called with -wipe-data
+ */
+ public boolean mWipeData = LaunchConfigDelegate.DEFAULT_WIPE_DATA;
+
+ /**
+ * Indicates whether the emulator should be called with -no-boot-anim
+ */
+ public boolean mNoBootAnim = LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM;
+
+ /**
+ * AVD Name.
+ */
+ public String mAvdName = null;
+
+ public String mNetworkSpeed = EmulatorConfigTab.getSpeed(
+ LaunchConfigDelegate.DEFAULT_SPEED);
+ public String mNetworkDelay = EmulatorConfigTab.getDelay(
+ LaunchConfigDelegate.DEFAULT_DELAY);
+
+ /**
+ * Optional custom command line parameter to launch the emulator
+ */
+ public String mEmulatorCommandLine;
+
+ /** Flag indicating whether the same device should be used for future launches. */
+ public boolean mReuseLastUsedDevice = false;
+
+ /** Serial number of the device used in the last launch of this config. */
+ public String mLastUsedDevice = null;
+
+ /**
+ * Initialized the structure from an ILaunchConfiguration object.
+ * @param config
+ */
+ public void set(ILaunchConfiguration config) {
+ try {
+ mLaunchAction = config.getAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION,
+ mLaunchAction);
+ } catch (CoreException e1) {
+ // nothing to be done here, we'll use the default value
+ }
+
+ mTargetMode = parseTargetMode(config, mTargetMode);
+
+ try {
+ mReuseLastUsedDevice = config.getAttribute(
+ LaunchConfigDelegate.ATTR_REUSE_LAST_USED_DEVICE, false);
+ mLastUsedDevice = config.getAttribute(
+ LaunchConfigDelegate.ATTR_LAST_USED_DEVICE, (String)null);
+ } catch (CoreException e) {
+ // nothing to be done here, we'll use the default value
+ }
+
+ try {
+ mAvdName = config.getAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, mAvdName);
+ } catch (CoreException e) {
+ // ignore
+ }
+
+ int index = LaunchConfigDelegate.DEFAULT_SPEED;
+ try {
+ index = config.getAttribute(LaunchConfigDelegate.ATTR_SPEED, index);
+ } catch (CoreException e) {
+ // nothing to be done here, we'll use the default value
+ }
+ mNetworkSpeed = EmulatorConfigTab.getSpeed(index);
+
+ index = LaunchConfigDelegate.DEFAULT_DELAY;
+ try {
+ index = config.getAttribute(LaunchConfigDelegate.ATTR_DELAY, index);
+ } catch (CoreException e) {
+ // nothing to be done here, we'll use the default value
+ }
+ mNetworkDelay = EmulatorConfigTab.getDelay(index);
+
+ try {
+ mEmulatorCommandLine = config.getAttribute(
+ LaunchConfigDelegate.ATTR_COMMANDLINE, ""); //$NON-NLS-1$
+ } catch (CoreException e) {
+ // lets not do anything here, we'll use the default value
+ }
+
+ try {
+ mWipeData = config.getAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, mWipeData);
+ } catch (CoreException e) {
+ // nothing to be done here, we'll use the default value
+ }
+
+ try {
+ mNoBootAnim = config.getAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM,
+ mNoBootAnim);
+ } catch (CoreException e) {
+ // nothing to be done here, we'll use the default value
+ }
+ }
+
+ /**
+ * Retrieve the {@link TargetMode} saved in the provided launch configuration.
+ * Returns defaultMode if there are any errors while retrieving or parsing the saved setting.
+ */
+ public static TargetMode parseTargetMode(ILaunchConfiguration config, TargetMode defaultMode) {
+ try {
+ String value = config.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
+ defaultMode.toString());
+ return TargetMode.getMode(value);
+ } catch (CoreException e) {
+ // ADT R20 changes the attribute type of ATTR_TARGET_MODE to be a string from a bool.
+ // So if parsing as a string fails, attempt parsing as a boolean.
+ try {
+ boolean value = config.getAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE, true);
+ return value ? TargetMode.AUTO : TargetMode.MANUAL;
+ } catch (CoreException e1) {
+ return defaultMode;
+ }
+ } catch (IllegalArgumentException e) {
+ return defaultMode;
+ }
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java
new file mode 100644
index 000000000..a95ed6882
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AndroidLaunchController.java
@@ -0,0 +1,1872 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDebugBridgeChangeListener;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.CanceledException;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.ClientData.DebuggerStatus;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.InstallException;
+import com.android.ddmlib.Log;
+import com.android.ddmlib.TimeoutException;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.actions.AvdManagerAction;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration.TargetMode;
+import com.android.ide.eclipse.adt.internal.launch.DelayedLaunchInfo.InstallRetryMode;
+import com.android.ide.eclipse.adt.internal.launch.DeviceChooserDialog.DeviceChooserResponse;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.ApkInstallManager;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.avd.AvdManager;
+import com.android.utils.NullLogger;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.debug.core.DebugPlugin;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationType;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.core.model.IDebugTarget;
+import org.eclipse.debug.ui.DebugUITools;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+import org.eclipse.jdt.launching.IVMConnector;
+import org.eclipse.jdt.launching.JavaRuntime;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Controls the launch of Android application either on a device or on the
+ * emulator. If an emulator is already running, this class will attempt to reuse
+ * it.
+ */
+public final class AndroidLaunchController implements IDebugBridgeChangeListener,
+ IDeviceChangeListener, IClientChangeListener, ILaunchController {
+
+ private static final String FLAG_AVD = "-avd"; //$NON-NLS-1$
+ private static final String FLAG_NETDELAY = "-netdelay"; //$NON-NLS-1$
+ private static final String FLAG_NETSPEED = "-netspeed"; //$NON-NLS-1$
+ private static final String FLAG_WIPE_DATA = "-wipe-data"; //$NON-NLS-1$
+ private static final String FLAG_NO_BOOT_ANIM = "-no-boot-anim"; //$NON-NLS-1$
+
+ /**
+ * Map to store {@link ILaunchConfiguration} objects that must be launched as simple connection
+ * to running application. The integer is the port on which to connect.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private static final HashMap<ILaunchConfiguration, Integer> sRunningAppMap =
+ new HashMap<ILaunchConfiguration, Integer>();
+
+ private static final Object sListLock = sRunningAppMap;
+
+ /**
+ * List of {@link DelayedLaunchInfo} waiting for an emulator to connect.
+ * <p>Once an emulator has connected, {@link DelayedLaunchInfo#getDevice()} is set and the
+ * DelayedLaunchInfo object is moved to
+ * {@link AndroidLaunchController#mWaitingForReadyEmulatorList}.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private final ArrayList<DelayedLaunchInfo> mWaitingForEmulatorLaunches =
+ new ArrayList<DelayedLaunchInfo>();
+
+ /**
+ * List of application waiting to be launched on a device/emulator.<br>
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ * */
+ private final ArrayList<DelayedLaunchInfo> mWaitingForReadyEmulatorList =
+ new ArrayList<DelayedLaunchInfo>();
+
+ /**
+ * Application waiting to show up as waiting for debugger.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private final ArrayList<DelayedLaunchInfo> mWaitingForDebuggerApplications =
+ new ArrayList<DelayedLaunchInfo>();
+
+ /**
+ * List of clients that have appeared as waiting for debugger before their name was available.
+ * <b>ALL ACCESS MUST BE INSIDE A <code>synchronized (sListLock)</code> block!</b>
+ */
+ private final ArrayList<Client> mUnknownClientsWaitingForDebugger = new ArrayList<Client>();
+
+ /** static instance for singleton */
+ private static AndroidLaunchController sThis = new AndroidLaunchController();
+
+ /** private constructor to enforce singleton */
+ private AndroidLaunchController() {
+ AndroidDebugBridge.addDebugBridgeChangeListener(this);
+ AndroidDebugBridge.addDeviceChangeListener(this);
+ AndroidDebugBridge.addClientChangeListener(this);
+ }
+
+ /**
+ * Returns the singleton reference.
+ */
+ public static AndroidLaunchController getInstance() {
+ return sThis;
+ }
+
+
+ /**
+ * Launches a remote java debugging session on an already running application
+ * @param project The project of the application to debug.
+ * @param debugPort The port to connect the debugger to.
+ */
+ public static void debugRunningApp(IProject project, int debugPort) {
+ // get an existing or new launch configuration
+ ILaunchConfiguration config = AndroidLaunchController.getLaunchConfig(project,
+ LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID);
+
+ if (config != null) {
+ setPortLaunchConfigAssociation(config, debugPort);
+
+ // and launch
+ DebugUITools.launch(config, ILaunchManager.DEBUG_MODE);
+ }
+ }
+
+ /**
+ * Returns an {@link ILaunchConfiguration} for the specified {@link IProject}.
+ * @param project the project
+ * @param launchTypeId launch delegate type id
+ * @return a new or already existing <code>ILaunchConfiguration</code> or null if there was
+ * an error when creating a new one.
+ */
+ public static ILaunchConfiguration getLaunchConfig(IProject project, String launchTypeId) {
+ // get the launch manager
+ ILaunchManager manager = DebugPlugin.getDefault().getLaunchManager();
+
+ // now get the config type for our particular android type.
+ ILaunchConfigurationType configType = manager.getLaunchConfigurationType(launchTypeId);
+
+ String name = project.getName();
+
+ // search for an existing launch configuration
+ ILaunchConfiguration config = findConfig(manager, configType, name);
+
+ // test if we found one or not
+ if (config == null) {
+ // Didn't find a matching config, so we make one.
+ // It'll be made in the "working copy" object first.
+ ILaunchConfigurationWorkingCopy wc = null;
+
+ try {
+ // make the working copy object
+ wc = configType.newInstance(null,
+ manager.generateLaunchConfigurationName(name));
+
+ // set the project name
+ wc.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name);
+
+ // set the launch mode to default.
+ wc.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION,
+ LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION);
+
+ // set default target mode
+ wc.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
+ LaunchConfigDelegate.DEFAULT_TARGET_MODE.toString());
+
+ // default AVD: None
+ wc.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String) null);
+
+ // set the default network speed
+ wc.setAttribute(LaunchConfigDelegate.ATTR_SPEED,
+ LaunchConfigDelegate.DEFAULT_SPEED);
+
+ // and delay
+ wc.setAttribute(LaunchConfigDelegate.ATTR_DELAY,
+ LaunchConfigDelegate.DEFAULT_DELAY);
+
+ // default wipe data mode
+ wc.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA,
+ LaunchConfigDelegate.DEFAULT_WIPE_DATA);
+
+ // default disable boot animation option
+ wc.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM,
+ LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM);
+
+ // set default emulator options
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+ String emuOptions = store.getString(AdtPrefs.PREFS_EMU_OPTIONS);
+ wc.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE, emuOptions);
+
+ // map the config and the project
+ wc.setMappedResources(getResourcesToMap(project));
+
+ // save the working copy to get the launch config object which we return.
+ return wc.doSave();
+
+ } catch (CoreException e) {
+ String msg = String.format(
+ "Failed to create a Launch config for project '%1$s': %2$s",
+ project.getName(), e.getMessage());
+ AdtPlugin.printErrorToConsole(project, msg);
+
+ // no launch!
+ return null;
+ }
+ }
+
+ return config;
+ }
+
+ /**
+ * Returns the list of resources to map to a Launch Configuration.
+ * @param project the project associated to the launch configuration.
+ */
+ public static IResource[] getResourcesToMap(IProject project) {
+ ArrayList<IResource> array = new ArrayList<IResource>(2);
+ array.add(project);
+
+ IFile manifest = ProjectHelper.getManifest(project);
+ if (manifest != null) {
+ array.add(manifest);
+ }
+
+ return array.toArray(new IResource[array.size()]);
+ }
+
+ /**
+ * Launches an android app on the device or emulator
+ *
+ * @param project The project we're launching
+ * @param mode the mode in which to launch, one of the mode constants
+ * defined by <code>ILaunchManager</code> - <code>RUN_MODE</code> or
+ * <code>DEBUG_MODE</code>.
+ * @param apk the resource to the apk to launch.
+ * @param packageName the Android package name of the app
+ * @param debugPackageName the Android package name to debug
+ * @param debuggable the debuggable value of the app's manifest, or null if not set.
+ * @param requiredApiVersionNumber the api version required by the app, or null if none.
+ * @param launchAction the action to perform after app sync
+ * @param config the launch configuration
+ * @param launch the launch object
+ */
+ public void 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) {
+
+ String message = String.format("Performing %1$s", launchAction.getLaunchDescription());
+ AdtPlugin.printToConsole(project, message);
+
+ // create the launch info
+ final DelayedLaunchInfo launchInfo = new DelayedLaunchInfo(project, packageName,
+ debugPackageName, launchAction, apk, debuggable, requiredApiVersionNumber, launch,
+ monitor);
+
+ // set the debug mode
+ launchInfo.setDebugMode(mode.equals(ILaunchManager.DEBUG_MODE));
+
+ // get the SDK
+ Sdk currentSdk = Sdk.getCurrent();
+ AvdManager avdManager = currentSdk.getAvdManager();
+
+ // reload the AVDs to make sure we are up to date
+ try {
+ avdManager.reloadAvds(NullLogger.getLogger());
+ } catch (AndroidLocationException e1) {
+ // this happens if the AVD Manager failed to find the folder in which the AVDs are
+ // stored. This is unlikely to happen, but if it does, we should force to go manual
+ // to allow using physical devices.
+ config.mTargetMode = TargetMode.MANUAL;
+ }
+
+ // get the sdk against which the project is built
+ IAndroidTarget projectTarget = currentSdk.getTarget(project);
+
+ // get the min required android version
+ ManifestInfo mi = ManifestInfo.get(project);
+ final int minApiLevel = mi.getMinSdkVersion();
+ final String minApiCodeName = mi.getMinSdkCodeName();
+ final AndroidVersion minApiVersion = new AndroidVersion(minApiLevel, minApiCodeName);
+
+ // FIXME: check errors on missing sdk, AVD manager, or project target.
+
+ // device chooser response object.
+ final DeviceChooserResponse response = new DeviceChooserResponse();
+
+ /*
+ * Launch logic:
+ * - Use Last Launched Device/AVD set.
+ * If user requested to use same device for future launches, and the last launched
+ * device/avd is still present, then simply launch on the same device/avd.
+ * - Manual Mode
+ * Always display a UI that lets a user see the current running emulators/devices.
+ * The UI must show which devices are compatibles, and allow launching new emulators
+ * with compatible (and not yet running) AVD.
+ * - Automatic Way
+ * * Preferred AVD set.
+ * If Preferred AVD is not running: launch it.
+ * Launch the application on the preferred AVD.
+ * * No preferred AVD.
+ * Count the number of compatible emulators/devices.
+ * If != 1, display a UI similar to manual mode.
+ * If == 1, launch the application on this AVD/device.
+ * - Launch on multiple devices:
+ * From the currently active devices & emulators, filter out those that cannot run
+ * the app (by api level), and launch on all the others.
+ */
+ IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
+ if (config.mReuseLastUsedDevice) {
+ // check to see if the last used device is still online
+ IDevice lastUsedDevice = getDeviceIfOnline(config.mLastUsedDevice,
+ devices);
+ if (lastUsedDevice != null) {
+ response.setDeviceToUse(lastUsedDevice);
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+ }
+
+ if (config.mTargetMode == TargetMode.AUTO) {
+ // first check if we have a preferred AVD name, and if it actually exists, and is valid
+ // (ie able to run the project).
+ // We need to check this in case the AVD was recreated with a different target that is
+ // not compatible.
+ AvdInfo preferredAvd = null;
+ if (config.mAvdName != null) {
+ preferredAvd = avdManager.getAvd(config.mAvdName, true /*validAvdOnly*/);
+ }
+
+ if (preferredAvd != null) {
+ IAndroidTarget preferredAvdTarget = preferredAvd.getTarget();
+ if (preferredAvdTarget != null
+ && !preferredAvdTarget.getVersion().canRun(minApiVersion)) {
+ preferredAvd = null;
+
+ AdtPlugin.printErrorToConsole(project, String.format(
+ "Preferred AVD '%1$s' (API Level: %2$d) cannot run application with minApi %3$s. Looking for a compatible AVD...",
+ config.mAvdName,
+ preferredAvdTarget.getVersion().getApiLevel(),
+ minApiVersion));
+ }
+ }
+
+ if (preferredAvd != null) {
+ // We have a preferred avd that can actually run the application.
+ // Now see if the AVD is running, and if so use it, otherwise launch it.
+
+ for (IDevice d : devices) {
+ String deviceAvd = d.getAvdName();
+ if (deviceAvd != null && deviceAvd.equals(config.mAvdName)) {
+ response.setDeviceToUse(d);
+
+ AdtPlugin.printToConsole(project, String.format(
+ "Automatic Target Mode: Preferred AVD '%1$s' is available on emulator '%2$s'",
+ config.mAvdName, d));
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+ }
+
+ // at this point we have a valid preferred AVD that is not running.
+ // We need to start it.
+ response.setAvdToLaunch(preferredAvd);
+
+ AdtPlugin.printToConsole(project, String.format(
+ "Automatic Target Mode: Preferred AVD '%1$s' is not available. Launching new emulator.",
+ config.mAvdName));
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+
+ // no (valid) preferred AVD? look for one.
+
+ // If the API level requested in the manifest is lower than the current project
+ // target, when we will iterate devices/avds later ideally we will want to find
+ // a device/avd which target is as close to the manifest as possible (instead of
+ // a device which target is the same as the project's target) and use it as the
+ // new default.
+
+ if (minApiCodeName != null && minApiLevel < projectTarget.getVersion().getApiLevel()) {
+ int maxDist = projectTarget.getVersion().getApiLevel() - minApiLevel;
+ IAndroidTarget candidate = null;
+
+ for (IAndroidTarget target : currentSdk.getTargets()) {
+ if (target.canRunOn(projectTarget)) {
+ int currDist = target.getVersion().getApiLevel() - minApiLevel;
+ if (currDist >= 0 && currDist < maxDist) {
+ maxDist = currDist;
+ candidate = target;
+ if (maxDist == 0) {
+ // Found a perfect match
+ break;
+ }
+ }
+ }
+ }
+
+ if (candidate != null) {
+ // We found a better SDK target candidate, that is closer to the
+ // API level from minSdkVersion than the one currently used by the
+ // project. Below (in the for...devices loop) we'll try to find
+ // a device/AVD for it.
+ projectTarget = candidate;
+ }
+ }
+
+ HashMap<IDevice, AvdInfo> compatibleRunningAvds = new HashMap<IDevice, AvdInfo>();
+ boolean hasDevice = false; // if there's 1+ device running, we may force manual mode,
+ // as we cannot always detect proper compatibility with
+ // devices. This is the case if the project target is not
+ // a standard platform
+ for (IDevice d : devices) {
+ String deviceAvd = d.getAvdName();
+ if (deviceAvd != null) { // physical devices return null.
+ AvdInfo info = avdManager.getAvd(deviceAvd, true /*validAvdOnly*/);
+ if (AvdCompatibility.canRun(info, projectTarget, minApiVersion)
+ == AvdCompatibility.Compatibility.YES) {
+ compatibleRunningAvds.put(d, info);
+ }
+ } else {
+ if (projectTarget.isPlatform()) { // means this can run on any device as long
+ // as api level is high enough
+ AndroidVersion deviceVersion = Sdk.getDeviceVersion(d);
+ // the deviceVersion may be null if it wasn't yet queried (device just
+ // plugged in or emulator just booting up.
+ if (deviceVersion != null &&
+ deviceVersion.canRun(projectTarget.getVersion())) {
+ // device is compatible with project
+ compatibleRunningAvds.put(d, null);
+ continue;
+ }
+ } else {
+ // for non project platform, we can't be sure if a device can
+ // run an application or not, since we don't query the device
+ // for the list of optional libraries that it supports.
+ }
+ hasDevice = true;
+ }
+ }
+
+ // depending on the number of devices, we'll simulate an automatic choice
+ // from the device chooser or simply show up the device chooser.
+ if (hasDevice == false && compatibleRunningAvds.size() == 0) {
+ // if zero emulators/devices, we launch an emulator.
+ // We need to figure out which AVD first.
+
+ // we are going to take the closest AVD. ie a compatible AVD that has the API level
+ // closest to the project target.
+ AvdInfo defaultAvd = findMatchingAvd(avdManager, projectTarget, minApiVersion);
+
+ if (defaultAvd != null) {
+ response.setAvdToLaunch(defaultAvd);
+
+ AdtPlugin.printToConsole(project, String.format(
+ "Automatic Target Mode: launching new emulator with compatible AVD '%1$s'",
+ defaultAvd.getName()));
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ } else {
+ AdtPlugin.printToConsole(project, String.format(
+ "Failed to find an AVD compatible with target '%1$s'.",
+ projectTarget.getName()));
+
+ final Display display = AdtPlugin.getDisplay();
+ final boolean[] searchAgain = new boolean[] { false };
+ // ask the user to create a new one.
+ display.syncExec(new Runnable() {
+ @Override
+ public void run() {
+ Shell shell = display.getActiveShell();
+ if (MessageDialog.openQuestion(shell, "Android AVD Error",
+ "No compatible targets were found. Do you wish to add a new Android Virtual Device?")) {
+ AvdManagerAction action = new AvdManagerAction();
+ action.run(null /*action*/);
+ searchAgain[0] = true;
+ }
+ }
+ });
+ if (searchAgain[0]) {
+ // attempt to reload the AVDs and find one compatible.
+ defaultAvd = findMatchingAvd(avdManager, projectTarget, minApiVersion);
+
+ if (defaultAvd == null) {
+ AdtPlugin.printErrorToConsole(project, String.format(
+ "Still no compatible AVDs with target '%1$s': Aborting launch.",
+ projectTarget.getName()));
+ stopLaunch(launchInfo);
+ } else {
+ response.setAvdToLaunch(defaultAvd);
+
+ AdtPlugin.printToConsole(project, String.format(
+ "Launching new emulator with compatible AVD '%1$s'",
+ defaultAvd.getName()));
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+ }
+ }
+ } else if (hasDevice == false && compatibleRunningAvds.size() == 1) {
+ Entry<IDevice, AvdInfo> e = compatibleRunningAvds.entrySet().iterator().next();
+ response.setDeviceToUse(e.getKey());
+
+ // get the AvdInfo, if null, the device is a physical device.
+ AvdInfo avdInfo = e.getValue();
+ if (avdInfo != null) {
+ message = String.format("Automatic Target Mode: using existing emulator '%1$s' running compatible AVD '%2$s'",
+ response.getDeviceToUse(), e.getValue().getName());
+ } else {
+ message = String.format("Automatic Target Mode: using device '%1$s'",
+ response.getDeviceToUse());
+ }
+ AdtPlugin.printToConsole(project, message);
+
+ continueLaunch(response, project, launch, launchInfo, config);
+ return;
+ }
+
+ // if more than one device, we'll bring up the DeviceChooser dialog below.
+ if (compatibleRunningAvds.size() >= 2) {
+ message = "Automatic Target Mode: Several compatible targets. Please select a target device.";
+ } else if (hasDevice) {
+ message = "Automatic Target Mode: Unable to detect device compatibility. Please select a target device.";
+ }
+
+ AdtPlugin.printToConsole(project, message);
+ } else if ((config.mTargetMode == TargetMode.ALL_DEVICES_AND_EMULATORS
+ || config.mTargetMode == TargetMode.ALL_DEVICES
+ || config.mTargetMode == TargetMode.ALL_EMULATORS)
+ && ILaunchManager.RUN_MODE.equals(mode)) {
+ // if running on multiple devices, identify all compatible devices
+ boolean includeDevices = config.mTargetMode != TargetMode.ALL_EMULATORS;
+ boolean includeAvds = config.mTargetMode != TargetMode.ALL_DEVICES;
+ Collection<IDevice> compatibleDevices = findCompatibleDevices(devices,
+ minApiVersion, includeDevices, includeAvds);
+ if (compatibleDevices.size() == 0) {
+ AdtPlugin.printErrorToConsole(project,
+ "No active compatible AVD's or devices found. "
+ + "Relaunch this configuration after connecting a device or starting an AVD.");
+ stopLaunch(launchInfo);
+ } else {
+ multiLaunch(launchInfo, compatibleDevices);
+ }
+ return;
+ }
+
+ // bring up the device chooser.
+ final IAndroidTarget desiredProjectTarget = projectTarget;
+ final AtomicBoolean continueLaunch = new AtomicBoolean(false);
+ AdtPlugin.getDisplay().syncExec(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // open the chooser dialog. It'll fill 'response' with the device to use
+ // or the AVD to launch.
+ DeviceChooserDialog dialog = new DeviceChooserDialog(
+ AdtPlugin.getShell(),
+ response, launchInfo.getPackageName(),
+ desiredProjectTarget, minApiVersion,
+ config.mReuseLastUsedDevice);
+ if (dialog.open() == Dialog.OK) {
+ updateLaunchConfigWithLastUsedDevice(launch.getLaunchConfiguration(),
+ response);
+ continueLaunch.set(true);
+ } else {
+ AdtPlugin.printErrorToConsole(project, "Launch canceled!");
+ stopLaunch(launchInfo);
+ return;
+ }
+ } catch (Exception e) {
+ // there seems to be some case where the shell will be null. (might be
+ // an OS X bug). Because of this the creation of the dialog will throw
+ // and IllegalArg exception interrupting the launch with no user feedback.
+ // So we trap all the exception and display something.
+ String msg = e.getMessage();
+ if (msg == null) {
+ msg = e.getClass().getCanonicalName();
+ }
+ AdtPlugin.printErrorToConsole(project,
+ String.format("Error during launch: %s", msg));
+ stopLaunch(launchInfo);
+ }
+ }
+ });
+
+ if (continueLaunch.get()) {
+ continueLaunch(response, project, launch, launchInfo, config);
+ }
+ }
+
+ /**
+ * Returns devices that can run a app of provided API level.
+ * @param devices list of devices to filter from
+ * @param requiredVersion minimum required API that should be supported
+ * @param includeDevices include physical devices in the filtered list
+ * @param includeAvds include emulators in the filtered list
+ * @return set of compatible devices, may be an empty set
+ */
+ private Collection<IDevice> findCompatibleDevices(IDevice[] devices,
+ AndroidVersion requiredVersion, boolean includeDevices, boolean includeAvds) {
+ Set<IDevice> compatibleDevices = new HashSet<IDevice>(devices.length);
+ AvdManager avdManager = Sdk.getCurrent().getAvdManager();
+ for (IDevice d: devices) {
+ boolean isEmulator = d.isEmulator();
+ boolean canRun = false;
+
+ if (isEmulator) {
+ if (!includeAvds) {
+ continue;
+ }
+
+ AvdInfo avdInfo = avdManager.getAvd(d.getAvdName(), true);
+ if (avdInfo != null && avdInfo.getTarget() != null) {
+ canRun = avdInfo.getTarget().getVersion().canRun(requiredVersion);
+ }
+ } else {
+ if (!includeDevices) {
+ continue;
+ }
+
+ AndroidVersion deviceVersion = Sdk.getDeviceVersion(d);
+ if (deviceVersion != null) {
+ canRun = deviceVersion.canRun(requiredVersion);
+ }
+ }
+
+ if (canRun) {
+ compatibleDevices.add(d);
+ }
+ }
+
+ return compatibleDevices;
+ }
+
+ /**
+ * Find a matching AVD.
+ * @param minApiVersion
+ */
+ private AvdInfo findMatchingAvd(AvdManager avdManager, final IAndroidTarget projectTarget,
+ AndroidVersion minApiVersion) {
+ AvdInfo[] avds = avdManager.getValidAvds();
+ AvdInfo bestAvd = null;
+ for (AvdInfo avd : avds) {
+ if (AvdCompatibility.canRun(avd, projectTarget, minApiVersion)
+ == AvdCompatibility.Compatibility.YES) {
+ // at this point we can ignore the code name issue since
+ // AvdCompatibility.canRun() will already have filtered out the non compatible AVDs.
+ if (bestAvd == null ||
+ avd.getTarget().getVersion().getApiLevel() <
+ bestAvd.getTarget().getVersion().getApiLevel()) {
+ bestAvd = avd;
+ }
+ }
+ }
+ return bestAvd;
+ }
+
+ /**
+ * Continues the launch based on the DeviceChooser response.
+ * @param response the device chooser response
+ * @param project The project being launched
+ * @param launch The eclipse launch info
+ * @param launchInfo The {@link DelayedLaunchInfo}
+ * @param config The config needed to start a new emulator.
+ */
+ private void continueLaunch(final DeviceChooserResponse response, final IProject project,
+ final AndroidLaunch launch, final DelayedLaunchInfo launchInfo,
+ final AndroidLaunchConfiguration config) {
+ if (response.getAvdToLaunch() != null) {
+ // there was no selected device, we start a new emulator.
+ synchronized (sListLock) {
+ AvdInfo info = response.getAvdToLaunch();
+ mWaitingForEmulatorLaunches.add(launchInfo);
+ AdtPlugin.printToConsole(project, String.format(
+ "Launching a new emulator with Virtual Device '%1$s'",
+ info.getName()));
+ boolean status = launchEmulator(config, info);
+
+ if (status == false) {
+ // launching the emulator failed!
+ AdtPlugin.displayError("Emulator Launch",
+ "Couldn't launch the emulator! Make sure the SDK directory is properly setup and the emulator is not missing.");
+
+ // stop the launch and return
+ mWaitingForEmulatorLaunches.remove(launchInfo);
+ AdtPlugin.printErrorToConsole(project, "Launch canceled!");
+ stopLaunch(launchInfo);
+ return;
+ }
+
+ return;
+ }
+ } else if (response.getDeviceToUse() != null) {
+ launchInfo.setDevice(response.getDeviceToUse());
+ simpleLaunch(launchInfo, launchInfo.getDevice());
+ }
+ }
+
+ /**
+ * Queries for a debugger port for a specific {@link ILaunchConfiguration}.
+ * <p/>
+ * If the configuration and a debugger port where added through
+ * {@link #setPortLaunchConfigAssociation(ILaunchConfiguration, int)}, then this method
+ * will return the debugger port, and remove the configuration from the list.
+ * @param launchConfig the {@link ILaunchConfiguration}
+ * @return the debugger port or {@link LaunchConfigDelegate#INVALID_DEBUG_PORT} if the
+ * configuration was not setup.
+ */
+ static int getPortForConfig(ILaunchConfiguration launchConfig) {
+ synchronized (sListLock) {
+ Integer port = sRunningAppMap.get(launchConfig);
+ if (port != null) {
+ sRunningAppMap.remove(launchConfig);
+ return port;
+ }
+ }
+
+ return LaunchConfigDelegate.INVALID_DEBUG_PORT;
+ }
+
+ /**
+ * Set a {@link ILaunchConfiguration} and its associated debug port, in the list of
+ * launch config to connect directly to a running app instead of doing full launch (sync,
+ * launch, and connect to).
+ * @param launchConfig the {@link ILaunchConfiguration} object.
+ * @param port The debugger port to connect to.
+ */
+ private static void setPortLaunchConfigAssociation(ILaunchConfiguration launchConfig,
+ int port) {
+ synchronized (sListLock) {
+ sRunningAppMap.put(launchConfig, port);
+ }
+ }
+
+ /**
+ * Checks the build information, and returns whether the launch should continue.
+ * <p/>The value tested are:
+ * <ul>
+ * <li>Minimum API version requested by the application. If the target device does not match,
+ * the launch is canceled.</li>
+ * <li>Debuggable attribute of the application and whether or not the device requires it. If
+ * the device requires it and it is not set in the manifest, the launch will be forced to
+ * "release" mode instead of "debug"</li>
+ * <ul>
+ */
+ private boolean checkBuildInfo(DelayedLaunchInfo launchInfo, IDevice device) {
+ if (device != null) {
+ // check the app required API level versus the target device API level
+
+ String deviceVersion = device.getProperty(IDevice.PROP_BUILD_VERSION);
+ String deviceApiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL);
+ String deviceCodeName = device.getProperty(IDevice.PROP_BUILD_CODENAME);
+
+ int deviceApiLevel = -1;
+ try {
+ deviceApiLevel = Integer.parseInt(deviceApiLevelString);
+ } catch (NumberFormatException e) {
+ // pass, we'll keep the apiLevel value at -1.
+ }
+
+ String requiredApiString = launchInfo.getRequiredApiVersionNumber();
+ if (requiredApiString != null) {
+ int requiredApi = -1;
+ try {
+ requiredApi = Integer.parseInt(requiredApiString);
+ } catch (NumberFormatException e) {
+ // pass, we'll keep requiredApi value at -1.
+ }
+
+ if (requiredApi == -1) {
+ // this means the manifest uses a codename for minSdkVersion
+ // check that the device is using the same codename
+ if (requiredApiString.equals(deviceCodeName) == false) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format(
+ "ERROR: Application requires a device running '%1$s'!",
+ requiredApiString));
+ return false;
+ }
+ } else {
+ // app requires a specific API level
+ if (deviceApiLevel == -1) {
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ "WARNING: Unknown device API version!");
+ } else if (deviceApiLevel < requiredApi) {
+ String msg = String.format(
+ "ERROR: Application requires API version %1$d. Device API version is %2$d (Android %3$s).",
+ requiredApi, deviceApiLevel, deviceVersion);
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);
+
+ // abort the launch
+ return false;
+ }
+ }
+ } else {
+ // warn the application API level requirement is not set.
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "WARNING: Application does not specify an API level requirement!");
+
+ // and display the target device API level (if known)
+ if (deviceApiLevel == -1) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "WARNING: Unknown device API version!");
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), String.format(
+ "Device API version is %1$d (Android %2$s)", deviceApiLevel,
+ deviceVersion));
+ }
+ }
+
+ // now checks that the device/app can be debugged (if needed)
+ if (device.isEmulator() == false && launchInfo.isDebugMode()) {
+ String debuggableDevice = device.getProperty(IDevice.PROP_DEBUGGABLE);
+ if (debuggableDevice != null && debuggableDevice.equals("0")) { //$NON-NLS-1$
+ // the device is "secure" and requires apps to declare themselves as debuggable!
+ // launchInfo.getDebuggable() will return null if the manifest doesn't declare
+ // anything. In this case this is fine since the build system does insert
+ // debuggable=true. The only case to look for is if false is manually set
+ // in the manifest.
+ if (launchInfo.getDebuggable() == Boolean.FALSE) {
+ String message = String.format("Application '%1$s' has its 'debuggable' attribute set to FALSE and cannot be debugged.",
+ launchInfo.getPackageName());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), message);
+
+ // because am -D does not check for ro.debuggable and the
+ // 'debuggable' attribute, it is important we do not use the -D option
+ // in this case or the app will wait for a debugger forever and never
+ // really launch.
+ launchInfo.setDebugMode(false);
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Do a simple launch on the specified device, attempting to sync the new
+ * package, and then launching the application. Failed sync/launch will
+ * stop the current AndroidLaunch and return false;
+ * @param launchInfo
+ * @param device
+ * @return true if succeed
+ */
+ private boolean simpleLaunch(DelayedLaunchInfo launchInfo, IDevice device) {
+ if (!doPreLaunchActions(launchInfo, device)) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), "Launch canceled!");
+ stopLaunch(launchInfo);
+ return false;
+ }
+
+ // launch the app
+ launchApp(launchInfo, device);
+
+ return true;
+ }
+
+ private boolean doPreLaunchActions(DelayedLaunchInfo launchInfo, IDevice device) {
+ // API level check
+ if (!checkBuildInfo(launchInfo, device)) {
+ return false;
+ }
+
+ // sync app
+ if (!syncApp(launchInfo, device)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void multiLaunch(DelayedLaunchInfo launchInfo, Collection<IDevice> devices) {
+ for (IDevice d: devices) {
+ boolean success = doPreLaunchActions(launchInfo, d);
+ if (!success) {
+ String deviceName = d.isEmulator() ? d.getAvdName() : d.getSerialNumber();
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Launch failed on device: " + deviceName);
+ continue;
+ }
+ }
+
+ doLaunchAction(launchInfo, devices);
+
+ // multiple launches are only supported for run configuration, so we can terminate
+ // the launch itself
+ stopLaunch(launchInfo);
+ }
+
+ /**
+ * If needed, syncs the application and all its dependencies on the device/emulator.
+ *
+ * @param launchInfo The Launch information object.
+ * @param device the device on which to sync the application
+ * @return true if the install succeeded.
+ */
+ private boolean syncApp(DelayedLaunchInfo launchInfo, IDevice device) {
+ boolean alreadyInstalled = ApkInstallManager.getInstance().isApplicationInstalled(
+ launchInfo.getProject(), launchInfo.getPackageName(), device);
+
+ if (alreadyInstalled) {
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ "Application already deployed. No need to reinstall.");
+ } else {
+ if (doSyncApp(launchInfo, device) == false) {
+ return false;
+ }
+ }
+
+ // The app is now installed, now try the dependent projects
+ for (DelayedLaunchInfo dependentLaunchInfo : getDependenciesLaunchInfo(launchInfo)) {
+ String msg = String.format("Project dependency found, installing: %s",
+ dependentLaunchInfo.getProject().getName());
+ AdtPlugin.printToConsole(launchInfo.getProject(), msg);
+ if (syncApp(dependentLaunchInfo, device) == false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Syncs the application on the device/emulator.
+ *
+ * @param launchInfo The Launch information object.
+ * @param device the device on which to sync the application
+ * @return true if the install succeeded.
+ */
+ private boolean doSyncApp(DelayedLaunchInfo launchInfo, IDevice device) {
+ IPath path = launchInfo.getPackageFile().getLocation();
+ String fileName = path.lastSegment();
+ try {
+ String message = String.format("Uploading %1$s onto device '%2$s'",
+ fileName, device.getSerialNumber());
+ AdtPlugin.printToConsole(launchInfo.getProject(), message);
+
+ String remotePackagePath = device.syncPackageToDevice(path.toOSString());
+ boolean installResult = installPackage(launchInfo, remotePackagePath, device);
+ device.removeRemotePackage(remotePackagePath);
+
+ // if the installation succeeded, we register it.
+ if (installResult) {
+ ApkInstallManager.getInstance().registerInstallation(
+ launchInfo.getProject(), launchInfo.getPackageName(), device);
+ }
+ return installResult;
+ }
+ catch (IOException e) {
+ String msg = String.format("Failed to install %1$s on device '%2$s': %3$s", fileName,
+ device.getSerialNumber(), e.getMessage());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
+ } catch (TimeoutException e) {
+ String msg = String.format("Failed to install %1$s on device '%2$s': timeout", fileName,
+ device.getSerialNumber());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);
+ } catch (AdbCommandRejectedException e) {
+ String msg = String.format(
+ "Failed to install %1$s on device '%2$s': adb rejected install command with: %3$s",
+ fileName, device.getSerialNumber(), e.getMessage());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
+ } catch (CanceledException e) {
+ if (e.wasCanceled()) {
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ String.format("Install of %1$s canceled", fileName));
+ } else {
+ String msg = String.format("Failed to install %1$s on device '%2$s': %3$s",
+ fileName, device.getSerialNumber(), e.getMessage());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * For the current launchInfo, create additional DelayedLaunchInfo that should be used to
+ * sync APKs that we are dependent on to the device.
+ *
+ * @param launchInfo the original launch info that we want to find the
+ * @return a list of DelayedLaunchInfo (may be empty if no dependencies were found or error)
+ */
+ public List<DelayedLaunchInfo> getDependenciesLaunchInfo(DelayedLaunchInfo launchInfo) {
+ List<DelayedLaunchInfo> dependencies = new ArrayList<DelayedLaunchInfo>();
+
+ // Convert to equivalent JavaProject
+ IJavaProject javaProject;
+ try {
+ //assuming this is an Android (and Java) project since it is attached to the launchInfo.
+ javaProject = BaseProjectHelper.getJavaProject(launchInfo.getProject());
+ } catch (CoreException e) {
+ // return empty dependencies
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
+ return dependencies;
+ }
+
+ // Get all projects that this depends on
+ List<IJavaProject> androidProjectList;
+ try {
+ androidProjectList = ProjectHelper.getAndroidProjectDependencies(javaProject);
+ } catch (JavaModelException e) {
+ // return empty dependencies
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), e);
+ return dependencies;
+ }
+
+ // for each project, parse manifest and create launch information
+ for (IJavaProject androidProject : androidProjectList) {
+ // Parse the Manifest to get various required information
+ // copied from LaunchConfigDelegate
+ ManifestData manifestData = AndroidManifestHelper.parseForData(
+ androidProject.getProject());
+
+ if (manifestData == null) {
+ continue;
+ }
+
+ // Get the APK location (can return null)
+ IFile apk = ProjectHelper.getApplicationPackage(androidProject.getProject());
+ if (apk == null) {
+ // getApplicationPackage will have logged an error message
+ continue;
+ }
+
+ // Create new launchInfo as an hybrid between parent and dependency information
+ DelayedLaunchInfo delayedLaunchInfo = new DelayedLaunchInfo(
+ androidProject.getProject(),
+ manifestData.getPackage(),
+ manifestData.getPackage(),
+ launchInfo.getLaunchAction(),
+ apk,
+ manifestData.getDebuggable(),
+ manifestData.getMinSdkVersionString(),
+ launchInfo.getLaunch(),
+ launchInfo.getMonitor());
+
+ // Add to the list
+ dependencies.add(delayedLaunchInfo);
+ }
+
+ return dependencies;
+ }
+
+ /**
+ * Installs the application package on the device, and handles return result
+ * @param launchInfo The launch information
+ * @param remotePath The remote path of the package.
+ * @param device The device on which the launch is done.
+ */
+ private boolean installPackage(DelayedLaunchInfo launchInfo, final String remotePath,
+ final IDevice device) {
+ String message = String.format("Installing %1$s...", launchInfo.getPackageFile().getName());
+ AdtPlugin.printToConsole(launchInfo.getProject(), message);
+ try {
+ // try a reinstall first, because the most common case is the app is already installed
+ String result = doInstall(launchInfo, remotePath, device, true /* reinstall */);
+
+ /* For now we force to retry the install (after uninstalling) because there's no
+ * other way around it: adb install does not want to update a package w/o uninstalling
+ * the old one first!
+ */
+ return checkInstallResult(result, device, launchInfo, remotePath,
+ InstallRetryMode.ALWAYS);
+ } catch (Exception e) {
+ String msg = String.format(
+ "Failed to install %1$s on device '%2$s!",
+ launchInfo.getPackageFile().getName(), device.getSerialNumber());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg, e.getMessage());
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks the result of an installation, and takes optional actions based on it.
+ * @param result the result string from the installation
+ * @param device the device on which the installation occured.
+ * @param launchInfo the {@link DelayedLaunchInfo}
+ * @param remotePath the temporary path of the package on the device
+ * @param retryMode indicates what to do in case, a package already exists.
+ * @return <code>true<code> if success, <code>false</code> otherwise.
+ * @throws InstallException
+ */
+ private boolean checkInstallResult(String result, IDevice device, DelayedLaunchInfo launchInfo,
+ String remotePath, InstallRetryMode retryMode) throws InstallException {
+ if (result == null) {
+ AdtPlugin.printToConsole(launchInfo.getProject(), "Success!");
+ return true;
+ }
+ else if (result.equals("INSTALL_FAILED_ALREADY_EXISTS")) { //$NON-NLS-1$
+ // this should never happen, since reinstall mode is used on the first attempt
+ if (retryMode == InstallRetryMode.PROMPT) {
+ boolean prompt = AdtPlugin.displayPrompt("Application Install",
+ "A previous installation needs to be uninstalled before the new package can be installed.\nDo you want to uninstall?");
+ if (prompt) {
+ retryMode = InstallRetryMode.ALWAYS;
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Installation error! The package already exists.");
+ return false;
+ }
+ }
+
+ if (retryMode == InstallRetryMode.ALWAYS) {
+ /*
+ * TODO: create a UI that gives the dev the choice to:
+ * - clean uninstall on launch
+ * - full uninstall if application exists.
+ * - soft uninstall if application exists (keeps the app data around).
+ * - always ask (choice of soft-reinstall, full reinstall)
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ "Application already exists, uninstalling...");
+ String res = doUninstall(device, launchInfo);
+ if (res == null) {
+ AdtPlugin.printToConsole(launchInfo.mProject, "Success!");
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.mProject,
+ String.format("Failed to uninstall: %1$s", res));
+ return false;
+ }
+ */
+
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ "Application already exists. Attempting to re-install instead...");
+ String res = doInstall(launchInfo, remotePath, device, true /* reinstall */ );
+ return checkInstallResult(res, device, launchInfo, remotePath,
+ InstallRetryMode.NEVER);
+ }
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Installation error! The package already exists.");
+ } else if (result.equals("INSTALL_FAILED_INVALID_APK")) { //$NON-NLS-1$
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Installation failed due to invalid APK file!",
+ "Please check logcat output for more details.");
+ } else if (result.equals("INSTALL_FAILED_INVALID_URI")) { //$NON-NLS-1$
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Installation failed due to invalid URI!",
+ "Please check logcat output for more details.");
+ } else if (result.equals("INSTALL_FAILED_COULDNT_COPY")) { //$NON-NLS-1$
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format("Installation failed: Could not copy %1$s to its final location!",
+ launchInfo.getPackageFile().getName()),
+ "Please check logcat output for more details.");
+ } else if (result.equals("INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES")) { //$NON-NLS-1$
+ if (retryMode != InstallRetryMode.NEVER) {
+ boolean prompt = AdtPlugin.displayPrompt("Application Install",
+ "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?");
+ if (prompt) {
+ doUninstall(device, launchInfo);
+ String res = doInstall(launchInfo, remotePath, device, false);
+ return checkInstallResult(res, device, launchInfo, remotePath,
+ InstallRetryMode.NEVER);
+ }
+ }
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Re-installation failed due to different application signatures.",
+ "You must perform a full uninstall of the application. WARNING: This will remove the application data!",
+ String.format("Please execute 'adb uninstall %1$s' in a shell.", launchInfo.getPackageName()));
+ } else {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format("Installation error: %1$s", result),
+ "Please check logcat output for more details.");
+ }
+
+ return false;
+ }
+
+ /**
+ * Performs the uninstallation of an application.
+ * @param device the device on which to install the application.
+ * @param launchInfo the {@link DelayedLaunchInfo}.
+ * @return a {@link String} with an error code, or <code>null</code> if success.
+ * @throws InstallException if the installation failed.
+ */
+ private String doUninstall(IDevice device, DelayedLaunchInfo launchInfo)
+ throws InstallException {
+ try {
+ return device.uninstallPackage(launchInfo.getPackageName());
+ } catch (InstallException e) {
+ String msg = String.format(
+ "Failed to uninstall %1$s: %2$s", launchInfo.getPackageName(), e.getMessage());
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(), msg);
+ throw e;
+ }
+ }
+
+ /**
+ * Performs the installation of an application whose package has been uploaded on the device.
+ *
+ * @param launchInfo the {@link DelayedLaunchInfo}.
+ * @param remotePath the path of the application package in the device tmp folder.
+ * @param device the device on which to install the application.
+ * @param reinstall
+ * @return a {@link String} with an error code, or <code>null</code> if success.
+ * @throws InstallException if the uninstallation failed.
+ */
+ private String doInstall(DelayedLaunchInfo launchInfo, final String remotePath,
+ final IDevice device, boolean reinstall) throws InstallException {
+ return device.installRemotePackage(remotePath, reinstall);
+ }
+
+ /**
+ * launches an application on a device or emulator
+ *
+ * @param info the {@link DelayedLaunchInfo} that indicates the launch action
+ * @param device the device or emulator to launch the application on
+ */
+ @Override
+ public void launchApp(final DelayedLaunchInfo info, IDevice device) {
+ if (info.isDebugMode()) {
+ synchronized (sListLock) {
+ if (mWaitingForDebuggerApplications.contains(info) == false) {
+ mWaitingForDebuggerApplications.add(info);
+ }
+ }
+ }
+ if (doLaunchAction(info, device)) {
+ // if the app is not a debug app, we need to do some clean up, as
+ // the process is done!
+ if (info.isDebugMode() == false) {
+ // stop the launch object, since there's no debug, and it can't
+ // provide any control over the app
+ stopLaunch(info);
+ }
+ } else {
+ // something went wrong or no further launch action needed
+ // lets stop the Launch
+ stopLaunch(info);
+ }
+ }
+
+ private boolean doLaunchAction(final DelayedLaunchInfo info, Collection<IDevice> devices) {
+ boolean result = info.getLaunchAction().doLaunchAction(info, devices);
+
+ // Monitor the logcat output on the launched device to notify
+ // the user if any significant error occurs that is visible from logcat
+ for (IDevice d : devices) {
+ DdmsPlugin.getDefault().startLogCatMonitor(d);
+ }
+
+ return result;
+ }
+
+ private boolean doLaunchAction(final DelayedLaunchInfo info, IDevice device) {
+ return doLaunchAction(info, Collections.singletonList(device));
+ }
+
+ private boolean launchEmulator(AndroidLaunchConfiguration config, AvdInfo avdToLaunch) {
+
+ // split the custom command line in segments
+ ArrayList<String> customArgs = new ArrayList<String>();
+ boolean hasWipeData = false;
+ if (config.mEmulatorCommandLine != null && config.mEmulatorCommandLine.length() > 0) {
+ String[] segments = config.mEmulatorCommandLine.split("\\s+"); //$NON-NLS-1$
+
+ // we need to remove the empty strings
+ for (String s : segments) {
+ if (s.length() > 0) {
+ customArgs.add(s);
+ if (!hasWipeData && s.equals(FLAG_WIPE_DATA)) {
+ hasWipeData = true;
+ }
+ }
+ }
+ }
+
+ boolean needsWipeData = config.mWipeData && !hasWipeData;
+ if (needsWipeData) {
+ if (!AdtPlugin.displayPrompt("Android Launch", "Are you sure you want to wipe all user data when starting this emulator?")) {
+ needsWipeData = false;
+ }
+ }
+
+ // build the command line based on the available parameters.
+ ArrayList<String> list = new ArrayList<String>();
+
+ String path = AdtPlugin.getOsAbsoluteEmulator();
+
+ list.add(path);
+
+ list.add(FLAG_AVD);
+ list.add(avdToLaunch.getName());
+
+ if (config.mNetworkSpeed != null) {
+ list.add(FLAG_NETSPEED);
+ list.add(config.mNetworkSpeed);
+ }
+
+ if (config.mNetworkDelay != null) {
+ list.add(FLAG_NETDELAY);
+ list.add(config.mNetworkDelay);
+ }
+
+ if (needsWipeData) {
+ list.add(FLAG_WIPE_DATA);
+ }
+
+ if (config.mNoBootAnim) {
+ list.add(FLAG_NO_BOOT_ANIM);
+ }
+
+ list.addAll(customArgs);
+
+ // convert the list into an array for the call to exec.
+ String[] command = list.toArray(new String[list.size()]);
+
+ // launch the emulator
+ try {
+ Process process = Runtime.getRuntime().exec(command);
+ grabEmulatorOutput(process);
+ } catch (IOException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Looks for and returns an existing {@link ILaunchConfiguration} object for a
+ * specified project.
+ * @param manager The {@link ILaunchManager}.
+ * @param type The {@link ILaunchConfigurationType}.
+ * @param projectName The name of the project
+ * @return an existing <code>ILaunchConfiguration</code> object matching the project, or
+ * <code>null</code>.
+ */
+ private static ILaunchConfiguration findConfig(ILaunchManager manager,
+ ILaunchConfigurationType type, String projectName) {
+ try {
+ ILaunchConfiguration[] configs = manager.getLaunchConfigurations(type);
+
+ for (ILaunchConfiguration config : configs) {
+ if (config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ "").equals(projectName)) { //$NON-NLS-1$
+ return config;
+ }
+ }
+ } catch (CoreException e) {
+ MessageDialog.openError(AdtPlugin.getShell(),
+ "Launch Error", e.getStatus().getMessage());
+ }
+
+ // didn't find anything that matches. Return null
+ return null;
+ }
+
+
+ /**
+ * Connects a remote debugger on the specified port.
+ * @param debugPort The port to connect the debugger to
+ * @param launch The associated AndroidLaunch object.
+ * @param monitor A Progress monitor
+ * @return false if cancelled by the monitor
+ * @throws CoreException
+ */
+ @SuppressWarnings("deprecation")
+ public static boolean connectRemoteDebugger(int debugPort,
+ AndroidLaunch launch, IProgressMonitor monitor)
+ throws CoreException {
+ // get some default parameters.
+ int connectTimeout = JavaRuntime.getPreferences().getInt(JavaRuntime.PREF_CONNECT_TIMEOUT);
+
+ HashMap<String, String> newMap = new HashMap<String, String>();
+
+ newMap.put("hostname", "localhost"); //$NON-NLS-1$ //$NON-NLS-2$
+
+ newMap.put("port", Integer.toString(debugPort)); //$NON-NLS-1$
+
+ newMap.put("timeout", Integer.toString(connectTimeout));
+
+ // get the default VM connector
+ IVMConnector connector = JavaRuntime.getDefaultVMConnector();
+
+ // connect to remote VM
+ connector.connect(newMap, monitor, launch);
+
+ // check for cancellation
+ if (monitor.isCanceled()) {
+ IDebugTarget[] debugTargets = launch.getDebugTargets();
+ for (IDebugTarget target : debugTargets) {
+ if (target.canDisconnect()) {
+ target.disconnect();
+ }
+ }
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Launch a new thread that connects a remote debugger on the specified port.
+ * @param debugPort The port to connect the debugger to
+ * @param androidLaunch The associated AndroidLaunch object.
+ * @param monitor A Progress monitor
+ * @see #connectRemoteDebugger(int, AndroidLaunch, IProgressMonitor)
+ */
+ public static void launchRemoteDebugger(final int debugPort, final AndroidLaunch androidLaunch,
+ final IProgressMonitor monitor) {
+ new Thread("Debugger connection") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ try {
+ connectRemoteDebugger(debugPort, androidLaunch, monitor);
+ } catch (CoreException e) {
+ androidLaunch.stopLaunch();
+ }
+ monitor.done();
+ }
+ }.start();
+ }
+
+ /**
+ * Sent when a new {@link AndroidDebugBridge} is started.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param bridge the new {@link AndroidDebugBridge} object.
+ *
+ * @see IDebugBridgeChangeListener#bridgeChanged(AndroidDebugBridge)
+ */
+ @Override
+ public void bridgeChanged(AndroidDebugBridge bridge) {
+ // The adb server has changed. We cancel any pending launches.
+ String message = "adb server change: cancelling '%1$s'!";
+ synchronized (sListLock) {
+ for (DelayedLaunchInfo launchInfo : mWaitingForReadyEmulatorList) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format(message, launchInfo.getLaunchAction().getLaunchDescription()));
+ stopLaunch(launchInfo);
+ }
+ for (DelayedLaunchInfo launchInfo : mWaitingForDebuggerApplications) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format(message,
+ launchInfo.getLaunchAction().getLaunchDescription()));
+ stopLaunch(launchInfo);
+ }
+
+ mWaitingForReadyEmulatorList.clear();
+ mWaitingForDebuggerApplications.clear();
+ }
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceConnected(IDevice)
+ */
+ @Override
+ public void deviceConnected(IDevice device) {
+ synchronized (sListLock) {
+ // look if there's an app waiting for a device
+ if (mWaitingForEmulatorLaunches.size() > 0) {
+ // get/remove first launch item from the list
+ // FIXME: what if we have multiple launches waiting?
+ DelayedLaunchInfo launchInfo = mWaitingForEmulatorLaunches.get(0);
+ mWaitingForEmulatorLaunches.remove(0);
+
+ // give the launch item its device for later use.
+ launchInfo.setDevice(device);
+
+ // and move it to the other list
+ mWaitingForReadyEmulatorList.add(launchInfo);
+
+ // and tell the user about it
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ String.format("New emulator found: %1$s", device.getSerialNumber()));
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ String.format("Waiting for HOME ('%1$s') to be launched...",
+ AdtPlugin.getDefault().getPreferenceStore().getString(
+ AdtPrefs.PREFS_HOME_PACKAGE)));
+ }
+ }
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceDisconnected(IDevice)
+ */
+ @Override
+ public void deviceDisconnected(IDevice device) {
+ // any pending launch on this device must be canceled.
+ String message = "%1$s disconnected! Cancelling '%2$s'!";
+ synchronized (sListLock) {
+ ArrayList<DelayedLaunchInfo> copyList =
+ (ArrayList<DelayedLaunchInfo>) mWaitingForReadyEmulatorList.clone();
+ for (DelayedLaunchInfo launchInfo : copyList) {
+ if (launchInfo.getDevice() == device) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format(message, device.getSerialNumber(),
+ launchInfo.getLaunchAction().getLaunchDescription()));
+ stopLaunch(launchInfo);
+ }
+ }
+ copyList = (ArrayList<DelayedLaunchInfo>) mWaitingForDebuggerApplications.clone();
+ for (DelayedLaunchInfo launchInfo : copyList) {
+ if (launchInfo.getDevice() == device) {
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format(message, device.getSerialNumber(),
+ launchInfo.getLaunchAction().getLaunchDescription()));
+ stopLaunch(launchInfo);
+ }
+ }
+ }
+ }
+
+ /**
+ * Sent when a device data changed, or when clients are started/terminated on the device.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the device that was updated.
+ * @param changeMask the mask indicating what changed.
+ *
+ * @see IDeviceChangeListener#deviceChanged(IDevice, int)
+ */
+ @Override
+ public void deviceChanged(IDevice device, int changeMask) {
+ // We could check if any starting device we care about is now ready, but we can wait for
+ // its home app to show up, so...
+ }
+
+ /**
+ * Sent when an existing client information changed.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param client the updated client.
+ * @param changeMask the bit mask describing the changed properties. It can contain
+ * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
+ * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
+ * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
+ * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
+ *
+ * @see IClientChangeListener#clientChanged(Client, int)
+ */
+ @Override
+ public void clientChanged(final Client client, int changeMask) {
+ boolean connectDebugger = false;
+ if ((changeMask & Client.CHANGE_NAME) == Client.CHANGE_NAME) {
+ String applicationName = client.getClientData().getClientDescription();
+ if (applicationName != null) {
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+ String home = store.getString(AdtPrefs.PREFS_HOME_PACKAGE);
+
+ if (home.equals(applicationName)) {
+
+ // looks like home is up, get its device
+ IDevice device = client.getDevice();
+
+ // look for application waiting for home
+ synchronized (sListLock) {
+ for (int i = 0; i < mWaitingForReadyEmulatorList.size(); ) {
+ DelayedLaunchInfo launchInfo = mWaitingForReadyEmulatorList.get(i);
+ if (launchInfo.getDevice() == device) {
+ // it's match, remove from the list
+ mWaitingForReadyEmulatorList.remove(i);
+
+ // We couldn't check earlier the API level of the device
+ // (it's asynchronous when the device boot, and usually
+ // deviceConnected is called before it's queried for its build info)
+ // so we check now
+ if (checkBuildInfo(launchInfo, device) == false) {
+ // device is not the proper API!
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Launch canceled!");
+ stopLaunch(launchInfo);
+ return;
+ }
+
+ AdtPlugin.printToConsole(launchInfo.getProject(),
+ String.format("HOME is up on device '%1$s'",
+ device.getSerialNumber()));
+
+ // attempt to sync the new package onto the device.
+ if (syncApp(launchInfo, device)) {
+ // application package is sync'ed, lets attempt to launch it.
+ launchApp(launchInfo, device);
+ } else {
+ // failure! Cancel and return
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ "Launch canceled!");
+ stopLaunch(launchInfo);
+ }
+
+ break;
+ } else {
+ i++;
+ }
+ }
+ }
+ }
+
+ // check if it's already waiting for a debugger, and if so we connect to it.
+ if (client.getClientData().getDebuggerConnectionStatus() == DebuggerStatus.WAITING) {
+ // search for this client in the list;
+ synchronized (sListLock) {
+ int index = mUnknownClientsWaitingForDebugger.indexOf(client);
+ if (index != -1) {
+ connectDebugger = true;
+ mUnknownClientsWaitingForDebugger.remove(client);
+ }
+ }
+ }
+ }
+ }
+
+ // if it's not home, it could be an app that is now in debugger mode that we're waiting for
+ // lets check it
+
+ if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) == Client.CHANGE_DEBUGGER_STATUS) {
+ ClientData clientData = client.getClientData();
+ String applicationName = client.getClientData().getClientDescription();
+ if (clientData.getDebuggerConnectionStatus() == DebuggerStatus.WAITING) {
+ // Get the application name, and make sure its valid.
+ if (applicationName == null) {
+ // looks like we don't have the client yet, so we keep it around for when its
+ // name becomes available.
+ synchronized (sListLock) {
+ mUnknownClientsWaitingForDebugger.add(client);
+ }
+ return;
+ } else {
+ connectDebugger = true;
+ }
+ }
+ }
+
+ if (connectDebugger) {
+ Log.d("adt", "Debugging " + client);
+ // now check it against the apps waiting for a debugger
+ String applicationName = client.getClientData().getClientDescription();
+ Log.d("adt", "App Name: " + applicationName);
+ synchronized (sListLock) {
+ for (int i = 0; i < mWaitingForDebuggerApplications.size(); ) {
+ final DelayedLaunchInfo launchInfo = mWaitingForDebuggerApplications.get(i);
+ if (client.getDevice() == launchInfo.getDevice() &&
+ applicationName.equals(launchInfo.getDebugPackageName())) {
+ // this is a match. We remove the launch info from the list
+ mWaitingForDebuggerApplications.remove(i);
+
+ // and connect the debugger.
+ String msg = String.format(
+ "Attempting to connect debugger to '%1$s' on port %2$d",
+ launchInfo.getDebugPackageName(), client.getDebuggerListenPort());
+ AdtPlugin.printToConsole(launchInfo.getProject(), msg);
+
+ new Thread("Debugger Connection") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ try {
+ if (connectRemoteDebugger(
+ client.getDebuggerListenPort(),
+ launchInfo.getLaunch(),
+ launchInfo.getMonitor()) == false) {
+ return;
+ }
+ } catch (CoreException e) {
+ // well something went wrong.
+ AdtPlugin.printErrorToConsole(launchInfo.getProject(),
+ String.format("Launch error: %s", e.getMessage()));
+ // stop the launch
+ stopLaunch(launchInfo);
+ }
+
+ launchInfo.getMonitor().done();
+ }
+ }.start();
+
+ // we're done processing this client.
+ return;
+
+ } else {
+ i++;
+ }
+ }
+ }
+
+ // if we get here, we haven't found an app that we were launching, so we look
+ // for opened android projects that contains the app asking for a debugger.
+ // If we find one, we automatically connect to it.
+ IProject project = ProjectHelper.findAndroidProjectByAppName(applicationName);
+
+ if (project != null) {
+ debugRunningApp(project, client.getDebuggerListenPort());
+ }
+ }
+ }
+
+ /**
+ * Get the stderr/stdout outputs of a process and return when the process is done.
+ * Both <b>must</b> be read or the process will block on windows.
+ * @param process The process to get the output from
+ */
+ private void grabEmulatorOutput(final Process process) {
+ // read the lines as they come. if null is returned, it's
+ // because the process finished
+ new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ // create a buffer to read the stderr output
+ InputStreamReader is = new InputStreamReader(process.getErrorStream());
+ BufferedReader errReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = errReader.readLine();
+ if (line != null) {
+ AdtPlugin.printErrorToConsole("Emulator", line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ }.start();
+
+ new Thread("") { //$NON-NLS-1$
+ @Override
+ public void run() {
+ InputStreamReader is = new InputStreamReader(process.getInputStream());
+ BufferedReader outReader = new BufferedReader(is);
+
+ try {
+ while (true) {
+ String line = outReader.readLine();
+ if (line != null) {
+ AdtPlugin.printToConsole("Emulator", line);
+ } else {
+ break;
+ }
+ }
+ } catch (IOException e) {
+ // do nothing.
+ }
+ }
+ }.start();
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ide.eclipse.adt.launch.ILaunchController#stopLaunch(com.android.ide.eclipse.adt.launch.AndroidLaunchController.DelayedLaunchInfo)
+ */
+ @Override
+ public void stopLaunch(DelayedLaunchInfo launchInfo) {
+ launchInfo.getLaunch().stopLaunch();
+ synchronized (sListLock) {
+ mWaitingForReadyEmulatorList.remove(launchInfo);
+ mWaitingForDebuggerApplications.remove(launchInfo);
+ }
+ }
+
+ public static void updateLaunchConfigWithLastUsedDevice(
+ ILaunchConfiguration launchConfiguration, DeviceChooserResponse response) {
+ try {
+ boolean configModified = false;
+ boolean reuse = launchConfiguration.getAttribute(
+ LaunchConfigDelegate.ATTR_REUSE_LAST_USED_DEVICE, false);
+ String serial = launchConfiguration.getAttribute(
+ LaunchConfigDelegate.ATTR_LAST_USED_DEVICE, (String)null);
+
+ ILaunchConfigurationWorkingCopy wc = launchConfiguration.getWorkingCopy();
+ if (reuse != response.useDeviceForFutureLaunches()) {
+ reuse = response.useDeviceForFutureLaunches();
+ wc.setAttribute(LaunchConfigDelegate.ATTR_REUSE_LAST_USED_DEVICE, reuse);
+ configModified = true;
+ }
+
+ if (reuse) {
+ String selected = getSerial(response);
+ if (selected != null && !selected.equalsIgnoreCase(serial)) {
+ wc.setAttribute(LaunchConfigDelegate.ATTR_LAST_USED_DEVICE, selected);
+ configModified = true;
+ }
+ }
+
+ if (configModified) {
+ wc.doSave();
+ }
+ } catch (CoreException e) {
+ // in such a case, users just won't see this setting take effect
+ return;
+ }
+ }
+
+ private static String getSerial(DeviceChooserResponse response) {
+ AvdInfo avd = response.getAvdToLaunch();
+ return (avd != null) ? avd.getName() : response.getDeviceToUse().getSerialNumber();
+ }
+
+ @Nullable
+ public static IDevice getDeviceIfOnline(@Nullable String serial,
+ @NonNull IDevice[] onlineDevices) {
+ if (serial == null) {
+ return null;
+ }
+
+ for (IDevice device : onlineDevices) {
+ if (serial.equals(device.getAvdName()) ||
+ serial.equals(device.getSerialNumber())) {
+ return device;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AvdCompatibility.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AvdCompatibility.java
new file mode 100644
index 000000000..6133002bc
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/AvdCompatibility.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.internal.avd.AvdInfo;
+
+public class AvdCompatibility {
+ public enum Compatibility {
+ YES,
+ NO,
+ UNKNOWN,
+ };
+
+ /**
+ * Returns whether the specified AVD can run the given project that is built against
+ * a particular SDK and has the specified minApiLevel.
+ * @param avd AVD to check compatibility for
+ * @param projectTarget project build target
+ * @param minApiVersion project min api level
+ * @return whether the given AVD can run the given application
+ */
+ public static Compatibility canRun(AvdInfo avd, IAndroidTarget projectTarget,
+ AndroidVersion minApiVersion) {
+ if (avd == null) {
+ return Compatibility.UNKNOWN;
+ }
+
+ IAndroidTarget avdTarget = avd.getTarget();
+ if (avdTarget == null) {
+ return Compatibility.UNKNOWN;
+ }
+
+ // for platform targets, we only need to check the min api version
+ if (projectTarget.isPlatform()) {
+ return avdTarget.getVersion().canRun(minApiVersion) ?
+ Compatibility.YES : Compatibility.NO;
+ }
+
+ // for add-on targets, delegate to the add on target to check for compatibility
+ return projectTarget.canRunOn(avdTarget) ? Compatibility.YES : Compatibility.NO;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/DelayedLaunchInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/DelayedLaunchInfo.java
new file mode 100644
index 000000000..b0a6dda20
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/DelayedLaunchInfo.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.ddmlib.IDevice;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.IProgressMonitor;
+
+/**
+ * A delayed launch waiting for a device to be present or ready before the
+ * application is launched.
+ */
+public final class DelayedLaunchInfo {
+
+ /**
+ * Used to indicate behavior when Android app already exists
+ */
+ enum InstallRetryMode {
+ NEVER, ALWAYS, PROMPT;
+ }
+
+ /** The device on which to launch the app */
+ private IDevice mDevice = null;
+
+ /** The eclipse project */
+ private final IProject mProject;
+
+ /** Package name */
+ private final String mPackageName;
+
+ /** Debug package name */
+ private final String mDebugPackageName;
+
+ /** IFile to the package (.apk) file */
+ private final IFile mPackageFile;
+
+ /** debuggable attribute of the manifest file. */
+ private final Boolean mDebuggable;
+
+ /** Required Api level by the app. null means no requirements */
+ private final String mRequiredApiVersionNumber;
+
+ private InstallRetryMode mRetryMode = InstallRetryMode.NEVER;
+
+ /** Launch action. */
+ private final IAndroidLaunchAction mLaunchAction;
+
+ /** the launch object */
+ private final AndroidLaunch mLaunch;
+
+ /** the monitor object */
+ private final IProgressMonitor mMonitor;
+
+ /** debug mode flag */
+ private boolean mDebugMode;
+
+ /** current number of launch attempts */
+ private int mAttemptCount = 0;
+
+ /** cancellation state of launch */
+ private boolean mCancelled = false;
+
+ /**
+ * Basic constructor with activity and package info.
+ *
+ * @param project the eclipse project that corresponds to Android app
+ * @param packageName package name of Android app
+ * @param debugPackageName the package name of the Andriod app to debug
+ * @param launchAction action to perform after app install
+ * @param pack IFile to the package (.apk) file
+ * @param debuggable the debuggable value of the app's manifest, or null if not set.
+ * @param requiredApiVersionNumber required SDK version by the app. null means no requirements.
+ * @param launch the launch object
+ * @param monitor progress monitor for launch
+ */
+ public DelayedLaunchInfo(IProject project, String packageName, String debugPackageName,
+ IAndroidLaunchAction launchAction, IFile pack, Boolean debuggable,
+ String requiredApiVersionNumber, AndroidLaunch launch, IProgressMonitor monitor) {
+ mProject = project;
+ mPackageName = packageName;
+ mDebugPackageName = debugPackageName;
+ mPackageFile = pack;
+ mLaunchAction = launchAction;
+ mLaunch = launch;
+ mMonitor = monitor;
+ mDebuggable = debuggable;
+ mRequiredApiVersionNumber = requiredApiVersionNumber;
+ }
+
+ /**
+ * @return the device on which to launch the app
+ */
+ public IDevice getDevice() {
+ return mDevice;
+ }
+
+ /**
+ * Set the device on which to launch the app
+ */
+ public void setDevice(IDevice device) {
+ mDevice = device;
+ }
+
+ /**
+ * @return the eclipse project that corresponds to Android app
+ */
+ public IProject getProject() {
+ return mProject;
+ }
+
+ /**
+ * @return the package name of the Android app
+ */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Returns the Android app process name that the debugger should connect to. Typically this is
+ * the same value as {@link #getPackageName()}.
+ */
+ public String getDebugPackageName() {
+ if (mDebugPackageName == null) {
+ return getPackageName();
+ }
+ return mDebugPackageName;
+ }
+
+ /**
+ * @return the application package file
+ */
+ public IFile getPackageFile() {
+ return mPackageFile;
+ }
+
+ /**
+ * Returns the value of the manifest debuggable attribute. If the attribute was not set,
+ * then the method returns null.
+ * @return the manifest debuggable attribute.
+ */
+ public Boolean getDebuggable() {
+ return mDebuggable;
+ }
+
+ /**
+ * @return the required api version number for the Android app.
+ */
+ public String getRequiredApiVersionNumber() {
+ return mRequiredApiVersionNumber;
+ }
+
+ /**
+ * @param retryMode the install retry mode to set
+ */
+ public void setRetryMode(InstallRetryMode retryMode) {
+ this.mRetryMode = retryMode;
+ }
+
+ /**
+ * @return the installation retry mode
+ */
+ public InstallRetryMode getRetryMode() {
+ return mRetryMode;
+ }
+
+ /**
+ * @return the launch action
+ */
+ public IAndroidLaunchAction getLaunchAction() {
+ return mLaunchAction;
+ }
+
+ /**
+ * @return the launch
+ */
+ public AndroidLaunch getLaunch() {
+ return mLaunch;
+ }
+
+ /**
+ * @return the launch progress monitor
+ */
+ public IProgressMonitor getMonitor() {
+ return mMonitor;
+ }
+
+ /**
+ * @param debugMode the debug mode to set
+ */
+ public void setDebugMode(boolean debugMode) {
+ this.mDebugMode = debugMode;
+ }
+
+ /**
+ * @return true if this is a debug launch
+ */
+ public boolean isDebugMode() {
+ return mDebugMode;
+ }
+
+ /**
+ * Increases the number of launch attempts
+ */
+ public void incrementAttemptCount() {
+ mAttemptCount++;
+ }
+
+ /**
+ * @return the number of launch attempts made
+ */
+ public int getAttemptCount() {
+ return mAttemptCount;
+ }
+
+ /**
+ * Set if launch has been cancelled
+ */
+ public void setCancelled(boolean cancelled) {
+ this.mCancelled = cancelled;
+ }
+
+ /**
+ * @return true if launch has been cancelled
+ */
+ public boolean isCancelled() {
+ return mCancelled;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/DeviceChooserDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/DeviceChooserDialog.java
new file mode 100644
index 000000000..995ccdf46
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/DeviceChooserDialog.java
@@ -0,0 +1,824 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.IDevice.DeviceState;
+import com.android.ddmuilib.ImageLoader;
+import com.android.ddmuilib.TableHelper;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.sdk.AdtConsoleSdkLog;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.ddms.DdmsPlugin;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdkuilib.internal.widgets.AvdSelector;
+import com.android.sdkuilib.internal.widgets.AvdSelector.DisplayMode;
+import com.android.sdkuilib.internal.widgets.AvdSelector.IAvdFilter;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.viewers.ILabelProviderListener;
+import org.eclipse.jface.viewers.IStructuredContentProvider;
+import org.eclipse.jface.viewers.ITableLabelProvider;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.SWTException;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A dialog that lets the user choose a device to deploy an application.
+ * The user can either choose an exiting running device (including running emulators)
+ * or start a new emulator using an Android Virtual Device configuration that matches
+ * the current project.
+ */
+public class DeviceChooserDialog extends Dialog implements IDeviceChangeListener {
+
+ private final static int ICON_WIDTH = 16;
+
+ private Table mDeviceTable;
+ private TableViewer mViewer;
+ private AvdSelector mPreferredAvdSelector;
+
+ private Image mDeviceImage;
+ private Image mEmulatorImage;
+ private Image mMatchImage;
+ private Image mNoMatchImage;
+ private Image mWarningImage;
+
+ private final DeviceChooserResponse mResponse;
+ private final String mPackageName;
+ private final IAndroidTarget mProjectTarget;
+ private final AndroidVersion mMinApiVersion;
+ private final Sdk mSdk;
+
+ private Button mDeviceRadioButton;
+ private Button mUseDeviceForFutureLaunchesCheckbox;
+ private boolean mUseDeviceForFutureLaunches;
+
+ private boolean mDisableAvdSelectionChange = false;
+
+ /**
+ * Basic Content Provider for a table full of {@link IDevice} objects. The input is
+ * a {@link AndroidDebugBridge}.
+ */
+ private class ContentProvider implements IStructuredContentProvider {
+ @Override
+ public Object[] getElements(Object inputElement) {
+ if (inputElement instanceof AndroidDebugBridge) {
+ return findCompatibleDevices(((AndroidDebugBridge)inputElement).getDevices());
+ }
+
+ return new Object[0];
+ }
+
+ private Object[] findCompatibleDevices(IDevice[] devices) {
+ if (devices == null) {
+ return null;
+ }
+
+ List<IDevice> compatibleDevices = new ArrayList<IDevice>(devices.length);
+ for (IDevice device : devices) {
+ AndroidVersion deviceVersion = Sdk.getDeviceVersion(device);
+ if (deviceVersion == null || deviceVersion.canRun(mMinApiVersion)) {
+ compatibleDevices.add(device);
+ }
+ }
+
+ return compatibleDevices.toArray();
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+ // pass
+ }
+ }
+
+ /**
+ * A Label Provider for the {@link TableViewer} in {@link DeviceChooserDialog}.
+ * It provides labels and images for {@link IDevice} objects.
+ */
+ private class LabelProvider implements ITableLabelProvider {
+
+ @Override
+ public Image getColumnImage(Object element, int columnIndex) {
+ if (element instanceof IDevice) {
+ IDevice device = (IDevice)element;
+ switch (columnIndex) {
+ case 0:
+ return device.isEmulator() ? mEmulatorImage : mDeviceImage;
+
+ case 2:
+ // check for compatibility.
+ if (device.isEmulator() == false) { // physical device
+ // get the version of the device
+ AndroidVersion deviceVersion = Sdk.getDeviceVersion(device);
+ if (deviceVersion == null) {
+ return mWarningImage;
+ } else {
+ if (!deviceVersion.canRun(mMinApiVersion)) {
+ return mNoMatchImage;
+ }
+
+ // if the project is compiling against an add-on,
+ // the optional API may be missing from the device.
+ return mProjectTarget.isPlatform() ?
+ mMatchImage : mWarningImage;
+ }
+ } else {
+ // get the AvdInfo
+ AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(),
+ true /*validAvdOnly*/);
+ AvdCompatibility.Compatibility c =
+ AvdCompatibility.canRun(info, mProjectTarget,
+ mMinApiVersion);
+ switch (c) {
+ case YES:
+ return mMatchImage;
+ case NO:
+ return mNoMatchImage;
+ case UNKNOWN:
+ return mWarningImage;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getColumnText(Object element, int columnIndex) {
+ if (element instanceof IDevice) {
+ IDevice device = (IDevice)element;
+ switch (columnIndex) {
+ case 0:
+ return device.getName();
+ case 1:
+ if (device.isEmulator()) {
+ return device.getAvdName();
+ } else {
+ return "N/A"; // devices don't have AVD names.
+ }
+ case 2:
+ if (device.isEmulator()) {
+ AvdInfo info = mSdk.getAvdManager().getAvd(device.getAvdName(),
+ true /*validAvdOnly*/);
+ if (info == null) {
+ return "?";
+ }
+ return info.getTarget().getFullName();
+ } else {
+ String deviceBuild = device.getProperty(IDevice.PROP_BUILD_VERSION);
+ if (deviceBuild == null) {
+ return "unknown";
+ }
+ return deviceBuild;
+ }
+ case 3:
+ String debuggable = device.getProperty(IDevice.PROP_DEBUGGABLE);
+ if (debuggable != null && debuggable.equals("1")) { //$NON-NLS-1$
+ return "Yes";
+ } else {
+ return "";
+ }
+ case 4:
+ return getStateString(device);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void addListener(ILabelProviderListener listener) {
+ // pass
+ }
+
+ @Override
+ public void dispose() {
+ // pass
+ }
+
+ @Override
+ public boolean isLabelProperty(Object element, String property) {
+ // pass
+ return false;
+ }
+
+ @Override
+ public void removeListener(ILabelProviderListener listener) {
+ // pass
+ }
+ }
+
+ public static class DeviceChooserResponse {
+ private AvdInfo mAvdToLaunch;
+ private IDevice mDeviceToUse;
+ private boolean mUseDeviceForFutureLaunches;
+
+ public void setDeviceToUse(IDevice d) {
+ mDeviceToUse = d;
+ mAvdToLaunch = null;
+ }
+
+ public void setAvdToLaunch(AvdInfo avd) {
+ mAvdToLaunch = avd;
+ mDeviceToUse = null;
+ }
+
+ public IDevice getDeviceToUse() {
+ return mDeviceToUse;
+ }
+
+ public AvdInfo getAvdToLaunch() {
+ return mAvdToLaunch;
+ }
+
+ public void setUseDeviceForFutureLaunches(boolean en) {
+ mUseDeviceForFutureLaunches = en;
+ }
+
+ public boolean useDeviceForFutureLaunches() {
+ return mUseDeviceForFutureLaunches;
+ }
+ }
+
+ public DeviceChooserDialog(Shell parent, DeviceChooserResponse response, String packageName,
+ IAndroidTarget projectTarget, AndroidVersion minApiVersion,
+ boolean useDeviceForFutureLaunches) {
+ super(parent);
+
+ mResponse = response;
+ mPackageName = packageName;
+ mProjectTarget = projectTarget;
+ mMinApiVersion = minApiVersion;
+ mSdk = Sdk.getCurrent();
+ mUseDeviceForFutureLaunches = useDeviceForFutureLaunches;
+
+ AndroidDebugBridge.addDeviceChangeListener(this);
+ loadImages();
+ }
+
+ private void cleanup() {
+ // done listening.
+ AndroidDebugBridge.removeDeviceChangeListener(this);
+ }
+
+ @Override
+ protected void okPressed() {
+ cleanup();
+ super.okPressed();
+ }
+
+ @Override
+ protected void cancelPressed() {
+ cleanup();
+ super.cancelPressed();
+ }
+
+ @Override
+ protected Control createContents(Composite parent) {
+ Control content = super.createContents(parent);
+
+ // this must be called after createContents() has happened so that the
+ // ok button has been created (it's created after the call to createDialogArea)
+ updateDefaultSelection();
+
+ return content;
+ }
+
+ /**
+ * Create the button bar: We override the Dialog implementation of this method
+ * so that we can create the checkbox at the same level as the 'Cancel' and 'OK' buttons.
+ */
+ @Override
+ protected Control createButtonBar(Composite parent) {
+ Composite composite = new Composite(parent, SWT.NONE);
+
+ GridLayout layout = new GridLayout(1, false);
+ layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN);
+ composite.setLayout(layout);
+ composite.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ mUseDeviceForFutureLaunchesCheckbox = new Button(composite, SWT.CHECK);
+ mUseDeviceForFutureLaunchesCheckbox.setSelection(mUseDeviceForFutureLaunches);
+ mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches);
+ mUseDeviceForFutureLaunchesCheckbox.setText("Use same device for future launches");
+ mUseDeviceForFutureLaunchesCheckbox.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ mUseDeviceForFutureLaunches =
+ mUseDeviceForFutureLaunchesCheckbox.getSelection();
+ mResponse.setUseDeviceForFutureLaunches(mUseDeviceForFutureLaunches);
+ }
+ });
+ mUseDeviceForFutureLaunchesCheckbox.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+
+ createButton(composite, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
+ createButton(composite, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false);
+
+ return composite;
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ // set dialog title
+ getShell().setText("Android Device Chooser");
+
+ Composite top = new Composite(parent, SWT.NONE);
+ top.setLayout(new GridLayout(1, true));
+
+ String msg;
+ if (mProjectTarget.isPlatform()) {
+ msg = String.format("Select a device with min API level %s.",
+ mMinApiVersion.getApiString());
+ } else {
+ msg = String.format("Select a device compatible with target %s.",
+ mProjectTarget.getFullName());
+ }
+ Label label = new Label(top, SWT.NONE);
+ label.setText(msg);
+
+ mDeviceRadioButton = new Button(top, SWT.RADIO);
+ mDeviceRadioButton.setText("Choose a running Android device");
+ mDeviceRadioButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ boolean deviceMode = mDeviceRadioButton.getSelection();
+
+ mDeviceTable.setEnabled(deviceMode);
+ mPreferredAvdSelector.setEnabled(!deviceMode);
+
+ if (deviceMode) {
+ handleDeviceSelection();
+ } else {
+ mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected());
+ }
+
+ enableOkButton();
+ }
+ });
+ mDeviceRadioButton.setSelection(true);
+
+
+ // offset the selector from the radio button
+ Composite offsetComp = new Composite(top, SWT.NONE);
+ offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ GridLayout layout = new GridLayout(1, false);
+ layout.marginRight = layout.marginHeight = 0;
+ layout.marginLeft = 30;
+ offsetComp.setLayout(layout);
+
+ mDeviceTable = new Table(offsetComp, SWT.SINGLE | SWT.FULL_SELECTION | SWT.BORDER);
+ GridData gd;
+ mDeviceTable.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
+ gd.heightHint = 100;
+
+ mDeviceTable.setHeaderVisible(true);
+ mDeviceTable.setLinesVisible(true);
+
+ TableHelper.createTableColumn(mDeviceTable, "Serial Number",
+ SWT.LEFT, "AAA+AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$
+ null /* prefs name */, null /* prefs store */);
+
+ TableHelper.createTableColumn(mDeviceTable, "AVD Name",
+ SWT.LEFT, "AAAAAAAAAAAAAAAAAAA", //$NON-NLS-1$
+ null /* prefs name */, null /* prefs store */);
+
+ TableHelper.createTableColumn(mDeviceTable, "Target",
+ SWT.LEFT, "AAA+Android 9.9.9", //$NON-NLS-1$
+ null /* prefs name */, null /* prefs store */);
+
+ TableHelper.createTableColumn(mDeviceTable, "Debug",
+ SWT.LEFT, "Debug", //$NON-NLS-1$
+ null /* prefs name */, null /* prefs store */);
+
+ TableHelper.createTableColumn(mDeviceTable, "State",
+ SWT.LEFT, "bootloader", //$NON-NLS-1$
+ null /* prefs name */, null /* prefs store */);
+
+ // create the viewer for it
+ mViewer = new TableViewer(mDeviceTable);
+ mViewer.setContentProvider(new ContentProvider());
+ mViewer.setLabelProvider(new LabelProvider());
+ mViewer.setInput(AndroidDebugBridge.getBridge());
+
+ mDeviceTable.addSelectionListener(new SelectionAdapter() {
+ /**
+ * Handles single-click selection on the device selector.
+ * {@inheritDoc}
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ handleDeviceSelection();
+ }
+
+ /**
+ * Handles double-click selection on the device selector.
+ * Note that the single-click handler will probably already have been called.
+ * {@inheritDoc}
+ */
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ handleDeviceSelection();
+ if (isOkButtonEnabled()) {
+ okPressed();
+ }
+ }
+ });
+
+ Button radio2 = new Button(top, SWT.RADIO);
+ radio2.setText("Launch a new Android Virtual Device");
+
+ // offset the selector from the radio button
+ offsetComp = new Composite(top, SWT.NONE);
+ offsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ layout = new GridLayout(1, false);
+ layout.marginRight = layout.marginHeight = 0;
+ layout.marginLeft = 30;
+ offsetComp.setLayout(layout);
+
+ mPreferredAvdSelector = new AvdSelector(offsetComp,
+ mSdk.getSdkOsLocation(),
+ mSdk.getAvdManager(),
+ new NonRunningAvdFilter(),
+ DisplayMode.SIMPLE_SELECTION,
+ new AdtConsoleSdkLog());
+ mPreferredAvdSelector.setTableHeightHint(100);
+ mPreferredAvdSelector.setEnabled(false);
+ mPreferredAvdSelector.setSelectionListener(new SelectionAdapter() {
+ /**
+ * Handles single-click selection on the AVD selector.
+ * {@inheritDoc}
+ */
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mDisableAvdSelectionChange == false) {
+ mResponse.setAvdToLaunch(mPreferredAvdSelector.getSelected());
+ enableOkButton();
+ }
+ }
+
+ /**
+ * Handles double-click selection on the AVD selector.
+ *
+ * Note that the single-click handler will probably already have been called
+ * but the selected item can have changed in between.
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ widgetSelected(e);
+ if (isOkButtonEnabled()) {
+ okPressed();
+ }
+ }
+ });
+
+ return top;
+ }
+
+ private void loadImages() {
+ ImageLoader ddmUiLibLoader = ImageLoader.getDdmUiLibLoader();
+ Display display = DdmsPlugin.getDisplay();
+ IconFactory factory = IconFactory.getInstance();
+
+ if (mDeviceImage == null) {
+ mDeviceImage = ddmUiLibLoader.loadImage(display,
+ "device.png", //$NON-NLS-1$
+ ICON_WIDTH, ICON_WIDTH,
+ display.getSystemColor(SWT.COLOR_RED));
+ }
+ if (mEmulatorImage == null) {
+ mEmulatorImage = ddmUiLibLoader.loadImage(display,
+ "emulator.png", ICON_WIDTH, ICON_WIDTH, //$NON-NLS-1$
+ display.getSystemColor(SWT.COLOR_BLUE));
+ }
+
+ if (mMatchImage == null) {
+ mMatchImage = factory.getIcon("match", //$NON-NLS-1$
+ IconFactory.COLOR_GREEN,
+ IconFactory.SHAPE_DEFAULT);
+ }
+
+ if (mNoMatchImage == null) {
+ mNoMatchImage = factory.getIcon("error", //$NON-NLS-1$
+ IconFactory.COLOR_RED,
+ IconFactory.SHAPE_DEFAULT);
+ }
+
+ if (mWarningImage == null) {
+ mWarningImage = factory.getIcon("warning", //$NON-NLS-1$
+ SWT.COLOR_YELLOW,
+ IconFactory.SHAPE_DEFAULT);
+ }
+
+ }
+
+ /**
+ * Returns a display string representing the state of the device.
+ * @param d the device
+ */
+ private static String getStateString(IDevice d) {
+ DeviceState deviceState = d.getState();
+ if (deviceState == DeviceState.ONLINE) {
+ return "Online";
+ } else if (deviceState == DeviceState.OFFLINE) {
+ return "Offline";
+ } else if (deviceState == DeviceState.BOOTLOADER) {
+ return "Bootloader";
+ }
+
+ return "??";
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceConnected(IDevice)
+ */
+ @Override
+ public void deviceConnected(IDevice device) {
+ final DeviceChooserDialog dialog = this;
+ exec(new Runnable() {
+ @Override
+ public void run() {
+ if (mDeviceTable.isDisposed() == false) {
+ // refresh all
+ mViewer.refresh();
+
+ // update the selection
+ updateDefaultSelection();
+
+ // update the display of AvdInfo (since it's filtered to only display
+ // non running AVD.)
+ refillAvdList(false /*reloadAvds*/);
+ } else {
+ // table is disposed, we need to do something.
+ // lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDeviceChangeListener(dialog);
+ }
+
+ }
+ });
+ }
+
+ /**
+ * Sent when the a device is connected to the {@link AndroidDebugBridge}.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the new device.
+ *
+ * @see IDeviceChangeListener#deviceDisconnected(IDevice)
+ */
+ @Override
+ public void deviceDisconnected(IDevice device) {
+ deviceConnected(device);
+ }
+
+ /**
+ * Sent when a device data changed, or when clients are started/terminated on the device.
+ * <p/>
+ * This is sent from a non UI thread.
+ * @param device the device that was updated.
+ * @param changeMask the mask indicating what changed.
+ *
+ * @see IDeviceChangeListener#deviceChanged(IDevice, int)
+ */
+ @Override
+ public void deviceChanged(final IDevice device, int changeMask) {
+ if ((changeMask & (IDevice.CHANGE_STATE | IDevice.CHANGE_BUILD_INFO)) != 0) {
+ final DeviceChooserDialog dialog = this;
+ exec(new Runnable() {
+ @Override
+ public void run() {
+ if (mDeviceTable.isDisposed() == false) {
+ // refresh the device
+ mViewer.refresh(device);
+
+ // update the defaultSelection.
+ updateDefaultSelection();
+
+ // update the display of AvdInfo (since it's filtered to only display
+ // non running AVD). This is done on deviceChanged because the avd name
+ // of a (emulator) device may be updated as the emulator boots.
+
+ refillAvdList(false /*reloadAvds*/);
+
+ // if the changed device is the current selection,
+ // we update the OK button based on its state.
+ if (device == mResponse.getDeviceToUse()) {
+ enableOkButton();
+ }
+
+ } else {
+ // table is disposed, we need to do something.
+ // lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDeviceChangeListener(dialog);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Returns whether the dialog is in "device" mode (true), or in "avd" mode (false).
+ */
+ private boolean isDeviceMode() {
+ return mDeviceRadioButton.getSelection();
+ }
+
+ /**
+ * Enables or disables the OK button of the dialog based on various selections in the dialog.
+ */
+ private void enableOkButton() {
+ Button okButton = getButton(IDialogConstants.OK_ID);
+
+ if (isDeviceMode()) {
+ okButton.setEnabled(mResponse.getDeviceToUse() != null &&
+ mResponse.getDeviceToUse().isOnline());
+ } else {
+ okButton.setEnabled(mResponse.getAvdToLaunch() != null);
+ }
+ }
+
+ /**
+ * Returns true if the ok button is enabled.
+ */
+ private boolean isOkButtonEnabled() {
+ Button okButton = getButton(IDialogConstants.OK_ID);
+ return okButton.isEnabled();
+ }
+
+ /**
+ * Executes the {@link Runnable} in the UI thread.
+ * @param runnable the runnable to execute.
+ */
+ private void exec(Runnable runnable) {
+ try {
+ Display display = mDeviceTable.getDisplay();
+ display.asyncExec(runnable);
+ } catch (SWTException e) {
+ // tree is disposed, we need to do something. lets remove ourselves from the listener.
+ AndroidDebugBridge.removeDeviceChangeListener(this);
+ }
+ }
+
+ private void handleDeviceSelection() {
+ int count = mDeviceTable.getSelectionCount();
+ if (count != 1) {
+ handleSelection(null);
+ } else {
+ int index = mDeviceTable.getSelectionIndex();
+ Object data = mViewer.getElementAt(index);
+ if (data instanceof IDevice) {
+ handleSelection((IDevice)data);
+ } else {
+ handleSelection(null);
+ }
+ }
+ }
+
+ private void handleSelection(IDevice device) {
+ mResponse.setDeviceToUse(device);
+ enableOkButton();
+ }
+
+ /**
+ * Look for a default device to select. This is done by looking for the running
+ * clients on each device and finding one similar to the one being launched.
+ * <p/>
+ * This is done every time the device list changed unless there is a already selection.
+ */
+ private void updateDefaultSelection() {
+ if (mDeviceTable.getSelectionCount() == 0) {
+ AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
+
+ IDevice[] devices = bridge.getDevices();
+
+ for (IDevice device : devices) {
+ Client[] clients = device.getClients();
+
+ for (Client client : clients) {
+
+ if (mPackageName.equals(client.getClientData().getClientDescription())) {
+ // found a match! Select it.
+ mViewer.setSelection(new StructuredSelection(device));
+ handleSelection(device);
+
+ // and we're done.
+ return;
+ }
+ }
+ }
+ }
+
+ handleDeviceSelection();
+ }
+
+ private final class NonRunningAvdFilter implements IAvdFilter {
+
+ private IDevice[] mDevices;
+
+ @Override
+ public void prepare() {
+ mDevices = AndroidDebugBridge.getBridge().getDevices();
+ }
+
+ @Override
+ public boolean accept(AvdInfo avd) {
+ if (mDevices != null) {
+ for (IDevice d : mDevices) {
+ // do not accept running avd's
+ if (avd.getName().equals(d.getAvdName())) {
+ return false;
+ }
+
+ // only accept avd's that can actually run the project
+ AvdCompatibility.Compatibility c =
+ AvdCompatibility.canRun(avd, mProjectTarget, mMinApiVersion);
+ return (c == AvdCompatibility.Compatibility.NO) ? false : true;
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public void cleanup() {
+ mDevices = null;
+ }
+ }
+
+ /**
+ * Refills the AVD list keeping the current selection.
+ */
+ private void refillAvdList(boolean reloadAvds) {
+ // save the current selection
+ AvdInfo selected = mPreferredAvdSelector.getSelected();
+
+ // disable selection change.
+ mDisableAvdSelectionChange = true;
+
+ // refresh the list
+ mPreferredAvdSelector.refresh(false);
+
+ // attempt to reselect the proper avd if needed
+ if (selected != null) {
+ if (mPreferredAvdSelector.setSelection(selected) == false) {
+ // looks like the selection is lost. this can happen if an emulator
+ // running the AVD that was selected was launched from outside of Eclipse).
+ mResponse.setAvdToLaunch(null);
+ enableOkButton();
+ }
+ }
+
+ // enable the selection change
+ mDisableAvdSelectionChange = false;
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/EmptyLaunchAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/EmptyLaunchAction.java
new file mode 100644
index 000000000..a8bb414e9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/EmptyLaunchAction.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.ddmlib.IDevice;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import java.util.Collection;
+
+/**
+ * A launch action that does nothing after the application has been installed
+ */
+public class EmptyLaunchAction implements IAndroidLaunchAction {
+ @Override
+ public boolean doLaunchAction(DelayedLaunchInfo info, Collection<IDevice> devices) {
+ for (IDevice d : devices) {
+ doLaunchAction(info, d);
+ }
+
+ return false;
+ }
+
+ public boolean doLaunchAction(DelayedLaunchInfo info, IDevice device) {
+ // we're not supposed to do anything, just return;
+ String msg = String.format("%1$s installed on device",
+ info.getPackageFile().getFullPath().toOSString());
+ AdtPlugin.printToConsole(info.getProject(), msg, "Done!");
+ // return false so launch controller will not wait for debugger to attach
+ return false;
+ }
+
+ @Override
+ public String getLaunchDescription() {
+ return "sync";
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/EmulatorConfigTab.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/EmulatorConfigTab.java
new file mode 100644
index 000000000..779dfa111
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/EmulatorConfigTab.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.ddmuilib.ImageLoader;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration.TargetMode;
+import com.android.ide.eclipse.adt.internal.launch.AvdCompatibility.Compatibility;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.AdtConsoleSdkLog;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.internal.avd.AvdInfo;
+import com.android.sdklib.internal.avd.AvdManager;
+import com.android.sdkuilib.internal.widgets.AvdSelector;
+import com.android.sdkuilib.internal.widgets.AvdSelector.DisplayMode;
+import com.android.sdkuilib.internal.widgets.AvdSelector.IAvdFilter;
+import com.android.utils.NullLogger;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Launch configuration tab to control the parameters of the Emulator
+ */
+public class EmulatorConfigTab extends AbstractLaunchConfigurationTab {
+
+ private final static String[][] NETWORK_SPEEDS = new String[][] {
+ { "Full", "full" }, //$NON-NLS-2$
+ { "GSM", "gsm" }, //$NON-NLS-2$
+ { "HSCSD", "hscsd" }, //$NON-NLS-2$
+ { "GPRS", "gprs" }, //$NON-NLS-2$
+ { "EDGE", "edge" }, //$NON-NLS-2$
+ { "UMTS", "umts" }, //$NON-NLS-2$
+ { "HSPDA", "hsdpa" }, //$NON-NLS-2$
+ };
+
+ private final static String[][] NETWORK_LATENCIES = new String[][] {
+ { "None", "none" }, //$NON-NLS-2$
+ { "GPRS", "gprs" }, //$NON-NLS-2$
+ { "EDGE", "edge" }, //$NON-NLS-2$
+ { "UMTS", "umts" }, //$NON-NLS-2$
+ };
+
+ private Button mAutoTargetButton;
+ private Button mManualTargetButton;
+ private AvdSelector mPreferredAvdSelector;
+ private Combo mSpeedCombo;
+ private Combo mDelayCombo;
+ private Group mEmulatorOptionsGroup;
+ private Text mEmulatorCLOptions;
+ private Button mWipeDataButton;
+ private Button mNoBootAnimButton;
+ private Label mPreferredAvdLabel;
+ private IAndroidTarget mProjectTarget;
+ private AndroidVersion mProjectMinApiVersion;
+ private Button mFutureLaunchesOnSameDevice;
+ private boolean mSupportMultiDeviceLaunch;
+ private Button mAllDevicesTargetButton;
+ private Combo mDeviceTypeCombo;
+
+ private static final String DEVICES_AND_EMULATORS = "Active devices and AVD's";
+ private static final String EMULATORS_ONLY = "Active AVD's";
+ private static final String DEVICES_ONLY = "Active devices";
+
+ /**
+ * Returns the emulator ready speed option value.
+ * @param value The index of the combo selection.
+ */
+ public static String getSpeed(int value) {
+ try {
+ return NETWORK_SPEEDS[value][1];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return NETWORK_SPEEDS[LaunchConfigDelegate.DEFAULT_SPEED][1];
+ }
+ }
+
+ /**
+ * Returns the emulator ready network latency value.
+ * @param value The index of the combo selection.
+ */
+ public static String getDelay(int value) {
+ try {
+ return NETWORK_LATENCIES[value][1];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return NETWORK_LATENCIES[LaunchConfigDelegate.DEFAULT_DELAY][1];
+ }
+ }
+
+ /**
+ *
+ */
+ public EmulatorConfigTab(boolean supportMultiDeviceLaunch) {
+ mSupportMultiDeviceLaunch = supportMultiDeviceLaunch;
+ }
+
+ /**
+ * @wbp.parser.entryPoint
+ */
+ @Override
+ public void createControl(Composite parent) {
+ Font font = parent.getFont();
+
+ // Reload the AVDs to make sure we are up to date
+ try {
+ // SDK can be null if the user opens the dialog before ADT finished
+ // initializing the SDK itself. In this case just don't reload anything
+ // so there's nothing obsolete yet.
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk != null) {
+ AvdManager avdMan = sdk.getAvdManager();
+ assert avdMan != null;
+ avdMan.reloadAvds(NullLogger.getLogger());
+ }
+ } catch (AndroidLocationException e1) {
+ // this happens if the AVD Manager failed to find the folder in which the AVDs are
+ // stored. There isn't much we can do at this point.
+ }
+
+ Composite topComp = new Composite(parent, SWT.NONE);
+ setControl(topComp);
+ GridLayout topLayout = new GridLayout();
+ topLayout.numColumns = 1;
+ topLayout.verticalSpacing = 0;
+ topComp.setLayout(topLayout);
+ topComp.setFont(font);
+
+ GridData gd;
+ GridLayout layout;
+
+ // radio button for the target mode
+ Group targetModeGroup = new Group(topComp, SWT.NONE);
+ targetModeGroup.setText("Deployment Target Selection Mode");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ targetModeGroup.setLayoutData(gd);
+ layout = new GridLayout();
+ layout.numColumns = 1;
+ targetModeGroup.setLayout(layout);
+ targetModeGroup.setFont(font);
+
+ mManualTargetButton = new Button(targetModeGroup, SWT.RADIO);
+ mManualTargetButton.setText("Always prompt to pick device");
+
+ mAllDevicesTargetButton = new Button(targetModeGroup, SWT.RADIO);
+ mAllDevicesTargetButton.setText("Launch on all compatible devices/AVD's");
+ mAllDevicesTargetButton.setEnabled(mSupportMultiDeviceLaunch);
+
+ Composite deviceTypeOffsetComp = new Composite(targetModeGroup, SWT.NONE);
+ deviceTypeOffsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ layout = new GridLayout(1, false);
+ layout.marginRight = layout.marginHeight = 0;
+ layout.marginLeft = 30;
+ deviceTypeOffsetComp.setLayout(layout);
+
+ mDeviceTypeCombo = new Combo(deviceTypeOffsetComp, SWT.READ_ONLY);
+ mDeviceTypeCombo.setItems(new String[] {
+ DEVICES_AND_EMULATORS,
+ EMULATORS_ONLY,
+ DEVICES_ONLY,
+ });
+ mDeviceTypeCombo.select(0);
+ mDeviceTypeCombo.setEnabled(false);
+
+ // add the radio button
+ mAutoTargetButton = new Button(targetModeGroup, SWT.RADIO);
+ mAutoTargetButton.setText("Automatically pick compatible device: "
+ + "Always uses preferred AVD if set below, "
+ + "launches on compatible device/AVD otherwise.");
+ mAutoTargetButton.setSelection(true);
+
+ SelectionListener targetModeChangeListener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ targetModeChanged();
+ }
+ };
+
+ mAutoTargetButton.addSelectionListener(targetModeChangeListener);
+ mAllDevicesTargetButton.addSelectionListener(targetModeChangeListener);
+ mManualTargetButton.addSelectionListener(targetModeChangeListener);
+
+ Composite avdOffsetComp = new Composite(targetModeGroup, SWT.NONE);
+ avdOffsetComp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ layout = new GridLayout(1, false);
+ layout.marginRight = layout.marginHeight = 0;
+ layout.marginLeft = 30;
+ avdOffsetComp.setLayout(layout);
+
+ mPreferredAvdLabel = new Label(avdOffsetComp, SWT.NONE);
+ mPreferredAvdLabel.setText("Select a preferred Android Virtual Device for deployment:");
+
+ // create the selector with no manager, we'll reset the manager every time this is
+ // displayed to ensure we have the latest one (dialog is reused but SDK could have
+ // been changed in between.
+ mPreferredAvdSelector = new AvdSelector(avdOffsetComp,
+ Sdk.getCurrent().getSdkOsLocation(),
+ null /* avd manager */,
+ DisplayMode.SIMPLE_CHECK,
+ new AdtConsoleSdkLog());
+ mPreferredAvdSelector.setTableHeightHint(100);
+ SelectionListener listener = new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateLaunchConfigurationDialog();
+ }
+ };
+ mPreferredAvdSelector.setSelectionListener(listener);
+ mDeviceTypeCombo.addSelectionListener(listener);
+
+ mFutureLaunchesOnSameDevice = new Button(targetModeGroup, SWT.CHECK);
+ mFutureLaunchesOnSameDevice.setText("Use same device for future launches");
+ mFutureLaunchesOnSameDevice.addSelectionListener(listener);
+
+ // emulator size
+ mEmulatorOptionsGroup = new Group(topComp, SWT.NONE);
+ mEmulatorOptionsGroup.setText("Emulator launch parameters:");
+ mEmulatorOptionsGroup.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ layout = new GridLayout();
+ layout.numColumns = 2;
+ mEmulatorOptionsGroup.setLayout(layout);
+ mEmulatorOptionsGroup.setFont(font);
+
+ // Explanation
+ Label l = new Label(mEmulatorOptionsGroup, SWT.NONE);
+ l.setText("If no compatible and active devices or AVD's are found, then an AVD "
+ + "might be launched. Provide options for the AVD launch below.");
+ gd = new GridData();
+ gd.horizontalSpan = 2;
+ l.setLayoutData(gd);
+
+ // network options
+ new Label(mEmulatorOptionsGroup, SWT.NONE).setText("Network Speed:");
+
+ mSpeedCombo = new Combo(mEmulatorOptionsGroup, SWT.READ_ONLY);
+ for (String[] speed : NETWORK_SPEEDS) {
+ mSpeedCombo.add(speed[0]);
+ }
+ mSpeedCombo.addSelectionListener(listener);
+ mSpeedCombo.pack();
+
+ new Label(mEmulatorOptionsGroup, SWT.NONE).setText("Network Latency:");
+
+ mDelayCombo = new Combo(mEmulatorOptionsGroup, SWT.READ_ONLY);
+
+ for (String[] delay : NETWORK_LATENCIES) {
+ mDelayCombo.add(delay[0]);
+ }
+ mDelayCombo.addSelectionListener(listener);
+ mDelayCombo.pack();
+
+ // wipe data option
+ mWipeDataButton = new Button(mEmulatorOptionsGroup, SWT.CHECK);
+ mWipeDataButton.setText("Wipe User Data");
+ mWipeDataButton.setToolTipText("Check this if you want to wipe your user data each time you start the emulator. You will be prompted for confirmation when the emulator starts.");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ mWipeDataButton.setLayoutData(gd);
+ mWipeDataButton.addSelectionListener(listener);
+
+ // no boot anim option
+ mNoBootAnimButton = new Button(mEmulatorOptionsGroup, SWT.CHECK);
+ mNoBootAnimButton.setText("Disable Boot Animation");
+ mNoBootAnimButton.setToolTipText("Check this if you want to disable the boot animation. This can help the emulator start faster on slow machines.");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ mNoBootAnimButton.setLayoutData(gd);
+ mNoBootAnimButton.addSelectionListener(listener);
+
+ // custom command line option for emulator
+ l = new Label(mEmulatorOptionsGroup, SWT.NONE);
+ l.setText("Additional Emulator Command Line Options");
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ l.setLayoutData(gd);
+
+ mEmulatorCLOptions = new Text(mEmulatorOptionsGroup, SWT.BORDER);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ mEmulatorCLOptions.setLayoutData(gd);
+ mEmulatorCLOptions.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+ }
+
+ private void targetModeChanged() {
+ updateLaunchConfigurationDialog();
+
+ boolean auto = mAutoTargetButton.getSelection();
+ mPreferredAvdSelector.setEnabled(auto);
+ mPreferredAvdLabel.setEnabled(auto);
+
+ boolean all = mAllDevicesTargetButton.getSelection();
+ mDeviceTypeCombo.setEnabled(all);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#getName()
+ */
+ @Override
+ public String getName() {
+ return "Target";
+ }
+
+ @Override
+ public Image getImage() {
+ return ImageLoader.getDdmUiLibLoader().loadImage("emulator.png", null); //$NON-NLS-1$
+ }
+
+ private void updateAvdList(AvdManager avdManager) {
+ if (avdManager == null) {
+ avdManager = Sdk.getCurrent().getAvdManager();
+ }
+
+ mPreferredAvdSelector.setManager(avdManager);
+ mPreferredAvdSelector.refresh(false);
+
+ mPreferredAvdSelector.setFilter(new IAvdFilter() {
+ @Override
+ public void prepare() {
+ }
+
+ @Override
+ public void cleanup() {
+ }
+
+ @Override
+ public boolean accept(AvdInfo avd) {
+ AvdCompatibility.Compatibility c =
+ AvdCompatibility.canRun(avd, mProjectTarget, mProjectMinApiVersion);
+ return (c == Compatibility.NO) ? false : true;
+ }
+ });
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#initializeFrom(org.eclipse.debug.core.ILaunchConfiguration)
+ */
+ @Override
+ public void initializeFrom(ILaunchConfiguration configuration) {
+ AvdManager avdManager = Sdk.getCurrent().getAvdManager();
+
+ TargetMode mode = AndroidLaunchConfiguration.parseTargetMode(configuration,
+ LaunchConfigDelegate.DEFAULT_TARGET_MODE);
+
+ boolean multipleDevices = mode.isMultiDevice();
+ if (multipleDevices && !mSupportMultiDeviceLaunch) {
+ // The launch config says to run on multiple devices, but this launch type does not
+ // suppport multiple devices. In such a case, switch back to default mode.
+ // This could happen if a launch config used for Run is then used for Debug.
+ multipleDevices = false;
+ mode = LaunchConfigDelegate.DEFAULT_TARGET_MODE;
+ }
+
+ mAutoTargetButton.setSelection(mode == TargetMode.AUTO);
+ mManualTargetButton.setSelection(mode == TargetMode.MANUAL);
+ mAllDevicesTargetButton.setSelection(multipleDevices);
+
+ targetModeChanged();
+
+ boolean reuseLastUsedDevice;
+ try {
+ reuseLastUsedDevice = configuration.getAttribute(
+ LaunchConfigDelegate.ATTR_REUSE_LAST_USED_DEVICE, false);
+ } catch (CoreException ex) {
+ reuseLastUsedDevice = false;
+ }
+ mFutureLaunchesOnSameDevice.setSelection(reuseLastUsedDevice);
+
+ mDeviceTypeCombo.setEnabled(multipleDevices);
+ if (multipleDevices) {
+ int index = 0;
+ if (mode == TargetMode.ALL_EMULATORS) {
+ index = 1;
+ } else if (mode == TargetMode.ALL_DEVICES) {
+ index = 2;
+ }
+ mDeviceTypeCombo.select(index);
+ }
+
+ // look for the project name to get its target.
+ String stringValue = "";
+ try {
+ stringValue = configuration.getAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, stringValue);
+ } catch (CoreException ce) {
+ // let's not do anything here, we'll use the default value
+ }
+
+ IProject project = null;
+
+ // get the list of existing Android projects from the workspace.
+ IJavaProject[] projects = BaseProjectHelper.getAndroidProjects(null /*filter*/);
+ if (projects != null) {
+ // look for the project whose name we read from the configuration.
+ for (IJavaProject p : projects) {
+ if (p.getElementName().equals(stringValue)) {
+ project = p.getProject();
+ break;
+ }
+ }
+ }
+
+ // update the AVD list
+ if (project != null) {
+ mProjectTarget = Sdk.getCurrent().getTarget(project);
+
+ ManifestInfo mi = ManifestInfo.get(project);
+ final int minApiLevel = mi.getMinSdkVersion();
+ final String minApiCodeName = mi.getMinSdkCodeName();
+ mProjectMinApiVersion = new AndroidVersion(minApiLevel, minApiCodeName);
+ }
+
+ updateAvdList(avdManager);
+
+ stringValue = "";
+ try {
+ stringValue = configuration.getAttribute(LaunchConfigDelegate.ATTR_AVD_NAME,
+ stringValue);
+ } catch (CoreException e) {
+ // let's not do anything here, we'll use the default value
+ }
+
+ if (stringValue != null && stringValue.length() > 0 && avdManager != null) {
+ AvdInfo targetAvd = avdManager.getAvd(stringValue, true /*validAvdOnly*/);
+ mPreferredAvdSelector.setSelection(targetAvd);
+ } else {
+ mPreferredAvdSelector.setSelection(null);
+ }
+
+ boolean value = LaunchConfigDelegate.DEFAULT_WIPE_DATA;
+ try {
+ value = configuration.getAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA, value);
+ } catch (CoreException e) {
+ // let's not do anything here, we'll use the default value
+ }
+ mWipeDataButton.setSelection(value);
+
+ value = LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM;
+ try {
+ value = configuration.getAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM, value);
+ } catch (CoreException e) {
+ // let's not do anything here, we'll use the default value
+ }
+ mNoBootAnimButton.setSelection(value);
+
+ int index = -1;
+
+ index = LaunchConfigDelegate.DEFAULT_SPEED;
+ try {
+ index = configuration.getAttribute(LaunchConfigDelegate.ATTR_SPEED,
+ index);
+ } catch (CoreException e) {
+ // let's not do anything here, we'll use the default value
+ }
+ if (index == -1) {
+ mSpeedCombo.clearSelection();
+ } else {
+ mSpeedCombo.select(index);
+ }
+
+ index = LaunchConfigDelegate.DEFAULT_DELAY;
+ try {
+ index = configuration.getAttribute(LaunchConfigDelegate.ATTR_DELAY,
+ index);
+ } catch (CoreException e) {
+ // let's not do anything here, we'll put a proper value in
+ // performApply anyway
+ }
+ if (index == -1) {
+ mDelayCombo.clearSelection();
+ } else {
+ mDelayCombo.select(index);
+ }
+
+ String commandLine = null;
+ try {
+ commandLine = configuration.getAttribute(
+ LaunchConfigDelegate.ATTR_COMMANDLINE, ""); //$NON-NLS-1$
+ } catch (CoreException e) {
+ // let's not do anything here, we'll use the default value
+ }
+ if (commandLine != null) {
+ mEmulatorCLOptions.setText(commandLine);
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#performApply(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+ */
+ @Override
+ public void performApply(ILaunchConfigurationWorkingCopy configuration) {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
+ getCurrentTargetMode().toString());
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_REUSE_LAST_USED_DEVICE,
+ mFutureLaunchesOnSameDevice.getSelection());
+ AvdInfo avd = mPreferredAvdSelector.getSelected();
+ if (avd != null) {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, avd.getName());
+ } else {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_AVD_NAME, (String)null);
+ }
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_SPEED,
+ mSpeedCombo.getSelectionIndex());
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_DELAY,
+ mDelayCombo.getSelectionIndex());
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE,
+ mEmulatorCLOptions.getText());
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA,
+ mWipeDataButton.getSelection());
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM,
+ mNoBootAnimButton.getSelection());
+ }
+
+ private TargetMode getCurrentTargetMode() {
+ if (mAutoTargetButton.getSelection()) {
+ return TargetMode.AUTO;
+ } else if (mManualTargetButton.getSelection()) {
+ return TargetMode.MANUAL;
+ } else {
+ String selection = mDeviceTypeCombo.getText();
+ if (DEVICES_AND_EMULATORS.equals(selection)) {
+ return TargetMode.ALL_DEVICES_AND_EMULATORS;
+ } else if (DEVICES_ONLY.equals(selection)) {
+ return TargetMode.ALL_DEVICES;
+ } else if (EMULATORS_ONLY.equals(selection)) {
+ return TargetMode.ALL_EMULATORS;
+ }
+ }
+
+ return TargetMode.AUTO;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#setDefaults(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+ */
+ @Override
+ public void setDefaults(ILaunchConfigurationWorkingCopy configuration) {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_TARGET_MODE,
+ LaunchConfigDelegate.DEFAULT_TARGET_MODE.toString());
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_SPEED,
+ LaunchConfigDelegate.DEFAULT_SPEED);
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_DELAY,
+ LaunchConfigDelegate.DEFAULT_DELAY);
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_WIPE_DATA,
+ LaunchConfigDelegate.DEFAULT_WIPE_DATA);
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_NO_BOOT_ANIM,
+ LaunchConfigDelegate.DEFAULT_NO_BOOT_ANIM);
+
+ IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore();
+ String emuOptions = store.getString(AdtPrefs.PREFS_EMU_OPTIONS);
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_COMMANDLINE, emuOptions);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/IAndroidLaunchAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/IAndroidLaunchAction.java
new file mode 100644
index 000000000..1ec46b603
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/IAndroidLaunchAction.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.ddmlib.IDevice;
+
+import java.util.Collection;
+
+/**
+ * An action to perform after performing a launch of an Android application
+ */
+public interface IAndroidLaunchAction {
+
+ /**
+ * Do the launch
+ *
+ * @param info the {@link DelayedLaunchInfo} that contains launch details
+ * @param devices Android devices on which the action will be performed
+ * @returns true if launch was successfully, and controller should wait for debugger to attach
+ * (if applicable)
+ */
+ boolean doLaunchAction(DelayedLaunchInfo info, Collection<IDevice> devices);
+
+ /**
+ * Return a description of launch, to be used for logging and error messages
+ */
+ String getLaunchDescription();
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/ILaunchController.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/ILaunchController.java
new file mode 100644
index 000000000..6a5c00947
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/ILaunchController.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.ddmlib.IDevice;
+
+/**
+ * Interface for managing Android launches
+ */
+public interface ILaunchController {
+
+ /**
+ * Launches an application on a device or emulator
+ *
+ * @param launchInfo the {@link DelayedLaunchInfo} that indicates the launch action
+ * @param device the device or emulator to launch the application on
+ */
+ public void launchApp(DelayedLaunchInfo launchInfo, IDevice device);
+
+ /**
+ * Cancels a launch
+ *
+ * @param launchInfo the {@link DelayedLaunchInfo} to cancel
+ */
+ void stopLaunch(DelayedLaunchInfo launchInfo);
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/JUnitLaunchConfigDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/JUnitLaunchConfigDelegate.java
new file mode 100644
index 000000000..fdb1b305a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/JUnitLaunchConfigDelegate.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.FileLocator;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate;
+import org.osgi.framework.Bundle;
+
+import java.io.IOException;
+import java.net.URL;
+
+/**
+ * <p>
+ * For Android projects, android.jar gets added to the launch configuration of
+ * JUnit tests as a bootstrap entry. This breaks JUnit tests as android.jar
+ * contains a skeleton version of JUnit classes and the JVM will stop with an error similar
+ * to: <blockquote> Error occurred during initialization of VM
+ * java/lang/NoClassDefFoundError: java/lang/ref/FinalReference </blockquote>
+ * <p>
+ * At compile time, Eclipse does not know that there is no valid junit.jar in
+ * the classpath since it can find a correct reference to all the necessary
+ * org.junit.* classes in the android.jar so it does not prompt the user to add
+ * the JUnit3 or JUnit4 jar.
+ * <p>
+ * This delegates removes the android.jar from the bootstrap path and if
+ * necessary also puts back the junit.jar in the user classpath.
+ * <p>
+ * This delegate will be present for both Java and Android projects (delegates
+ * setting instead of only the current project) but the behavior for Java
+ * projects should be neutral since:
+ * <ol>
+ * <li>Java tests can only compile (and then run) when a valid junit.jar is
+ * present
+ * <li>There is no android.jar in Java projects
+ * </ol>
+ */
+public class JUnitLaunchConfigDelegate extends JUnitLaunchConfigurationDelegate {
+
+ private static final String JUNIT_JAR = "junit.jar"; //$NON-NLS-1$
+
+ @Override
+ public String[][] getBootpathExt(ILaunchConfiguration configuration) throws CoreException {
+ String[][] bootpath = super.getBootpathExt(configuration);
+ return fixBootpathExt(bootpath);
+ }
+
+ @Override
+ public String[] getClasspath(ILaunchConfiguration configuration) throws CoreException {
+ String[] classpath = super.getClasspath(configuration);
+ return fixClasspath(classpath, getJavaProjectName(configuration));
+ }
+
+ /**
+ * Removes the android.jar from the bootstrap path if present.
+ *
+ * @param bootpath Array of Arrays of bootstrap class paths
+ * @return a new modified (if applicable) bootpath
+ */
+ public static String[][] fixBootpathExt(String[][] bootpath) {
+ for (int i = 0; i < bootpath.length; i++) {
+ if (bootpath[i] != null && bootpath[i].length > 0) {
+ // we assume that the android.jar can only be present in the
+ // bootstrap path of android tests
+ if (bootpath[i][0].endsWith(SdkConstants.FN_FRAMEWORK_LIBRARY)) {
+ bootpath[i] = null;
+ }
+ }
+ }
+ return bootpath;
+ }
+
+ /**
+ * Add the junit.jar to the user classpath; since Eclipse was relying on
+ * android.jar to provide the appropriate org.junit classes, it does not
+ * know it actually needs the junit.jar.
+ *
+ * @param classpath Array containing classpath
+ * @param projectName The name of the project (for logging purposes)
+ *
+ * @return a new modified (if applicable) classpath
+ */
+ public static String[] fixClasspath(String[] classpath, String projectName) {
+ // search for junit.jar; if any are found return immediately
+ for (int i = 0; i < classpath.length; i++) {
+ if (classpath[i].endsWith(JUNIT_JAR)) {
+ return classpath;
+ }
+ }
+
+ // This delegate being called without a junit.jar present is only
+ // possible for Android projects. In a non-Android project, the test
+ // would not compile and would be unable to run.
+ try {
+ // junit4 is backward compatible with junit3 and they uses the
+ // same junit.jar from bundle org.junit:
+ // When a project has mixed JUnit3 and JUnit4 tests, if JUnit3 jar
+ // is added first it is then replaced by the JUnit4 jar when user is
+ // prompted to fix the JUnit4 test failure
+ String jarLocation = getJunitJarLocation();
+ // we extend the classpath by one element and append junit.jar
+ String[] newClasspath = new String[classpath.length + 1];
+ System.arraycopy(classpath, 0, newClasspath, 0, classpath.length);
+ newClasspath[newClasspath.length - 1] = jarLocation;
+ classpath = newClasspath;
+ } catch (IOException e) {
+ // This should not happen as we depend on the org.junit
+ // plugin explicitly; the error is logged here so that the user can
+ // trace back the cause when the test fails to run
+ AdtPlugin.log(e, "Could not find a valid junit.jar");
+ AdtPlugin.printErrorToConsole(projectName,
+ "Could not find a valid junit.jar");
+ // Return the classpath as-is (with no junit.jar) anyway because we
+ // will let the actual launch config fails.
+ }
+
+ return classpath;
+ }
+
+ /**
+ * Returns the path of the junit jar in the highest version bundle.
+ *
+ * (This is public only so that the test can call it)
+ *
+ * @return the path as a string
+ * @throws IOException
+ */
+ public static String getJunitJarLocation() throws IOException {
+ Bundle bundle = Platform.getBundle("org.junit"); //$NON-NLS-1$
+ if (bundle == null) {
+ throw new IOException("Cannot find org.junit bundle");
+ }
+ URL jarUrl = bundle.getEntry(AdtConstants.WS_SEP + JUNIT_JAR);
+ return FileLocator.resolve(jarUrl).getFile();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigDelegate.java
new file mode 100644
index 000000000..86fc2ffae
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigDelegate.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.common.xml.ManifestData.Activity;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration.TargetMode;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.debug.core.ILaunch;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.model.LaunchConfigurationDelegate;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+
+/**
+ * Implementation of an eclipse LauncConfigurationDelegate to launch android
+ * application in debug.
+ */
+public class LaunchConfigDelegate extends LaunchConfigurationDelegate {
+ final static int INVALID_DEBUG_PORT = -1;
+
+ public final static String ANDROID_LAUNCH_TYPE_ID =
+ "com.android.ide.eclipse.adt.debug.LaunchConfigType"; //$NON-NLS-1$
+
+ /** Target mode parameters: true is automatic, false is manual */
+ public static final String ATTR_TARGET_MODE = AdtPlugin.PLUGIN_ID + ".target"; //$NON-NLS-1$
+ public static final TargetMode DEFAULT_TARGET_MODE = TargetMode.AUTO;
+
+ /** Flag indicating whether the last used device should be used for future launches. */
+ public static final String ATTR_REUSE_LAST_USED_DEVICE =
+ AdtPlugin.PLUGIN_ID + ".reuse.last.used.device"; //$NON-NLS-1$
+
+ /** Device on which the last launch happened. */
+ public static final String ATTR_LAST_USED_DEVICE =
+ AdtPlugin.PLUGIN_ID + ".last.used.device"; //$NON-NLS-1$
+
+ /**
+ * Launch action:
+ * <ul>
+ * <li>0: launch default activity</li>
+ * <li>1: launch specified activity. See {@link #ATTR_ACTIVITY}</li>
+ * <li>2: Do Nothing</li>
+ * </ul>
+ */
+ public final static String ATTR_LAUNCH_ACTION = AdtPlugin.PLUGIN_ID + ".action"; //$NON-NLS-1$
+
+ /** Default launch action. This launches the activity that is setup to be found in the HOME
+ * screen.
+ */
+ public final static int ACTION_DEFAULT = 0;
+ /** Launch action starting a specific activity. */
+ public final static int ACTION_ACTIVITY = 1;
+ /** Launch action that does nothing. */
+ public final static int ACTION_DO_NOTHING = 2;
+ /** Default launch action value. */
+ public final static int DEFAULT_LAUNCH_ACTION = ACTION_DEFAULT;
+
+ /**
+ * Activity to be launched if {@link #ATTR_LAUNCH_ACTION} is 1
+ */
+ public static final String ATTR_ACTIVITY = AdtPlugin.PLUGIN_ID + ".activity"; //$NON-NLS-1$
+
+ public static final String ATTR_AVD_NAME = AdtPlugin.PLUGIN_ID + ".avd"; //$NON-NLS-1$
+
+ public static final String ATTR_SPEED = AdtPlugin.PLUGIN_ID + ".speed"; //$NON-NLS-1$
+
+ /**
+ * Index of the default network speed setting for the emulator.<br>
+ * Get the emulator option with <code>EmulatorConfigTab.getSpeed(index)</code>
+ */
+ public static final int DEFAULT_SPEED = 0;
+
+ public static final String ATTR_DELAY = AdtPlugin.PLUGIN_ID + ".delay"; //$NON-NLS-1$
+
+ /**
+ * Index of the default network latency setting for the emulator.<br>
+ * Get the emulator option with <code>EmulatorConfigTab.getDelay(index)</code>
+ */
+ public static final int DEFAULT_DELAY = 0;
+
+ public static final String ATTR_COMMANDLINE = AdtPlugin.PLUGIN_ID + ".commandline"; //$NON-NLS-1$
+
+ public static final String ATTR_WIPE_DATA = AdtPlugin.PLUGIN_ID + ".wipedata"; //$NON-NLS-1$
+ public static final boolean DEFAULT_WIPE_DATA = false;
+
+ public static final String ATTR_NO_BOOT_ANIM = AdtPlugin.PLUGIN_ID + ".nobootanim"; //$NON-NLS-1$
+ public static final boolean DEFAULT_NO_BOOT_ANIM = false;
+
+ public static final String ATTR_DEBUG_PORT =
+ AdtPlugin.PLUGIN_ID + ".debugPort"; //$NON-NLS-1$
+
+ @Override
+ public void launch(ILaunchConfiguration configuration, String mode,
+ ILaunch launch, IProgressMonitor monitor) throws CoreException {
+ // We need to check if it's a standard launch or if it's a launch
+ // to debug an application already running.
+ int debugPort = AndroidLaunchController.getPortForConfig(configuration);
+
+ // get the project
+ IProject project = getProject(configuration);
+
+ // first we make sure the launch is of the proper type
+ AndroidLaunch androidLaunch = null;
+ if (launch instanceof AndroidLaunch) {
+ androidLaunch = (AndroidLaunch)launch;
+ } else {
+ // wrong type, not sure how we got there, but we don't do
+ // anything else
+ AdtPlugin.printErrorToConsole(project, "Wrong Launch Type!");
+ return;
+ }
+
+ // if we have a valid debug port, this means we're debugging an app
+ // that's already launched.
+ if (debugPort != INVALID_DEBUG_PORT) {
+ AndroidLaunchController.launchRemoteDebugger(debugPort, androidLaunch, monitor);
+ return;
+ }
+
+ if (project == null) {
+ AdtPlugin.printErrorToConsole("Couldn't get project object!");
+ androidLaunch.stopLaunch();
+ return;
+ }
+
+ // make sure the project and its dependencies are built
+ // and PostCompilerBuilder runs.
+ // This is a synchronous call which returns when the
+ // build is done.
+ ProjectHelper.doFullIncrementalDebugBuild(project, monitor);
+
+ // check if the project has errors, and abort in this case.
+ if (ProjectHelper.hasError(project, true)) {
+ AdtPlugin.displayError("Android Launch",
+ "Your project contains error(s), please fix them before running your application.");
+ return;
+ }
+
+ AdtPlugin.printToConsole(project, "------------------------------"); //$NON-NLS-1$
+ AdtPlugin.printToConsole(project, "Android Launch!");
+
+ // check if the project is using the proper sdk.
+ // if that throws an exception, we simply let it propagate to the caller.
+ if (checkAndroidProject(project) == false) {
+ AdtPlugin.printErrorToConsole(project, "Project is not an Android Project. Aborting!");
+ androidLaunch.stopLaunch();
+ return;
+ }
+
+ // Check adb status and abort if needed.
+ AndroidDebugBridge bridge = AndroidDebugBridge.getBridge();
+ if (bridge == null || bridge.isConnected() == false) {
+ try {
+ int connections = -1;
+ int restarts = -1;
+ if (bridge != null) {
+ connections = bridge.getConnectionAttemptCount();
+ restarts = bridge.getRestartAttemptCount();
+ }
+
+ // if we get -1, the device monitor is not even setup (anymore?).
+ // We need to ask the user to restart eclipse.
+ // This shouldn't happen, but it's better to let the user know in case it does.
+ if (connections == -1 || restarts == -1) {
+ AdtPlugin.printErrorToConsole(project,
+ "The connection to adb is down, and a severe error has occured.",
+ "You must restart adb and Eclipse.",
+ String.format(
+ "Please ensure that adb is correctly located at '%1$s' and can be executed.",
+ AdtPlugin.getOsAbsoluteAdb()));
+ return;
+ }
+
+ if (restarts == 0) {
+ AdtPlugin.printErrorToConsole(project,
+ "Connection with adb was interrupted.",
+ String.format("%1$s attempts have been made to reconnect.", connections),
+ "You may want to manually restart adb from the Devices view.");
+ } else {
+ AdtPlugin.printErrorToConsole(project,
+ "Connection with adb was interrupted, and attempts to reconnect have failed.",
+ String.format("%1$s attempts have been made to restart adb.", restarts),
+ "You may want to manually restart adb from the Devices view.");
+
+ }
+ return;
+ } finally {
+ androidLaunch.stopLaunch();
+ }
+ }
+
+ // since adb is working, we let the user know
+ // TODO have a verbose mode for launch with more info (or some of the less useful info we now have).
+ AdtPlugin.printToConsole(project, "adb is running normally.");
+
+ // make a config class
+ AndroidLaunchConfiguration config = new AndroidLaunchConfiguration();
+
+ // fill it with the config coming from the ILaunchConfiguration object
+ config.set(configuration);
+
+ // get the launch controller singleton
+ AndroidLaunchController controller = AndroidLaunchController.getInstance();
+
+ // get the application package
+ IFile applicationPackage = ProjectHelper.getApplicationPackage(project);
+ if (applicationPackage == null) {
+ androidLaunch.stopLaunch();
+ return;
+ }
+
+ // we need some information from the manifest
+ ManifestData manifestData = AndroidManifestHelper.parseForData(project);
+
+ if (manifestData == null) {
+ AdtPlugin.printErrorToConsole(project, "Failed to parse AndroidManifest: aborting!");
+ androidLaunch.stopLaunch();
+ return;
+ }
+
+ doLaunch(configuration, mode, monitor, project, androidLaunch, config, controller,
+ applicationPackage, manifestData);
+ }
+
+ protected void doLaunch(ILaunchConfiguration configuration, String mode,
+ IProgressMonitor monitor, IProject project, AndroidLaunch androidLaunch,
+ AndroidLaunchConfiguration config, AndroidLaunchController controller,
+ IFile applicationPackage, ManifestData manifestData) {
+
+ String activityName = null;
+
+ if (config.mLaunchAction == ACTION_ACTIVITY) {
+ // Get the activity name defined in the config
+ activityName = getActivityName(configuration);
+
+ // Get the full activity list and make sure the one we got matches.
+ Activity[] activities = manifestData.getActivities();
+
+ // first we check that there are, in fact, activities.
+ if (activities.length == 0) {
+ // if the activities list is null, then the manifest is empty
+ // and we can't launch the app. We'll revert to a sync-only launch
+ AdtPlugin.printErrorToConsole(project,
+ "The Manifest defines no activity!",
+ "The launch will only sync the application package on the device!");
+ config.mLaunchAction = ACTION_DO_NOTHING;
+ } else if (activityName == null) {
+ // if the activity we got is null, we look for the default one.
+ AdtPlugin.printErrorToConsole(project,
+ "No activity specified! Getting the launcher activity.");
+ Activity launcherActivity = manifestData.getLauncherActivity();
+ if (launcherActivity != null) {
+ activityName = launcherActivity.getName();
+ }
+
+ // if there's no default activity. We revert to a sync-only launch.
+ if (activityName == null) {
+ revertToNoActionLaunch(project, config);
+ }
+ } else {
+
+ // check the one we got from the config matches any from the list
+ boolean match = false;
+ for (Activity a : activities) {
+ if (a != null && a.getName().equals(activityName)) {
+ match = true;
+ break;
+ }
+ }
+
+ // if we didn't find a match, we revert to the default activity if any.
+ if (match == false) {
+ AdtPlugin.printErrorToConsole(project,
+ "The specified activity does not exist! Getting the launcher activity.");
+ Activity launcherActivity = manifestData.getLauncherActivity();
+ if (launcherActivity != null) {
+ activityName = launcherActivity.getName();
+ } else {
+ // if there's no default activity. We revert to a sync-only launch.
+ revertToNoActionLaunch(project, config);
+ }
+ }
+ }
+ } else if (config.mLaunchAction == ACTION_DEFAULT) {
+ Activity launcherActivity = manifestData.getLauncherActivity();
+ if (launcherActivity != null) {
+ activityName = launcherActivity.getName();
+ }
+
+ // if there's no default activity. We revert to a sync-only launch.
+ if (activityName == null) {
+ revertToNoActionLaunch(project, config);
+ }
+ }
+
+ IAndroidLaunchAction launchAction = null;
+ if (config.mLaunchAction == ACTION_DO_NOTHING || activityName == null) {
+ launchAction = new EmptyLaunchAction();
+ } else {
+ launchAction = new ActivityLaunchAction(activityName, controller);
+ }
+
+ // everything seems fine, we ask the launch controller to handle
+ // the rest
+ controller.launch(project, mode, applicationPackage,manifestData.getPackage(),
+ manifestData.getPackage(), manifestData.getDebuggable(),
+ manifestData.getMinSdkVersionString(), launchAction, config, androidLaunch,
+ monitor);
+ }
+
+ @Override
+ public boolean buildForLaunch(ILaunchConfiguration configuration,
+ String mode, IProgressMonitor monitor) throws CoreException {
+ // if this returns true, this forces a full workspace rebuild which is not
+ // what we want.
+ // Instead in the #launch method, we'll rebuild only the launching project.
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws CoreException
+ */
+ @Override
+ public ILaunch getLaunch(ILaunchConfiguration configuration, String mode)
+ throws CoreException {
+ return new AndroidLaunch(configuration, mode, null);
+ }
+
+ /**
+ * Returns the IProject object matching the name found in the configuration
+ * object under the name
+ * <code>IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME</code>
+ * @param configuration
+ * @return The IProject object or null
+ */
+ private IProject getProject(ILaunchConfiguration configuration){
+ // get the project name from the config
+ String projectName;
+ try {
+ projectName = configuration.getAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, "");
+ } catch (CoreException e) {
+ return null;
+ }
+
+ // get the current workspace
+ IWorkspace workspace = ResourcesPlugin.getWorkspace();
+
+ // and return the project with the name from the config
+ return workspace.getRoot().getProject(projectName);
+ }
+
+ /**
+ * Checks the project is an android project.
+ * @param project The project to check
+ * @return true if the project is an android SDK.
+ * @throws CoreException
+ */
+ private boolean checkAndroidProject(IProject project) throws CoreException {
+ // check if the project is a java and an android project.
+ if (project.hasNature(JavaCore.NATURE_ID) == false) {
+ String msg = String.format("%1$s is not a Java project!", project.getName());
+ AdtPlugin.displayError("Android Launch", msg);
+ return false;
+ }
+
+ if (project.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
+ String msg = String.format("%1$s is not an Android project!", project.getName());
+ AdtPlugin.displayError("Android Launch", msg);
+ return false;
+ }
+
+ return true;
+ }
+
+
+ /**
+ * Returns the name of the activity.
+ */
+ private String getActivityName(ILaunchConfiguration configuration) {
+ String empty = "";
+ String activityName;
+ try {
+ activityName = configuration.getAttribute(ATTR_ACTIVITY, empty);
+ } catch (CoreException e) {
+ return null;
+ }
+
+ return (activityName != empty) ? activityName : null;
+ }
+
+ private final void revertToNoActionLaunch(IProject project, AndroidLaunchConfiguration config) {
+ AdtPlugin.printErrorToConsole(project,
+ "No Launcher activity found!",
+ "The launch will only sync the application package on the device!");
+ config.mLaunchAction = ACTION_DO_NOTHING;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigTabGroup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigTabGroup.java
new file mode 100644
index 000000000..fbf17ce63
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchConfigTabGroup.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup;
+import org.eclipse.debug.ui.CommonTab;
+import org.eclipse.debug.ui.ILaunchConfigurationDialog;
+import org.eclipse.debug.ui.ILaunchConfigurationTab;
+
+/**
+ * Tab group object for Android Launch Config type.
+ */
+public class LaunchConfigTabGroup extends AbstractLaunchConfigurationTabGroup {
+
+ public LaunchConfigTabGroup() {
+ }
+
+ @Override
+ public void createTabs(ILaunchConfigurationDialog dialog, String mode) {
+ ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] {
+ new MainLaunchConfigTab(),
+ new EmulatorConfigTab(ILaunchManager.RUN_MODE.equals(mode)),
+ new CommonTab()
+ };
+ setTabs(tabs);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchMessages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchMessages.java
new file mode 100644
index 000000000..1fd2b5aa1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchMessages.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import org.eclipse.osgi.util.NLS;
+
+public class LaunchMessages extends NLS {
+ private static final String BUNDLE_NAME = "com.android.ide.eclipse.adt.internal.launch.messages"; //$NON-NLS-1$
+
+ // generic messages that could be used by multiple classes
+ public static String LaunchDialogTitle;
+ public static String NonAndroidProjectError;
+ public static String ParseFileFailure_s;
+
+ // specialized, class-specific messages
+ public static String AndroidJUnitLaunchAction_LaunchDesc_s;
+ public static String AndroidJUnitLaunchAction_LaunchFail;
+ public static String AndroidJUnitLaunchAction_LaunchInstr_2s;
+ public static String AndroidJUnitDelegate_NoRunnerConfigMsg_s;
+ public static String AndroidJUnitDelegate_NoRunnerConsoleMsg_4s;
+ public static String AndroidJUnitDelegate_NoRunnerMsg_s;
+ public static String AndroidJUnitDelegate_NoTargetMsg_3s;
+ public static String AndroidJUnitTab_LoaderLabel;
+ public static String AndroidJUnitTab_LoadInstrError_s;
+ public static String AndroidJUnitTab_NoRunnerError;
+ public static String AndroidJUnitTab_SizeLabel;
+ public static String AndroidJUnitTab_TestContainerText;
+ public static String InstrValidator_NoTestLibMsg_s;
+ public static String InstrValidator_WrongRunnerTypeMsg_s;
+ public static String RemoteAdtTestRunner_RunCompleteMsg;
+ public static String RemoteAdtTestRunner_RunFailedMsg_s;
+
+ public static String RemoteAdtTestRunner_RunIOException_s;
+ public static String RemoteAdtTestRunner_RunTimeoutException;
+ public static String RemoteAdtTestRunner_RunAdbCommandRejectedException_s;
+ public static String RemoteAdtTestRunner_RunShellCommandUnresponsiveException;
+ public static String RemoteAdtTestRunner_RunStoppedMsg;
+
+ static {
+ // initialize resource bundle
+ NLS.initializeMessages(BUNDLE_NAME, LaunchMessages.class);
+ }
+
+ private LaunchMessages() {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchShortcut.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchShortcut.java
new file mode 100644
index 000000000..bb02b29b6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/LaunchShortcut.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.ui.DebugUITools;
+import org.eclipse.debug.ui.ILaunchShortcut;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.PlatformUI;
+
+/**
+ * Launch shortcut to launch debug/run configuration directly.
+ */
+public class LaunchShortcut implements ILaunchShortcut {
+
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchShortcut#launch(
+ * org.eclipse.jface.viewers.ISelection, java.lang.String)
+ */
+ @Override
+ public void launch(ISelection selection, String mode) {
+ if (selection instanceof IStructuredSelection) {
+
+ // get the object and the project from it
+ IStructuredSelection structSelect = (IStructuredSelection)selection;
+ Object o = structSelect.getFirstElement();
+
+ // get the first (and normally only) element
+ if (o instanceof IAdaptable) {
+ IResource r = (IResource)((IAdaptable)o).getAdapter(IResource.class);
+
+ // get the project from the resource
+ if (r != null) {
+ IProject project = r.getProject();
+
+ if (project != null) {
+ ProjectState state = Sdk.getProjectState(project);
+ if (state != null && state.isLibrary()) {
+
+ MessageDialog.openError(
+ PlatformUI.getWorkbench().getDisplay().getActiveShell(),
+ "Android Launch",
+ "Android library projects cannot be launched.");
+ } else{
+ // and launch
+ launch(project, mode);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchShortcut#launch(
+ * org.eclipse.ui.IEditorPart, java.lang.String)
+ */
+ @Override
+ public void launch(IEditorPart editor, String mode) {
+ // since we force the shortcut to only work on selection in the
+ // package explorer, this will never be called.
+ }
+
+
+ /**
+ * Launch a config for the specified project.
+ * @param project The project to launch
+ * @param mode The launch mode ("debug", "run" or "profile")
+ */
+ private void launch(IProject project, String mode) {
+ // get an existing or new launch configuration
+ ILaunchConfiguration config = AndroidLaunchController.getLaunchConfig(project,
+ LaunchConfigDelegate.ANDROID_LAUNCH_TYPE_ID);
+
+ if (config != null) {
+ // and launch!
+ DebugUITools.launch(config, mode);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/MainLaunchConfigTab.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/MainLaunchConfigTab.java
new file mode 100644
index 000000000..c66458fc9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/MainLaunchConfigTab.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch;
+
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.common.xml.ManifestData.Activity;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper.IProjectChooserFilter;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper.NonLibraryProjectOnlyFilter;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
+import org.eclipse.debug.ui.ILaunchConfigurationTab;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Text;
+
+import java.util.ArrayList;
+
+/**
+ * Class for the main launch configuration tab.
+ */
+public class MainLaunchConfigTab extends AbstractLaunchConfigurationTab {
+
+ /**
+ *
+ */
+ public static final String LAUNCH_TAB_IMAGE = "mainLaunchTab"; //$NON-NLS-1$
+
+ protected static final String EMPTY_STRING = ""; //$NON-NLS-1$
+
+ protected Text mProjText;
+ private Button mProjButton;
+
+ private Combo mActivityCombo;
+ private final ArrayList<Activity> mActivities = new ArrayList<Activity>();
+
+ private WidgetListener mListener = new WidgetListener();
+
+ private Button mDefaultActionButton;
+ private Button mActivityActionButton;
+ private Button mDoNothingActionButton;
+ private int mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;
+
+ private ProjectChooserHelper mProjectChooserHelper;
+
+ /**
+ * A listener which handles widget change events for the controls in this
+ * tab.
+ */
+ private class WidgetListener implements ModifyListener, SelectionListener {
+
+ @Override
+ public void modifyText(ModifyEvent e) {
+ IProject project = checkParameters();
+ loadActivities(project);
+ setDirty(true);
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {/* do nothing */
+ }
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ Object source = e.getSource();
+ if (source == mProjButton) {
+ handleProjectButtonSelected();
+ } else {
+ checkParameters();
+ }
+ }
+ }
+
+ public MainLaunchConfigTab() {
+ }
+
+ protected IProjectChooserFilter getProjectFilter() {
+ return new NonLibraryProjectOnlyFilter();
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ mProjectChooserHelper = new ProjectChooserHelper(parent.getShell(), getProjectFilter());
+
+ Font font = parent.getFont();
+ Composite comp = new Composite(parent, SWT.NONE);
+ setControl(comp);
+ GridLayout topLayout = new GridLayout();
+ topLayout.verticalSpacing = 0;
+ comp.setLayout(topLayout);
+ comp.setFont(font);
+ createProjectEditor(comp);
+ createVerticalSpacer(comp, 1);
+
+ // create the combo for the activity chooser
+ Group group = new Group(comp, SWT.NONE);
+ group.setText("Launch Action:");
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ group.setLayoutData(gd);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 2;
+ group.setLayout(layout);
+ group.setFont(font);
+
+ mDefaultActionButton = new Button(group, SWT.RADIO);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ mDefaultActionButton.setLayoutData(gd);
+ mDefaultActionButton.setText("Launch Default Activity");
+ mDefaultActionButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // event are received for both selection and deselection, so we only process
+ // the selection event to avoid doing it twice.
+ if (mDefaultActionButton.getSelection() == true) {
+ mLaunchAction = LaunchConfigDelegate.ACTION_DEFAULT;
+ mActivityCombo.setEnabled(false);
+ checkParameters();
+ }
+ }
+ });
+
+ mActivityActionButton = new Button(group, SWT.RADIO);
+ mActivityActionButton.setText("Launch:");
+ mActivityActionButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // event are received for both selection and deselection, so we only process
+ // the selection event to avoid doing it twice.
+ if (mActivityActionButton.getSelection() == true) {
+ mLaunchAction = LaunchConfigDelegate.ACTION_ACTIVITY;
+ mActivityCombo.setEnabled(true);
+ checkParameters();
+ }
+ }
+ });
+
+ mActivityCombo = new Combo(group, SWT.DROP_DOWN | SWT.READ_ONLY);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ mActivityCombo.setLayoutData(gd);
+ mActivityCombo.clearSelection();
+ mActivityCombo.setEnabled(false);
+ mActivityCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ checkParameters();
+ }
+ });
+
+ mDoNothingActionButton = new Button(group, SWT.RADIO);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalSpan = 2;
+ mDoNothingActionButton.setLayoutData(gd);
+ mDoNothingActionButton.setText("Do Nothing");
+ mDoNothingActionButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ // event are received for both selection and deselection, so we only process
+ // the selection event to avoid doing it twice.
+ if (mDoNothingActionButton.getSelection() == true) {
+ mLaunchAction = LaunchConfigDelegate.ACTION_DO_NOTHING;
+ mActivityCombo.setEnabled(false);
+ checkParameters();
+ }
+ }
+ });
+
+ }
+
+ @Override
+ public String getName() {
+ return "Android";
+ }
+
+ @Override
+ public Image getImage() {
+ return IconFactory.getInstance().getIcon(LAUNCH_TAB_IMAGE);
+ }
+
+ @Override
+ public void performApply(ILaunchConfigurationWorkingCopy configuration) {
+ configuration.setAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, mProjText.getText());
+ configuration.setAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_ALLOW_TERMINATE, true);
+
+ // add the launch mode
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION, mLaunchAction);
+
+ // add the activity
+ int selection = mActivityCombo.getSelectionIndex();
+ if (mActivities != null && selection >=0 && selection < mActivities.size()) {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_ACTIVITY,
+ mActivities.get(selection).getName());
+ }
+
+ // link the project and the launch config.
+ mapResources(configuration);
+ }
+
+ @Override
+ public void setDefaults(ILaunchConfigurationWorkingCopy configuration) {
+ configuration.setAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION,
+ LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION);
+ }
+
+ /**
+ * Creates the widgets for specifying a main type.
+ *
+ * @param parent the parent composite
+ */
+ protected void createProjectEditor(Composite parent) {
+ Font font = parent.getFont();
+ Group group = new Group(parent, SWT.NONE);
+ group.setText("Project:");
+ GridData gd = new GridData(GridData.FILL_HORIZONTAL);
+ group.setLayoutData(gd);
+ GridLayout layout = new GridLayout();
+ layout.numColumns = 2;
+ group.setLayout(layout);
+ group.setFont(font);
+ mProjText = new Text(group, SWT.SINGLE | SWT.BORDER);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ mProjText.setLayoutData(gd);
+ mProjText.setFont(font);
+ mProjText.addModifyListener(mListener);
+ mProjButton = createPushButton(group, "Browse...", null);
+ mProjButton.addSelectionListener(mListener);
+ }
+
+ /**
+ * returns the default listener from this class. For all subclasses this
+ * listener will only provide the functi Jaonality of updating the current
+ * tab
+ *
+ * @return a widget listener
+ */
+ protected WidgetListener getDefaultListener() {
+ return mListener;
+ }
+
+ /**
+ * Return the {@link IJavaProject} corresponding to the project name in the project
+ * name text field, or null if the text does not match a project name.
+ * @param javaModel the Java Model object corresponding for the current workspace root.
+ * @return a IJavaProject object or null.
+ */
+ protected IJavaProject getJavaProject(IJavaModel javaModel) {
+ String projectName = mProjText.getText().trim();
+ if (projectName.length() < 1) {
+ return null;
+ }
+ return javaModel.getJavaProject(projectName);
+ }
+
+ /**
+ * Show a dialog that lets the user select a project. This in turn provides
+ * context for the main type, allowing the user to key a main type name, or
+ * constraining the search for main types to the specified project.
+ */
+ protected void handleProjectButtonSelected() {
+ IJavaProject javaProject = mProjectChooserHelper.chooseJavaProject(
+ mProjText.getText().trim(),
+ "Please select a project to launch");
+ if (javaProject == null) {
+ return;
+ }// end if
+ String projectName = javaProject.getElementName();
+ mProjText.setText(projectName);
+
+ // get the list of activities and fill the combo
+ IProject project = javaProject.getProject();
+ loadActivities(project);
+ }// end handle selected
+
+ /**
+ * Initializes this tab's controls with values from the given
+ * launch configuration. This method is called when
+ * a configuration is selected to view or edit, after this
+ * tab's control has been created.
+ *
+ * @param config launch configuration
+ *
+ * @see ILaunchConfigurationTab
+ */
+ @Override
+ public void initializeFrom(ILaunchConfiguration config) {
+ String projectName = EMPTY_STRING;
+ try {
+ projectName = config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ EMPTY_STRING);
+ }// end try
+ catch (CoreException ce) {
+ }
+ mProjText.setText(projectName);
+
+ IProject proj = mProjectChooserHelper.getAndroidProject(projectName);
+ loadActivities(proj);
+
+ // load the launch action.
+ mLaunchAction = LaunchConfigDelegate.DEFAULT_LAUNCH_ACTION;
+ try {
+ mLaunchAction = config.getAttribute(LaunchConfigDelegate.ATTR_LAUNCH_ACTION,
+ mLaunchAction);
+ } catch (CoreException e) {
+ // nothing to be done really. launchAction will keep its default value.
+ }
+
+ mDefaultActionButton.setSelection(mLaunchAction == LaunchConfigDelegate.ACTION_DEFAULT);
+ mActivityActionButton.setSelection(mLaunchAction == LaunchConfigDelegate.ACTION_ACTIVITY);
+ mDoNothingActionButton.setSelection(
+ mLaunchAction == LaunchConfigDelegate.ACTION_DO_NOTHING);
+
+ // now look for the activity and load it if present, otherwise, revert
+ // to the current one.
+ String activityName = EMPTY_STRING;
+ try {
+ activityName = config.getAttribute(LaunchConfigDelegate.ATTR_ACTIVITY, EMPTY_STRING);
+ }// end try
+ catch (CoreException ce) {
+ // nothing to be done really. activityName will stay empty
+ }
+
+ if (mLaunchAction != LaunchConfigDelegate.ACTION_ACTIVITY) {
+ mActivityCombo.setEnabled(false);
+ mActivityCombo.clearSelection();
+ } else {
+ mActivityCombo.setEnabled(true);
+ if (activityName == null || activityName.equals(EMPTY_STRING)) {
+ mActivityCombo.clearSelection();
+ } else if (mActivities != null && mActivities.size() > 0) {
+ // look for the name of the activity in the combo.
+ boolean found = false;
+ for (int i = 0 ; i < mActivities.size() ; i++) {
+ if (activityName.equals(mActivities.get(i).getName())) {
+ found = true;
+ mActivityCombo.select(i);
+ break;
+ }
+ }
+
+ // if we haven't found a matching activity we clear the combo selection
+ if (found == false) {
+ mActivityCombo.clearSelection();
+ }
+ }
+ }
+ }
+
+ /**
+ * Associates the launch config and the project. This allows Eclipse to delete the launch
+ * config when the project is deleted.
+ *
+ * @param config the launch config working copy.
+ */
+ protected void mapResources(ILaunchConfigurationWorkingCopy config) {
+ // get the java model
+ IWorkspaceRoot workspaceRoot = ResourcesPlugin.getWorkspace().getRoot();
+ IJavaModel javaModel = JavaCore.create(workspaceRoot);
+
+ // get the IJavaProject described by the text field.
+ IJavaProject javaProject = getJavaProject(javaModel);
+ IResource[] resources = null;
+ if (javaProject != null) {
+ resources = AndroidLaunchController.getResourcesToMap(javaProject.getProject());
+ }
+ config.setMappedResources(resources);
+ }
+
+ /**
+ * Loads the ui with the activities of the specified project, and stores the
+ * activities in <code>mActivities</code>.
+ * <p/>
+ * First activity is selected by default if present.
+ *
+ * @param project The project to load the activities from.
+ */
+ private void loadActivities(IProject project) {
+ if (project != null) {
+ // parse the manifest for the list of activities.
+ ManifestData manifestData = AndroidManifestHelper.parseForData(project);
+ if (manifestData != null) {
+ Activity[] activities = manifestData.getActivities();
+
+ mActivities.clear();
+ mActivityCombo.removeAll();
+
+ for (Activity activity : activities) {
+ if (activity.isExported() && activity.hasAction()) {
+ mActivities.add(activity);
+ mActivityCombo.add(activity.getName());
+ }
+ }
+
+ if (mActivities.size() > 0) {
+ if (mLaunchAction == LaunchConfigDelegate.ACTION_ACTIVITY) {
+ mActivityCombo.setEnabled(true);
+ }
+ } else {
+ mActivityCombo.setEnabled(false);
+ }
+
+ // the selection will be set when we update the ui from the current
+ // config object.
+ mActivityCombo.clearSelection();
+
+ return;
+ }
+ }
+
+ // if we reach this point, either project is null, or we got an exception during
+ // the parsing. In either case, we empty the activity list.
+ mActivityCombo.removeAll();
+ mActivities.clear();
+ }
+
+ /**
+ * Checks the parameters for correctness, and update the error message and buttons.
+ * @return the current IProject of this launch config.
+ */
+ private IProject checkParameters() {
+ try {
+ //test the project name first!
+ String text = mProjText.getText();
+ if (text.length() == 0) {
+ setErrorMessage("Project Name is required!");
+ } else if (text.matches("[a-zA-Z0-9_ \\.-]+") == false) {
+ setErrorMessage("Project name contains unsupported characters!");
+ } else {
+ IJavaProject[] projects = mProjectChooserHelper.getAndroidProjects(null);
+ IProject found = null;
+ for (IJavaProject javaProject : projects) {
+ if (javaProject.getProject().getName().equals(text)) {
+ found = javaProject.getProject();
+ break;
+ }
+
+ }
+
+ if (found != null) {
+ setErrorMessage(null);
+ } else {
+ setErrorMessage(String.format("There is no android project named '%1$s'",
+ text));
+ }
+
+ return found;
+ }
+ } finally {
+ updateLaunchConfigurationDialog();
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchAction.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchAction.java
new file mode 100644
index 000000000..c773ab9ba
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchAction.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.launch.junit;
+
+import com.android.ddmlib.IDevice;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.launch.DelayedLaunchInfo;
+import com.android.ide.eclipse.adt.internal.launch.IAndroidLaunchAction;
+import com.android.ide.eclipse.adt.internal.launch.LaunchMessages;
+import com.android.ide.eclipse.adt.internal.launch.junit.runtime.AndroidJUnitLaunchInfo;
+import com.android.ide.eclipse.adt.internal.launch.junit.runtime.RemoteAdtTestRunner;
+import com.google.common.base.Joiner;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.debug.core.ILaunch;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.core.model.IProcess;
+import org.eclipse.debug.core.model.IStreamsProxy;
+import org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate;
+import org.eclipse.jdt.launching.IVMRunner;
+import org.eclipse.jdt.launching.VMRunnerConfiguration;
+import org.eclipse.swt.widgets.Display;
+
+import java.util.Collection;
+
+/**
+ * A launch action that executes a instrumentation test run on an Android device.
+ */
+class AndroidJUnitLaunchAction implements IAndroidLaunchAction {
+ private static final Joiner JOINER = Joiner.on(',').skipNulls();
+ private final AndroidJUnitLaunchInfo mLaunchInfo;
+
+ /**
+ * Creates a AndroidJUnitLaunchAction.
+ *
+ * @param launchInfo the {@link AndroidJUnitLaunchInfo} for the JUnit run
+ */
+ public AndroidJUnitLaunchAction(AndroidJUnitLaunchInfo launchInfo) {
+ mLaunchInfo = launchInfo;
+ }
+
+ /**
+ * Launch a instrumentation test run on given Android devices.
+ * Reuses JDT JUnit launch delegate so results can be communicated back to JDT JUnit UI.
+ * <p/>
+ * Note: Must be executed on non-UI thread.
+ *
+ * @see IAndroidLaunchAction#doLaunchActions(DelayedLaunchInfo, IDevice)
+ */
+ @Override
+ public boolean doLaunchAction(DelayedLaunchInfo info, Collection<IDevice> devices) {
+ String msg = String.format(LaunchMessages.AndroidJUnitLaunchAction_LaunchInstr_2s,
+ mLaunchInfo.getRunner(), JOINER.join(devices));
+ AdtPlugin.printToConsole(info.getProject(), msg);
+
+ try {
+ mLaunchInfo.setDebugMode(info.isDebugMode());
+ mLaunchInfo.setDevices(devices);
+ JUnitLaunchDelegate junitDelegate = new JUnitLaunchDelegate(mLaunchInfo);
+ final String mode = info.isDebugMode() ? ILaunchManager.DEBUG_MODE :
+ ILaunchManager.RUN_MODE;
+
+ junitDelegate.launch(info.getLaunch().getLaunchConfiguration(), mode, info.getLaunch(),
+ info.getMonitor());
+
+ // TODO: need to add AMReceiver-type functionality somewhere
+ } catch (CoreException e) {
+ AdtPlugin.printErrorToConsole(info.getProject(),
+ LaunchMessages.AndroidJUnitLaunchAction_LaunchFail);
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLaunchDescription() {
+ return String.format(LaunchMessages.AndroidJUnitLaunchAction_LaunchDesc_s,
+ mLaunchInfo.getRunner());
+ }
+
+ /**
+ * Extends the JDT JUnit launch delegate to allow for JUnit UI reuse.
+ */
+ private static class JUnitLaunchDelegate extends JUnitLaunchConfigurationDelegate {
+
+ private AndroidJUnitLaunchInfo mLaunchInfo;
+
+ public JUnitLaunchDelegate(AndroidJUnitLaunchInfo launchInfo) {
+ mLaunchInfo = launchInfo;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#launch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.debug.core.ILaunch, org.eclipse.core.runtime.IProgressMonitor)
+ */
+ @Override
+ public synchronized void launch(ILaunchConfiguration configuration, String mode,
+ ILaunch launch, IProgressMonitor monitor) throws CoreException {
+ // TODO: is progress monitor adjustment needed here?
+ super.launch(configuration, mode, launch, monitor);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationDelegate#verifyMainTypeName(org.eclipse.debug.core.ILaunchConfiguration)
+ */
+ @Override
+ public String verifyMainTypeName(ILaunchConfiguration configuration) {
+ return "com.android.ide.eclipse.adt.junit.internal.runner.RemoteAndroidTestRunner"; //$NON-NLS-1$
+ }
+
+ /**
+ * Overrides parent to return a VM Runner implementation which launches a thread, rather
+ * than a separate VM process
+ */
+ @Override
+ public IVMRunner getVMRunner(ILaunchConfiguration configuration, String mode) {
+ return new VMTestRunner(mLaunchInfo);
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.eclipse.debug.core.model.LaunchConfigurationDelegate#getLaunch(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String)
+ */
+ @Override
+ public ILaunch getLaunch(ILaunchConfiguration configuration, String mode) {
+ return mLaunchInfo.getLaunch();
+ }
+ }
+
+ /**
+ * Provides a VM runner implementation which starts a inline implementation of a launch process
+ */
+ private static class VMTestRunner implements IVMRunner {
+
+ private final AndroidJUnitLaunchInfo mJUnitInfo;
+
+ VMTestRunner(AndroidJUnitLaunchInfo info) {
+ mJUnitInfo = info;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @throws CoreException
+ */
+ @Override
+ public void run(final VMRunnerConfiguration config, ILaunch launch,
+ IProgressMonitor monitor) throws CoreException {
+
+ TestRunnerProcess runnerProcess =
+ new TestRunnerProcess(config, mJUnitInfo);
+ launch.addProcess(runnerProcess);
+ runnerProcess.run();
+ }
+ }
+
+ /**
+ * Launch process that executes the tests.
+ */
+ private static class TestRunnerProcess implements IProcess {
+
+ private final VMRunnerConfiguration mRunConfig;
+ private final AndroidJUnitLaunchInfo mJUnitInfo;
+ private RemoteAdtTestRunner mTestRunner = null;
+ private boolean mIsTerminated = false;
+
+ TestRunnerProcess(VMRunnerConfiguration runConfig, AndroidJUnitLaunchInfo info) {
+ mRunConfig = runConfig;
+ mJUnitInfo = info;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getAttribute(java.lang.String)
+ */
+ @Override
+ public String getAttribute(String key) {
+ return null;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.eclipse.debug.core.model.IProcess#getExitValue()
+ */
+ @Override
+ public int getExitValue() {
+ return 0;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getLabel()
+ */
+ @Override
+ public String getLabel() {
+ return mJUnitInfo.getLaunch().getLaunchMode();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getLaunch()
+ */
+ @Override
+ public ILaunch getLaunch() {
+ return mJUnitInfo.getLaunch();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#getStreamsProxy()
+ */
+ @Override
+ public IStreamsProxy getStreamsProxy() {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.IProcess#setAttribute(java.lang.String,
+ * java.lang.String)
+ */
+ @Override
+ public void setAttribute(String key, String value) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
+ */
+ @Override
+ public Object getAdapter(Class adapter) {
+ return null;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.ITerminate#canTerminate()
+ */
+ @Override
+ public boolean canTerminate() {
+ return true;
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.core.model.ITerminate#isTerminated()
+ */
+ @Override
+ public boolean isTerminated() {
+ return mIsTerminated;
+ }
+
+ /**
+ * {@inheritDoc}
+ * @see org.eclipse.debug.core.model.ITerminate#terminate()
+ */
+ @Override
+ public void terminate() {
+ if (mTestRunner != null) {
+ mTestRunner.terminate();
+ }
+ mIsTerminated = true;
+ }
+
+ /**
+ * Launches a test runner that will communicate results back to JDT JUnit UI.
+ * <p/>
+ * Must be executed on a non-UI thread.
+ */
+ public void run() {
+ if (Display.getCurrent() != null) {
+ AdtPlugin.log(IStatus.ERROR, "Adt test runner executed on UI thread");
+ AdtPlugin.printErrorToConsole(mJUnitInfo.getProject(),
+ "Test launch failed due to internal error: Running tests on UI thread");
+ terminate();
+ return;
+ }
+ mTestRunner = new RemoteAdtTestRunner();
+ mTestRunner.runTests(mRunConfig.getProgramArguments(), mJUnitInfo);
+ }
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigDelegate.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigDelegate.java
new file mode 100755
index 000000000..6e47ce91c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigDelegate.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch.junit;
+
+import com.android.SdkConstants;
+import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner.TestSize;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.common.xml.ManifestData.Instrumentation;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.launch.AndroidLaunch;
+import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchConfiguration;
+import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchController;
+import com.android.ide.eclipse.adt.internal.launch.IAndroidLaunchAction;
+import com.android.ide.eclipse.adt.internal.launch.LaunchConfigDelegate;
+import com.android.ide.eclipse.adt.internal.launch.LaunchMessages;
+import com.android.ide.eclipse.adt.internal.launch.junit.runtime.AndroidJUnitLaunchInfo;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
+import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+import org.eclipse.swt.widgets.Display;
+
+/**
+ * Run configuration that can execute JUnit tests on an Android platform.
+ * <p/>
+ * Will deploy apps on target Android platform by reusing functionality from ADT
+ * LaunchConfigDelegate, and then run JUnits tests by reusing functionality from JDT
+ * JUnitLaunchConfigDelegate.
+ */
+@SuppressWarnings("restriction")
+public class AndroidJUnitLaunchConfigDelegate extends LaunchConfigDelegate {
+
+ /** Launch config attribute that stores instrumentation runner. */
+ static final String ATTR_INSTR_NAME = AdtPlugin.PLUGIN_ID + ".instrumentation"; //$NON-NLS-1$
+
+ /** Launch config attribute that stores the test size annotation to run. */
+ static final String ATTR_TEST_SIZE = AdtPlugin.PLUGIN_ID + ".testSize"; //$NON-NLS-1$
+
+ private static final String EMPTY_STRING = ""; //$NON-NLS-1$
+
+ @Override
+ protected void doLaunch(final ILaunchConfiguration configuration, final String mode,
+ final IProgressMonitor monitor, final IProject project,
+ final AndroidLaunch androidLaunch, final AndroidLaunchConfiguration config,
+ final AndroidLaunchController controller, final IFile applicationPackage,
+ final ManifestData manifestData) {
+
+ String runner = getRunner(project, configuration, manifestData);
+ if (runner == null) {
+ AdtPlugin.displayError(LaunchMessages.LaunchDialogTitle,
+ String.format(LaunchMessages.AndroidJUnitDelegate_NoRunnerMsg_s,
+ project.getName()));
+ androidLaunch.stopLaunch();
+ return;
+ }
+ // get the target app's package
+ final String targetAppPackage = getTargetPackage(manifestData, runner);
+ if (targetAppPackage == null) {
+ AdtPlugin.displayError(LaunchMessages.LaunchDialogTitle,
+ String.format(LaunchMessages.AndroidJUnitDelegate_NoTargetMsg_3s,
+ project.getName(), runner, SdkConstants.FN_ANDROID_MANIFEST_XML));
+ androidLaunch.stopLaunch();
+ return;
+ }
+ final String testAppPackage = manifestData.getPackage();
+ AndroidJUnitLaunchInfo junitLaunchInfo = new AndroidJUnitLaunchInfo(project,
+ testAppPackage, runner);
+ junitLaunchInfo.setTestClass(getTestClass(configuration));
+ junitLaunchInfo.setTestPackage(getTestPackage(configuration));
+ junitLaunchInfo.setTestMethod(getTestMethod(configuration));
+ junitLaunchInfo.setLaunch(androidLaunch);
+ junitLaunchInfo.setTestSize(getTestSize(configuration));
+ final IAndroidLaunchAction junitLaunch = new AndroidJUnitLaunchAction(junitLaunchInfo);
+
+ // launch on a separate thread if currently on the display thread
+ if (Display.getCurrent() != null) {
+ Job job = new Job("Junit Launch") { //$NON-NLS-1$
+ @Override
+ protected IStatus run(IProgressMonitor m) {
+ controller.launch(project, mode, applicationPackage, testAppPackage,
+ targetAppPackage, manifestData.getDebuggable(),
+ manifestData.getMinSdkVersionString(),
+ junitLaunch, config, androidLaunch, monitor);
+ return Status.OK_STATUS;
+ }
+ };
+ job.setPriority(Job.INTERACTIVE);
+ job.schedule();
+ } else {
+ controller.launch(project, mode, applicationPackage, testAppPackage, targetAppPackage,
+ manifestData.getDebuggable(), manifestData.getMinSdkVersionString(),
+ junitLaunch, config, androidLaunch, monitor);
+ }
+ }
+
+ /**
+ * Get the target Android application's package for the given instrumentation runner, or
+ * <code>null</code> if it could not be found.
+ *
+ * @param manifestParser the {@link ManifestData} for the test project
+ * @param runner the instrumentation runner class name
+ * @return the target package or <code>null</code>
+ */
+ private String getTargetPackage(ManifestData manifestParser, String runner) {
+ for (Instrumentation instr : manifestParser.getInstrumentations()) {
+ if (instr.getName().equals(runner)) {
+ return instr.getTargetPackage();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the test package stored in the launch configuration, or <code>null</code> if not
+ * specified.
+ *
+ * @param configuration the {@link ILaunchConfiguration} to retrieve the test package info from
+ * @return the test package or <code>null</code>.
+ */
+ private String getTestPackage(ILaunchConfiguration configuration) {
+ // try to retrieve a package name from the JUnit container attribute
+ String containerHandle = getStringLaunchAttribute(
+ JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, configuration);
+ if (containerHandle != null && containerHandle.length() > 0) {
+ IJavaElement element = JavaCore.create(containerHandle);
+ // containerHandle could be a IProject, check if its a java package
+ if (element.getElementType() == IJavaElement.PACKAGE_FRAGMENT) {
+ return element.getElementName();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the test class stored in the launch configuration.
+ *
+ * @param configuration the {@link ILaunchConfiguration} to retrieve the test class info from
+ * @return the test class. <code>null</code> if not specified.
+ */
+ private String getTestClass(ILaunchConfiguration configuration) {
+ return getStringLaunchAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
+ configuration);
+ }
+
+ /**
+ * Returns the test method stored in the launch configuration.
+ *
+ * @param configuration the {@link ILaunchConfiguration} to retrieve the test method info from
+ * @return the test method. <code>null</code> if not specified.
+ */
+ private String getTestMethod(ILaunchConfiguration configuration) {
+ return getStringLaunchAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
+ configuration);
+ }
+
+ /**
+ * Returns the test sizes to run as saved in the launch configuration.
+ * @return {@link TestSize} if only tests of specific sizes should be run,
+ * null if all tests should be run
+ */
+ private TestSize getTestSize(ILaunchConfiguration configuration) {
+ String testSizeAnnotation = getStringLaunchAttribute(
+ AndroidJUnitLaunchConfigDelegate.ATTR_TEST_SIZE,
+ configuration);
+ if (AndroidJUnitLaunchConfigurationTab.SMALL_TEST_ANNOTATION.equals(
+ testSizeAnnotation)){
+ return TestSize.SMALL;
+ } else if (AndroidJUnitLaunchConfigurationTab.MEDIUM_TEST_ANNOTATION.equals(
+ testSizeAnnotation)) {
+ return TestSize.MEDIUM;
+ } else if (AndroidJUnitLaunchConfigurationTab.LARGE_TEST_ANNOTATION.equals(
+ testSizeAnnotation)) {
+ return TestSize.LARGE;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Gets a instrumentation runner for the launch.
+ * <p/>
+ * If a runner is stored in the given <code>configuration</code>, will return that.
+ * Otherwise, will try to find the first valid runner for the project.
+ * If a runner can still not be found, will return <code>null</code>, and will log an error
+ * to the console.
+ *
+ * @param project the {@link IProject} for the app
+ * @param configuration the {@link ILaunchConfiguration} for the launch
+ * @param manifestData the {@link ManifestData} for the project
+ *
+ * @return <code>null</code> if no instrumentation runner can be found, otherwise return
+ * the fully qualified runner name.
+ */
+ private String getRunner(IProject project, ILaunchConfiguration configuration,
+ ManifestData manifestData) {
+ try {
+ String runner = getRunnerFromConfig(configuration);
+ if (runner != null) {
+ return runner;
+ }
+ final InstrumentationRunnerValidator instrFinder = new InstrumentationRunnerValidator(
+ BaseProjectHelper.getJavaProject(project), manifestData);
+ runner = instrFinder.getValidInstrumentationTestRunner();
+ if (runner != null) {
+ AdtPlugin.printErrorToConsole(project, String.format(
+ LaunchMessages.AndroidJUnitDelegate_NoRunnerConfigMsg_s, runner));
+ return runner;
+ }
+ AdtPlugin.printErrorToConsole(project, String.format(
+ LaunchMessages.AndroidJUnitDelegate_NoRunnerConsoleMsg_4s,
+ project.getName(),
+ SdkConstants.CLASS_INSTRUMENTATION_RUNNER,
+ AdtConstants.LIBRARY_TEST_RUNNER,
+ SdkConstants.FN_ANDROID_MANIFEST_XML));
+ return null;
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "Error when retrieving instrumentation info"); //$NON-NLS-1$
+ }
+
+ return null;
+ }
+
+ private String getRunnerFromConfig(ILaunchConfiguration configuration) {
+ return getStringLaunchAttribute(ATTR_INSTR_NAME, configuration);
+ }
+
+ /**
+ * Helper method to retrieve a string attribute from the launch configuration
+ *
+ * @param attributeName name of the launch attribute
+ * @param configuration the {@link ILaunchConfiguration} to retrieve the attribute from
+ * @return the attribute's value. <code>null</code> if not found.
+ */
+ private String getStringLaunchAttribute(String attributeName,
+ ILaunchConfiguration configuration) {
+ try {
+ String attrValue = configuration.getAttribute(attributeName, EMPTY_STRING);
+ if (attrValue.length() < 1) {
+ return null;
+ }
+ return attrValue;
+ } catch (CoreException e) {
+ AdtPlugin.log(e, String.format("Error when retrieving launch info %1$s", //$NON-NLS-1$
+ attributeName));
+ }
+ return null;
+ }
+
+ /**
+ * Helper method to set JUnit-related attributes expected by JDT JUnit runner
+ *
+ * @param config the launch configuration to modify
+ */
+ static void setJUnitDefaults(ILaunchConfigurationWorkingCopy config) {
+ // set the test runner to JUnit3 to placate JDT JUnit runner logic
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND,
+ TestKindRegistry.JUNIT3_TEST_KIND_ID);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigurationTab.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigurationTab.java
new file mode 100644
index 000000000..038f0b91f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchConfigurationTab.java
@@ -0,0 +1,1061 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.launch.junit;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.launch.LaunchMessages;
+import com.android.ide.eclipse.adt.internal.launch.MainLaunchConfigTab;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectChooserHelper;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.debug.core.ILaunchConfiguration;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaModel;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.ISourceReference;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.internal.junit.Messages;
+import org.eclipse.jdt.internal.junit.launcher.ITestKind;
+import org.eclipse.jdt.internal.junit.launcher.JUnitLaunchConfigurationConstants;
+import org.eclipse.jdt.internal.junit.launcher.JUnitMigrationDelegate;
+import org.eclipse.jdt.internal.junit.launcher.TestKindRegistry;
+import org.eclipse.jdt.internal.junit.launcher.TestSelectionDialog;
+import org.eclipse.jdt.internal.junit.ui.JUnitMessages;
+import org.eclipse.jdt.internal.junit.util.LayoutUtil;
+import org.eclipse.jdt.internal.junit.util.TestSearchEngine;
+import org.eclipse.jdt.internal.ui.wizards.TypedElementSelectionValidator;
+import org.eclipse.jdt.internal.ui.wizards.TypedViewerFilter;
+import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
+import org.eclipse.jdt.ui.JavaElementComparator;
+import org.eclipse.jdt.ui.JavaElementLabelProvider;
+import org.eclipse.jdt.ui.StandardJavaElementContentProvider;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.viewers.ILabelProvider;
+import org.eclipse.jface.viewers.ISelection;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.viewers.ViewerFilter;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Combo;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorPart;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.dialogs.ElementTreeSelectionDialog;
+import org.eclipse.ui.dialogs.SelectionDialog;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The launch config UI tab for Android JUnit
+ * <p/>
+ * Based on org.eclipse.jdt.junit.launcher.JUnitLaunchConfigurationTab
+ */
+@SuppressWarnings("restriction")
+public class AndroidJUnitLaunchConfigurationTab extends AbstractLaunchConfigurationTab {
+
+ // Project UI widgets
+ private Label mProjLabel;
+ private Text mProjText;
+ private Button mProjButton;
+
+ // Test class UI widgets
+ private Text mTestText;
+ private Button mSearchButton;
+ private String mOriginalTestMethodName;
+ private Label mTestMethodLabel;
+ private Text mContainerText;
+ private IJavaElement mContainerElement;
+ private final ILabelProvider mJavaElementLabelProvider = new JavaElementLabelProvider();
+
+ private Button mContainerSearchButton;
+ private Button mTestContainerRadioButton;
+ private Button mTestRadioButton;
+ private Label mTestLabel;
+
+ // Android specific members
+ private Image mTabIcon = null;
+ private Combo mInstrumentationCombo;
+ private Combo mTestSizeCombo;
+ private static final String EMPTY_STRING = ""; //$NON-NLS-1$
+ private static final String TAG = "AndroidJUnitLaunchConfigurationTab"; //$NON-NLS-1$
+ private String[] mInstrumentations = null;
+ private InstrumentationRunnerValidator mInstrValidator = null;
+ private ProjectChooserHelper mProjectChooserHelper;
+
+ public static final String SMALL_TEST_ANNOTATION = "@SmallTest"; //$NON-NLS-1$
+ public static final String MEDIUM_TEST_ANNOTATION = "@MediumTest"; //$NON-NLS-1$
+ public static final String LARGE_TEST_ANNOTATION = "@LargeTest"; //$NON-NLS-1$
+ private static final List<String> TEST_SIZE_OPTIONS = Arrays.asList(
+ "All Tests",
+ SMALL_TEST_ANNOTATION,
+ MEDIUM_TEST_ANNOTATION,
+ LARGE_TEST_ANNOTATION
+ );
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#createControl(org.eclipse.swt.widgets.Composite)
+ */
+ @Override
+ public void createControl(Composite parent) {
+ mProjectChooserHelper = new ProjectChooserHelper(parent.getShell(), null /*filter*/);
+
+ Composite comp = new Composite(parent, SWT.NONE);
+ setControl(comp);
+
+ GridLayout topLayout = new GridLayout();
+ topLayout.numColumns = 3;
+ comp.setLayout(topLayout);
+
+ createSingleTestSection(comp);
+ createTestContainerSelectionGroup(comp);
+
+ createSpacer(comp);
+
+ createInstrumentationGroup(comp);
+ createSizeSelector(comp);
+
+ Dialog.applyDialogFont(comp);
+ // TODO: add help link here when available
+ //PlatformUI.getWorkbench().getHelpSystem().setHelp(getControl(),
+ // IJUnitHelpContextIds.LAUNCH_CONFIGURATION_DIALOG_JUNIT_MAIN_TAB);
+ validatePage();
+ }
+
+
+ private void createSpacer(Composite comp) {
+ Label label = new Label(comp, SWT.NONE);
+ GridData gd = new GridData();
+ gd.horizontalSpan = 3;
+ label.setLayoutData(gd);
+ }
+
+ private void createSingleTestSection(Composite comp) {
+ mTestRadioButton = new Button(comp, SWT.RADIO);
+ mTestRadioButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_oneTest);
+ GridData gd = new GridData();
+ gd.horizontalSpan = 3;
+ mTestRadioButton.setLayoutData(gd);
+ mTestRadioButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mTestRadioButton.getSelection()) {
+ testModeChanged();
+ }
+ }
+ });
+
+ mProjLabel = new Label(comp, SWT.NONE);
+ mProjLabel.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_project);
+ gd = new GridData();
+ gd.horizontalIndent = 25;
+ mProjLabel.setLayoutData(gd);
+
+ mProjText = new Text(comp, SWT.SINGLE | SWT.BORDER);
+ mProjText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mProjText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent evt) {
+ validatePage();
+ updateLaunchConfigurationDialog();
+ mSearchButton.setEnabled(mTestRadioButton.getSelection() &&
+ mProjText.getText().length() > 0);
+ }
+ });
+
+ mProjButton = new Button(comp, SWT.PUSH);
+ mProjButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_browse);
+ mProjButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent evt) {
+ handleProjectButtonSelected();
+ }
+ });
+ setButtonGridData(mProjButton);
+
+ mTestLabel = new Label(comp, SWT.NONE);
+ gd = new GridData();
+ gd.horizontalIndent = 25;
+ mTestLabel.setLayoutData(gd);
+ mTestLabel.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_test);
+
+
+ mTestText = new Text(comp, SWT.SINGLE | SWT.BORDER);
+ mTestText.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
+ mTestText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent evt) {
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+ });
+
+ mSearchButton = new Button(comp, SWT.PUSH);
+ mSearchButton.setEnabled(mProjText.getText().length() > 0);
+ mSearchButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_search);
+ mSearchButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent evt) {
+ handleSearchButtonSelected();
+ }
+ });
+ setButtonGridData(mSearchButton);
+
+ new Label(comp, SWT.NONE);
+
+ mTestMethodLabel = new Label(comp, SWT.NONE);
+ mTestMethodLabel.setText(""); //$NON-NLS-1$
+ gd = new GridData();
+ gd.horizontalSpan = 2;
+ mTestMethodLabel.setLayoutData(gd);
+ }
+
+ private void createTestContainerSelectionGroup(Composite comp) {
+ mTestContainerRadioButton = new Button(comp, SWT.RADIO);
+ mTestContainerRadioButton.setText(
+ LaunchMessages.AndroidJUnitTab_TestContainerText);
+ GridData gd = new GridData();
+ gd.horizontalSpan = 3;
+ mTestContainerRadioButton.setLayoutData(gd);
+ mTestContainerRadioButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (mTestContainerRadioButton.getSelection()) {
+ testModeChanged();
+ }
+ }
+ });
+
+ mContainerText = new Text(comp, SWT.SINGLE | SWT.BORDER | SWT.READ_ONLY);
+ gd = new GridData(GridData.FILL_HORIZONTAL);
+ gd.horizontalIndent = 25;
+ gd.horizontalSpan = 2;
+ mContainerText.setLayoutData(gd);
+ mContainerText.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent evt) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+
+ mContainerSearchButton = new Button(comp, SWT.PUSH);
+ mContainerSearchButton.setText(JUnitMessages.JUnitLaunchConfigurationTab_label_search);
+ mContainerSearchButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent evt) {
+ handleContainerSearchButtonSelected();
+ }
+ });
+ setButtonGridData(mContainerSearchButton);
+ }
+
+ private void createInstrumentationGroup(Composite comp) {
+ Label loaderLabel = new Label(comp, SWT.NONE);
+ loaderLabel.setText(LaunchMessages.AndroidJUnitTab_LoaderLabel);
+ GridData gd = new GridData();
+ gd.horizontalIndent = 0;
+ loaderLabel.setLayoutData(gd);
+
+ mInstrumentationCombo = new Combo(comp, SWT.DROP_DOWN | SWT.READ_ONLY);
+ GridDataFactory.defaultsFor(mInstrumentationCombo)
+ .span(2, 1)
+ .applyTo(mInstrumentationCombo);
+ mInstrumentationCombo.clearSelection();
+ mInstrumentationCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+ });
+ }
+
+ private void createSizeSelector(Composite comp) {
+ Label l = new Label(comp, SWT.NONE);
+ l.setText(LaunchMessages.AndroidJUnitTab_SizeLabel);
+ GridData gd = new GridData();
+ gd.horizontalIndent = 0;
+ l.setLayoutData(gd);
+
+ mTestSizeCombo = new Combo(comp, SWT.DROP_DOWN | SWT.READ_ONLY);
+ mTestSizeCombo.setItems(TEST_SIZE_OPTIONS.toArray(new String[TEST_SIZE_OPTIONS.size()]));
+ mTestSizeCombo.select(0);
+ mTestSizeCombo.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ updateLaunchConfigurationDialog();
+ }
+ });
+ }
+
+ private void handleContainerSearchButtonSelected() {
+ IJavaElement javaElement = chooseContainer(mContainerElement);
+ if (javaElement != null) {
+ setContainerElement(javaElement);
+ }
+ }
+
+ private void setContainerElement(IJavaElement javaElement) {
+ mContainerElement = javaElement;
+ mContainerText.setText(getPresentationName(javaElement));
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#initializeFrom(org.eclipse.debug.core.ILaunchConfiguration)
+ */
+ @Override
+ public void initializeFrom(ILaunchConfiguration config) {
+ String projectName = updateProjectFromConfig(config);
+ String containerHandle = EMPTY_STRING;
+ try {
+ containerHandle = config.getAttribute(
+ JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, EMPTY_STRING);
+ } catch (CoreException ce) {
+ // ignore
+ }
+
+ if (containerHandle.length() > 0) {
+ updateTestContainerFromConfig(config);
+ } else {
+ updateTestTypeFromConfig(config);
+ }
+
+ IProject proj = mProjectChooserHelper.getAndroidProject(projectName);
+ loadInstrumentations(proj);
+ updateInstrumentationFromConfig(config);
+ updateTestSizeFromConfig(config);
+
+ validatePage();
+ }
+
+ private void updateInstrumentationFromConfig(ILaunchConfiguration config) {
+ boolean found = false;
+ try {
+ String currentInstrumentation = config.getAttribute(
+ AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME, EMPTY_STRING);
+ if (mInstrumentations != null) {
+ // look for the name of the instrumentation in the combo.
+ for (int i = 0; i < mInstrumentations.length; i++) {
+ if (currentInstrumentation.equals(mInstrumentations[i])) {
+ found = true;
+ mInstrumentationCombo.select(i);
+ break;
+ }
+ }
+ }
+ } catch (CoreException ce) {
+ // ignore
+ }
+ if (!found) {
+ mInstrumentationCombo.clearSelection();
+ }
+ }
+
+ private void updateTestSizeFromConfig(ILaunchConfiguration config) {
+ try {
+ String testSize = config.getAttribute(
+ AndroidJUnitLaunchConfigDelegate.ATTR_TEST_SIZE, EMPTY_STRING);
+ int index = TEST_SIZE_OPTIONS.indexOf(testSize);
+ if (index >= 0 && mTestSizeCombo != null) {
+ mTestSizeCombo.select(index);
+ }
+ } catch (CoreException e) {
+ // ignore
+ }
+ }
+
+ private String updateProjectFromConfig(ILaunchConfiguration config) {
+ String projectName = EMPTY_STRING;
+ try {
+ projectName = config.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ EMPTY_STRING);
+ } catch (CoreException ce) {
+ // ignore
+ }
+ mProjText.setText(projectName);
+ return projectName;
+ }
+
+ private void updateTestTypeFromConfig(ILaunchConfiguration config) {
+ String testTypeName = EMPTY_STRING;
+ mOriginalTestMethodName = EMPTY_STRING;
+ try {
+ testTypeName = config.getAttribute(
+ IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, ""); //$NON-NLS-1$
+ mOriginalTestMethodName = config.getAttribute(
+ JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME, ""); //$NON-NLS-1$
+ } catch (CoreException ce) {
+ // ignore
+ }
+ mTestRadioButton.setSelection(true);
+ setEnableSingleTestGroup(true);
+ setEnableContainerTestGroup(false);
+ mTestContainerRadioButton.setSelection(false);
+ mTestText.setText(testTypeName);
+ mContainerText.setText(EMPTY_STRING);
+ setTestMethodLabel(mOriginalTestMethodName);
+ }
+
+ private void setTestMethodLabel(String testMethodName) {
+ if (!EMPTY_STRING.equals(testMethodName)) {
+ mTestMethodLabel.setText(
+ JUnitMessages.JUnitLaunchConfigurationTab_label_method +
+ mOriginalTestMethodName);
+ } else {
+ mTestMethodLabel.setText(EMPTY_STRING);
+ }
+ }
+
+ private void updateTestContainerFromConfig(ILaunchConfiguration config) {
+ String containerHandle = EMPTY_STRING;
+ IJavaElement containerElement = null;
+ try {
+ containerHandle = config.getAttribute(
+ JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER, EMPTY_STRING);
+ if (containerHandle.length() > 0) {
+ containerElement = JavaCore.create(containerHandle);
+ }
+ } catch (CoreException ce) {
+ // ignore
+ }
+ if (containerElement != null) {
+ mContainerElement = containerElement;
+ }
+ mTestContainerRadioButton.setSelection(true);
+ setEnableSingleTestGroup(false);
+ setEnableContainerTestGroup(true);
+ mTestRadioButton.setSelection(false);
+ if (mContainerElement != null) {
+ mContainerText.setText(getPresentationName(mContainerElement));
+ }
+ mTestText.setText(EMPTY_STRING);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#performApply(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+ */
+ @Override
+ public void performApply(ILaunchConfigurationWorkingCopy config) {
+ if (mTestContainerRadioButton.getSelection() && mContainerElement != null) {
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ mContainerElement.getJavaProject().getElementName());
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ mContainerElement.getHandleIdentifier());
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
+ EMPTY_STRING);
+ //workaround for Eclipse bug 65399
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
+ EMPTY_STRING);
+ } else {
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME,
+ mProjText.getText());
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME,
+ mTestText.getText());
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ EMPTY_STRING);
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_METHOD_NAME,
+ mOriginalTestMethodName);
+ }
+ try {
+ mapResources(config);
+ } catch (CoreException e) {
+ // TODO: does the real error need to be extracted out of CoreException
+ AdtPlugin.log(e, "Error occurred saving configuration"); //$NON-NLS-1$
+ }
+ AndroidJUnitLaunchConfigDelegate.setJUnitDefaults(config);
+
+ config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME,
+ getSelectedInstrumentation());
+ config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_TEST_SIZE,
+ getSelectedTestSize());
+ }
+
+ private void mapResources(ILaunchConfigurationWorkingCopy config) throws CoreException {
+ JUnitMigrationDelegate.mapResources(config);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#dispose()
+ */
+ @Override
+ public void dispose() {
+ super.dispose();
+ mTabIcon = null;
+ mJavaElementLabelProvider.dispose();
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#getImage()
+ */
+ @Override
+ public Image getImage() {
+ // reuse icon from the Android App Launch config tab
+ if (mTabIcon == null) {
+ mTabIcon = IconFactory.getInstance().getIcon(MainLaunchConfigTab.LAUNCH_TAB_IMAGE);
+ }
+ return mTabIcon;
+ }
+
+ /**
+ * Show a dialog that lists all main types
+ */
+ private void handleSearchButtonSelected() {
+ Shell shell = getShell();
+
+ IJavaProject javaProject = getJavaProject();
+
+ IType[] types = new IType[0];
+ boolean[] radioSetting = new boolean[2];
+ try {
+ // fix for Eclipse bug 66922 Wrong radio behaviour when switching
+ // remember the selected radio button
+ radioSetting[0] = mTestRadioButton.getSelection();
+ radioSetting[1] = mTestContainerRadioButton.getSelection();
+
+ types = TestSearchEngine.findTests(getLaunchConfigurationDialog(), javaProject,
+ getTestKind());
+ } catch (InterruptedException e) {
+ setErrorMessage(e.getMessage());
+ return;
+ } catch (InvocationTargetException e) {
+ AdtPlugin.log(e.getTargetException(), "Error finding test types"); //$NON-NLS-1$
+ return;
+ } finally {
+ mTestRadioButton.setSelection(radioSetting[0]);
+ mTestContainerRadioButton.setSelection(radioSetting[1]);
+ }
+
+ SelectionDialog dialog = new TestSelectionDialog(shell, types);
+ dialog.setTitle(JUnitMessages.JUnitLaunchConfigurationTab_testdialog_title);
+ dialog.setMessage(JUnitMessages.JUnitLaunchConfigurationTab_testdialog_message);
+ if (dialog.open() == Window.CANCEL) {
+ return;
+ }
+
+ Object[] results = dialog.getResult();
+ if ((results == null) || (results.length < 1)) {
+ return;
+ }
+ IType type = (IType) results[0];
+
+ if (type != null) {
+ mTestText.setText(type.getFullyQualifiedName('.'));
+ javaProject = type.getJavaProject();
+ mProjText.setText(javaProject.getElementName());
+ }
+ }
+
+ private ITestKind getTestKind() {
+ // harddcode this to JUnit 3
+ return TestKindRegistry.getDefault().getKind(TestKindRegistry.JUNIT3_TEST_KIND_ID);
+ }
+
+ /**
+ * Show a dialog that lets the user select a Android project. This in turn provides
+ * context for the main type, allowing the user to key a main type name, or
+ * constraining the search for main types to the specified project.
+ */
+ private void handleProjectButtonSelected() {
+ IJavaProject project = mProjectChooserHelper.chooseJavaProject(getProjectName(),
+ "Please select a project to launch");
+ if (project == null) {
+ return;
+ }
+
+ String projectName = project.getElementName();
+ mProjText.setText(projectName);
+ loadInstrumentations(project.getProject());
+ }
+
+ /**
+ * Return the IJavaProject corresponding to the project name in the project name
+ * text field, or null if the text does not match a Android project name.
+ */
+ private IJavaProject getJavaProject() {
+ String projectName = getProjectName();
+ return getJavaModel().getJavaProject(projectName);
+ }
+
+ /**
+ * Returns the name of the currently specified project. Null if no project is selected.
+ */
+ private String getProjectName() {
+ String projectName = mProjText.getText().trim();
+ if (projectName.length() < 1) {
+ return null;
+ }
+ return projectName;
+ }
+
+ /**
+ * Convenience method to get the workspace root.
+ */
+ private IWorkspaceRoot getWorkspaceRoot() {
+ return ResourcesPlugin.getWorkspace().getRoot();
+ }
+
+ /**
+ * Convenience method to get access to the java model.
+ */
+ private IJavaModel getJavaModel() {
+ return JavaCore.create(getWorkspaceRoot());
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#isValid(org.eclipse.debug.core.ILaunchConfiguration)
+ */
+ @Override
+ public boolean isValid(ILaunchConfiguration config) {
+ validatePage();
+ return getErrorMessage() == null;
+ }
+
+ private void testModeChanged() {
+ boolean isSingleTestMode = mTestRadioButton.getSelection();
+ setEnableSingleTestGroup(isSingleTestMode);
+ setEnableContainerTestGroup(!isSingleTestMode);
+ if (!isSingleTestMode && mContainerText.getText().length() == 0) {
+ String projText = mProjText.getText();
+ if (Path.EMPTY.isValidSegment(projText)) {
+ IJavaProject javaProject = getJavaModel().getJavaProject(projText);
+ if (javaProject != null && javaProject.exists()) {
+ setContainerElement(javaProject);
+ }
+ }
+ }
+ validatePage();
+ updateLaunchConfigurationDialog();
+ }
+
+ private void validatePage() {
+ setErrorMessage(null);
+ setMessage(null);
+
+ if (mTestContainerRadioButton.getSelection()) {
+ if (mContainerElement == null) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_noContainer);
+ return;
+ }
+ validateJavaProject(mContainerElement.getJavaProject());
+ return;
+ }
+
+ String projectName = mProjText.getText().trim();
+ if (projectName.length() == 0) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_projectnotdefined);
+ return;
+ }
+
+ IStatus status = ResourcesPlugin.getWorkspace().validatePath(IPath.SEPARATOR + projectName,
+ IResource.PROJECT);
+ if (!status.isOK() || !Path.ROOT.isValidSegment(projectName)) {
+ setErrorMessage(Messages.format(
+ JUnitMessages.JUnitLaunchConfigurationTab_error_invalidProjectName,
+ projectName));
+ return;
+ }
+
+ IProject project = getWorkspaceRoot().getProject(projectName);
+ if (!project.exists()) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_projectnotexists);
+ return;
+ }
+ IJavaProject javaProject = JavaCore.create(project);
+ validateJavaProject(javaProject);
+
+ try {
+ if (!project.hasNature(AdtConstants.NATURE_DEFAULT)) {
+ setErrorMessage(
+ LaunchMessages.NonAndroidProjectError);
+ return;
+ }
+ String className = mTestText.getText().trim();
+ if (className.length() == 0) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_testnotdefined);
+ return;
+ }
+ if (javaProject.findType(className) == null) {
+ setErrorMessage(Messages.format(
+ JUnitMessages.JUnitLaunchConfigurationTab_error_test_class_not_found,
+ new String[] { className, projectName }));
+ return;
+ }
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "validatePage failed"); //$NON-NLS-1$
+ }
+
+ validateInstrumentation();
+ }
+
+ private void validateJavaProject(IJavaProject javaProject) {
+ if (!TestSearchEngine.hasTestCaseType(javaProject)) {
+ setErrorMessage(JUnitMessages.JUnitLaunchConfigurationTab_error_testcasenotonpath);
+ return;
+ }
+ }
+
+ private void validateInstrumentation() {
+ String instrumentation = getSelectedInstrumentation();
+ if (instrumentation == null) {
+ setErrorMessage(LaunchMessages.AndroidJUnitTab_NoRunnerError);
+ return;
+ }
+ String result = mInstrValidator.validateInstrumentationRunner(instrumentation);
+ if (result != InstrumentationRunnerValidator.INSTRUMENTATION_OK) {
+ setErrorMessage(result);
+ return;
+ }
+ }
+
+ private String getSelectedInstrumentation() {
+ int selectionIndex = mInstrumentationCombo.getSelectionIndex();
+ if (mInstrumentations != null && selectionIndex >= 0 &&
+ selectionIndex < mInstrumentations.length) {
+ return mInstrumentations[selectionIndex];
+ }
+ return null;
+ }
+
+ private String getSelectedTestSize() {
+ if (mTestSizeCombo != null) {
+ int index = mTestSizeCombo.getSelectionIndex();
+ return TEST_SIZE_OPTIONS.get(index);
+ } else {
+ return null;
+ }
+ }
+
+ private void setEnableContainerTestGroup(boolean enabled) {
+ mContainerSearchButton.setEnabled(enabled);
+ mContainerText.setEnabled(enabled);
+ }
+
+ private void setEnableSingleTestGroup(boolean enabled) {
+ mProjLabel.setEnabled(enabled);
+ mProjText.setEnabled(enabled);
+ mProjButton.setEnabled(enabled);
+ mTestLabel.setEnabled(enabled);
+ mTestText.setEnabled(enabled);
+ mSearchButton.setEnabled(enabled && mProjText.getText().length() > 0);
+ mTestMethodLabel.setEnabled(enabled);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#setDefaults(org.eclipse.debug.core.ILaunchConfigurationWorkingCopy)
+ */
+ @Override
+ public void setDefaults(ILaunchConfigurationWorkingCopy config) {
+ IJavaElement javaElement = getContext();
+ if (javaElement != null) {
+ initializeJavaProject(javaElement, config);
+ } else {
+ // We set empty attributes for project & main type so that when one config is
+ // compared to another, the existence of empty attributes doesn't cause an
+ // incorrect result (the performApply() method can result in empty values
+ // for these attributes being set on a config if there is nothing in the
+ // corresponding text boxes)
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, EMPTY_STRING);
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ EMPTY_STRING);
+ }
+ initializeTestAttributes(javaElement, config);
+ }
+
+ private void initializeTestAttributes(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ if (javaElement != null && javaElement.getElementType() < IJavaElement.COMPILATION_UNIT) {
+ initializeTestContainer(javaElement, config);
+ } else {
+ initializeTestType(javaElement, config);
+ }
+ }
+
+ private void initializeTestContainer(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_CONTAINER,
+ javaElement.getHandleIdentifier());
+ initializeName(config, javaElement.getElementName());
+ }
+
+ private void initializeName(ILaunchConfigurationWorkingCopy config, String name) {
+ if (name == null) {
+ name = EMPTY_STRING;
+ }
+ if (name.length() > 0) {
+ int index = name.lastIndexOf('.');
+ if (index > 0) {
+ name = name.substring(index + 1);
+ }
+ name = getLaunchConfigurationDialog().generateName(name);
+ config.rename(name);
+ }
+ }
+
+ /**
+ * Sets the main type & name attributes on the working copy based on the IJavaElement
+ */
+ private void initializeTestType(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ String name = EMPTY_STRING;
+ String testKindId = null;
+ try {
+ // only do a search for compilation units or class files or source references
+ if (javaElement instanceof ISourceReference) {
+ ITestKind testKind = TestKindRegistry.getContainerTestKind(javaElement);
+ testKindId = testKind.getId();
+
+ IType[] types = TestSearchEngine.findTests(getLaunchConfigurationDialog(),
+ javaElement, testKind);
+ if ((types == null) || (types.length < 1)) {
+ return;
+ }
+ // Simply grab the first main type found in the searched element
+ name = types[0].getFullyQualifiedName('.');
+
+ }
+ } catch (InterruptedException ie) {
+ // ignore
+ } catch (InvocationTargetException ite) {
+ // ignore
+ }
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_MAIN_TYPE_NAME, name);
+ if (testKindId != null) {
+ config.setAttribute(JUnitLaunchConfigurationConstants.ATTR_TEST_RUNNER_KIND,
+ testKindId);
+ }
+ initializeName(config, name);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.ILaunchConfigurationTab#getName()
+ */
+ @Override
+ public String getName() {
+ return JUnitMessages.JUnitLaunchConfigurationTab_tab_label;
+ }
+
+ private IJavaElement chooseContainer(IJavaElement initElement) {
+ @SuppressWarnings("rawtypes")
+ Class[] acceptedClasses = new Class[] { IJavaProject.class,
+ IPackageFragment.class };
+ TypedElementSelectionValidator validator = new TypedElementSelectionValidator(
+ acceptedClasses, false) {
+ @Override
+ public boolean isSelectedValid(Object element) {
+ return true;
+ }
+ };
+
+ acceptedClasses = new Class[] { IJavaModel.class, IPackageFragmentRoot.class,
+ IJavaProject.class, IPackageFragment.class };
+ ViewerFilter filter = new TypedViewerFilter(acceptedClasses) {
+ @Override
+ public boolean select(Viewer viewer, Object parent, Object element) {
+ if (element instanceof IPackageFragmentRoot &&
+ ((IPackageFragmentRoot) element).isArchive()) {
+ return false;
+ }
+ try {
+ if (element instanceof IPackageFragment &&
+ !((IPackageFragment) element).hasChildren()) {
+ return false;
+ }
+ } catch (JavaModelException e) {
+ return false;
+ }
+ return super.select(viewer, parent, element);
+ }
+ };
+
+ AndroidJavaElementContentProvider provider = new AndroidJavaElementContentProvider();
+ ILabelProvider labelProvider = new JavaElementLabelProvider(
+ JavaElementLabelProvider.SHOW_DEFAULT);
+ ElementTreeSelectionDialog dialog = new ElementTreeSelectionDialog(getShell(),
+ labelProvider, provider);
+ dialog.setValidator(validator);
+ dialog.setComparator(new JavaElementComparator());
+ dialog.setTitle(JUnitMessages.JUnitLaunchConfigurationTab_folderdialog_title);
+ dialog.setMessage(JUnitMessages.JUnitLaunchConfigurationTab_folderdialog_message);
+ dialog.addFilter(filter);
+ dialog.setInput(JavaCore.create(getWorkspaceRoot()));
+ dialog.setInitialSelection(initElement);
+ dialog.setAllowMultiple(false);
+
+ if (dialog.open() == Window.OK) {
+ Object element = dialog.getFirstResult();
+ return (IJavaElement) element;
+ }
+ return null;
+ }
+
+ private String getPresentationName(IJavaElement element) {
+ return mJavaElementLabelProvider.getText(element);
+ }
+
+ /**
+ * Returns the current Java element context from which to initialize
+ * default settings, or <code>null</code> if none.
+ *
+ * @return Java element context.
+ */
+ private IJavaElement getContext() {
+ IWorkbenchWindow activeWorkbenchWindow =
+ PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+ if (activeWorkbenchWindow == null) {
+ return null;
+ }
+ IWorkbenchPage page = activeWorkbenchWindow.getActivePage();
+ if (page != null) {
+ ISelection selection = page.getSelection();
+ if (selection instanceof IStructuredSelection) {
+ IStructuredSelection ss = (IStructuredSelection) selection;
+ if (!ss.isEmpty()) {
+ Object obj = ss.getFirstElement();
+ if (obj instanceof IJavaElement) {
+ return (IJavaElement) obj;
+ }
+ if (obj instanceof IResource) {
+ IJavaElement je = JavaCore.create((IResource) obj);
+ if (je == null) {
+ IProject pro = ((IResource) obj).getProject();
+ je = JavaCore.create(pro);
+ }
+ if (je != null) {
+ return je;
+ }
+ }
+ }
+ }
+ IEditorPart part = page.getActiveEditor();
+ if (part != null) {
+ IEditorInput input = part.getEditorInput();
+ return (IJavaElement) input.getAdapter(IJavaElement.class);
+ }
+ }
+ return null;
+ }
+
+ private void initializeJavaProject(IJavaElement javaElement,
+ ILaunchConfigurationWorkingCopy config) {
+ IJavaProject javaProject = javaElement.getJavaProject();
+ String name = null;
+ if (javaProject != null && javaProject.exists()) {
+ name = javaProject.getElementName();
+ }
+ config.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, name);
+ }
+
+ private void setButtonGridData(Button button) {
+ GridData gridData = new GridData();
+ button.setLayoutData(gridData);
+ LayoutUtil.setButtonDimensionHint(button);
+ }
+
+ /* (non-Javadoc)
+ * @see org.eclipse.debug.ui.AbstractLaunchConfigurationTab#getId()
+ */
+ @Override
+ public String getId() {
+ return "com.android.ide.eclipse.adt.launch.AndroidJUnitLaunchConfigurationTab"; //$NON-NLS-1$
+ }
+
+ /**
+ * Loads the UI with the instrumentations of the specified project, and stores the
+ * instrumentations in <code>mInstrumentations</code>.
+ *
+ * @param project the {@link IProject} to load the instrumentations from.
+ */
+ private void loadInstrumentations(IProject project) {
+ try {
+ mInstrValidator = new InstrumentationRunnerValidator(project);
+ mInstrumentations = mInstrValidator.getInstrumentationNames();
+ if (mInstrumentations.length > 0) {
+ mInstrumentationCombo.removeAll();
+ for (String instrumentation : mInstrumentations) {
+ mInstrumentationCombo.add(instrumentation);
+ }
+ // the selection will be set when we update the ui from the current
+ // config object.
+ return;
+ }
+ } catch (CoreException e) {
+ AdtPlugin.logAndPrintError(e, project.getName(),
+ LaunchMessages.AndroidJUnitTab_LoadInstrError_s,
+ SdkConstants.FN_ANDROID_MANIFEST_XML);
+ }
+ // if we reach this point, either project is null, or we got an exception during
+ // the parsing. In either case, we empty the instrumentation list.
+ mInstrValidator = null;
+ mInstrumentations = null;
+ mInstrumentationCombo.removeAll();
+ }
+
+ /**
+ * Overrides the {@link StandardJavaElementContentProvider} to only display Android projects
+ */
+ private static class AndroidJavaElementContentProvider
+ extends StandardJavaElementContentProvider {
+
+ /**
+ * Override parent to return only Android projects if at the root. Otherwise, use parent
+ * functionality.
+ */
+ @Override
+ public Object[] getChildren(Object element) {
+ if (element instanceof IJavaModel) {
+ return BaseProjectHelper.getAndroidProjects((IJavaModel) element, null /*filter*/);
+ }
+ return super.getChildren(element);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchShortcut.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchShortcut.java
new file mode 100755
index 000000000..b94ced891
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitLaunchShortcut.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch.junit;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.junit.launcher.JUnitLaunchShortcut;
+
+/**
+ * Launch shortcut to launch debug/run Android JUnit configuration directly.
+ */
+public class AndroidJUnitLaunchShortcut extends JUnitLaunchShortcut {
+
+ @Override
+ protected String getLaunchConfigurationTypeId() {
+ return "com.android.ide.eclipse.adt.junit.launchConfigurationType"; //$NON-NLS-1$
+ }
+
+ /**
+ * Creates a default Android JUnit launch configuration. Sets the instrumentation runner to the
+ * first instrumentation found in the AndroidManifest.
+ */
+ @Override
+ protected ILaunchConfigurationWorkingCopy createLaunchConfiguration(IJavaElement element)
+ throws CoreException {
+ ILaunchConfigurationWorkingCopy config = super.createLaunchConfiguration(element);
+ // just get first valid instrumentation runner
+ String instrumentation = new InstrumentationRunnerValidator(element.getJavaProject()).
+ getValidInstrumentationTestRunner();
+ if (instrumentation != null) {
+ config.setAttribute(AndroidJUnitLaunchConfigDelegate.ATTR_INSTR_NAME,
+ instrumentation);
+ }
+ // if a valid runner is not found, rely on launch delegate to log error.
+ // This method is called without explicit user action to launch Android JUnit, so avoid
+ // logging an error here.
+
+ AndroidJUnitLaunchConfigDelegate.setJUnitDefaults(config);
+ return config;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitPropertyTester.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitPropertyTester.java
new file mode 100644
index 000000000..5172e09a7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitPropertyTester.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.launch.junit;
+
+import org.eclipse.core.expressions.PropertyTester;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.jdt.core.IClassFile;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IMember;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.internal.junit.util.TestSearchEngine;
+
+/**
+ * A {@link PropertyTester} that checks if selected elements can be run as Android
+ * JUnit tests.
+ * <p/>
+ * Based on org.eclipse.jdt.internal.junit.JUnitPropertyTester. The only substantial difference in
+ * this implementation is source folders cannot be run as Android JUnit.
+ */
+@SuppressWarnings("restriction")
+public class AndroidJUnitPropertyTester extends PropertyTester {
+ private static final String PROPERTY_IS_TEST = "isTest"; //$NON-NLS-1$
+
+ private static final String PROPERTY_CAN_LAUNCH_AS_JUNIT_TEST = "canLaunchAsJUnit"; //$NON-NLS-1$
+
+ /* (non-Javadoc)
+ * @see org.eclipse.jdt.internal.corext.refactoring.participants.properties.IPropertyEvaluator#test(java.lang.Object, java.lang.String, java.lang.String)
+ */
+ @Override
+ public boolean test(Object receiver, String property, Object[] args, Object expectedValue) {
+ if (!(receiver instanceof IAdaptable)) {
+ final String elementName = (receiver == null ? "null" : //$NON-NLS-1$
+ receiver.getClass().getName());
+ throw new IllegalArgumentException(
+ String.format("Element must be of type IAdaptable, is %s", //$NON-NLS-1$
+ elementName));
+ }
+
+ IJavaElement element;
+ if (receiver instanceof IJavaElement) {
+ element = (IJavaElement) receiver;
+ } else if (receiver instanceof IResource) {
+ element = JavaCore.create((IResource) receiver);
+ if (element == null) {
+ return false;
+ }
+ } else { // is IAdaptable
+ element= (IJavaElement) ((IAdaptable) receiver).getAdapter(IJavaElement.class);
+ if (element == null) {
+ IResource resource = (IResource) ((IAdaptable) receiver).getAdapter(
+ IResource.class);
+ element = JavaCore.create(resource);
+ if (element == null) {
+ return false;
+ }
+ }
+ }
+ if (PROPERTY_IS_TEST.equals(property)) {
+ return isJUnitTest(element);
+ } else if (PROPERTY_CAN_LAUNCH_AS_JUNIT_TEST.equals(property)) {
+ return canLaunchAsJUnitTest(element);
+ }
+ throw new IllegalArgumentException(
+ String.format("Unknown test property '%s'", property)); //$NON-NLS-1$
+ }
+
+ private boolean canLaunchAsJUnitTest(IJavaElement element) {
+ try {
+ switch (element.getElementType()) {
+ case IJavaElement.JAVA_PROJECT:
+ return true; // can run, let JDT detect if there are tests
+ case IJavaElement.PACKAGE_FRAGMENT_ROOT:
+ return false; // not supported by Android test runner
+ case IJavaElement.PACKAGE_FRAGMENT:
+ return ((IPackageFragment) element).hasChildren();
+ case IJavaElement.COMPILATION_UNIT:
+ case IJavaElement.CLASS_FILE:
+ case IJavaElement.TYPE:
+ case IJavaElement.METHOD:
+ return isJUnitTest(element);
+ default:
+ return false;
+ }
+ } catch (JavaModelException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Return whether the target resource is a JUnit test.
+ */
+ private boolean isJUnitTest(IJavaElement element) {
+ try {
+ IType testType = null;
+ if (element instanceof ICompilationUnit) {
+ testType = (((ICompilationUnit) element)).findPrimaryType();
+ } else if (element instanceof IClassFile) {
+ testType = (((IClassFile) element)).getType();
+ } else if (element instanceof IType) {
+ testType = (IType) element;
+ } else if (element instanceof IMember) {
+ testType = ((IMember) element).getDeclaringType();
+ }
+ if (testType != null && testType.exists()) {
+ return TestSearchEngine.isTestOrTestSuite(testType);
+ }
+ } catch (CoreException e) {
+ // ignore, return false
+ }
+ return false;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitTabGroup.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitTabGroup.java
new file mode 100644
index 000000000..deb30de01
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/AndroidJUnitTabGroup.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch.junit;
+
+import com.android.ide.eclipse.adt.internal.launch.EmulatorConfigTab;
+
+import org.eclipse.debug.core.ILaunchManager;
+import org.eclipse.debug.ui.AbstractLaunchConfigurationTabGroup;
+import org.eclipse.debug.ui.CommonTab;
+import org.eclipse.debug.ui.ILaunchConfigurationDialog;
+import org.eclipse.debug.ui.ILaunchConfigurationTab;
+
+/**
+ * Tab group object for Android JUnit launch configuration type.
+ */
+public class AndroidJUnitTabGroup extends AbstractLaunchConfigurationTabGroup {
+
+ /**
+ * Creates the UI tabs for the Android JUnit configuration
+ */
+ @Override
+ public void createTabs(ILaunchConfigurationDialog dialog, String mode) {
+ ILaunchConfigurationTab[] tabs = new ILaunchConfigurationTab[] {
+ new AndroidJUnitLaunchConfigurationTab(),
+ new EmulatorConfigTab(ILaunchManager.RUN_MODE.equals(mode)),
+ new CommonTab()
+ };
+ setTabs(tabs);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java
new file mode 100644
index 000000000..820fed7d3
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/InstrumentationRunnerValidator.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.launch.junit;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.common.xml.ManifestData.Instrumentation;
+import com.android.ide.common.xml.ManifestData.UsesLibrary;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.launch.LaunchMessages;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJavaProject;
+
+/**
+ * Provides validation for Android instrumentation test runner
+ */
+class InstrumentationRunnerValidator {
+ private final IJavaProject mJavaProject;
+ private String[] mInstrumentationNames = null;
+ private boolean mHasRunnerLibrary = false;
+
+ static final String INSTRUMENTATION_OK = null;
+
+ /**
+ * Initializes the InstrumentationRunnerValidator.
+ *
+ * @param javaProject the {@link IJavaProject} for the Android project to validate
+ */
+ InstrumentationRunnerValidator(IJavaProject javaProject) {
+ mJavaProject = javaProject;
+ ManifestData manifestData = AndroidManifestHelper.parseForData(javaProject.getProject());
+ init(manifestData);
+ }
+
+ /**
+ * Initializes the InstrumentationRunnerValidator.
+ *
+ * @param project the {@link IProject} for the Android project to validate
+ * @throws CoreException if a fatal error occurred in initialization
+ */
+ InstrumentationRunnerValidator(IProject project) throws CoreException {
+ this(BaseProjectHelper.getJavaProject(project));
+ }
+
+ /**
+ * Initializes the InstrumentationRunnerValidator with an existing {@link AndroidManifestHelper}
+ *
+ * @param javaProject the {@link IJavaProject} for the Android project to validate
+ * @param manifestData the {@link ManifestData} for the Android project
+ */
+ InstrumentationRunnerValidator(IJavaProject javaProject, ManifestData manifestData) {
+ mJavaProject = javaProject;
+ init(manifestData);
+ }
+
+ private void init(ManifestData manifestData) {
+ if (manifestData == null) {
+ mInstrumentationNames = new String[0];
+ mHasRunnerLibrary = false;
+ return;
+ }
+
+ Instrumentation[] instrumentations = manifestData.getInstrumentations();
+ mInstrumentationNames = new String[instrumentations.length];
+ for (int i = 0; i < instrumentations.length; i++) {
+ mInstrumentationNames[i] = instrumentations[i].getName();
+ }
+ mHasRunnerLibrary = hasTestRunnerLibrary(manifestData);
+ }
+
+ /**
+ * Helper method to determine if given manifest has a <code>SdkConstants.LIBRARY_TEST_RUNNER
+ * </code> library reference
+ *
+ * @param manifestParser the {@link ManifestData} to search
+ * @return true if test runner library found, false otherwise
+ */
+ private boolean hasTestRunnerLibrary(ManifestData manifestData) {
+ for (UsesLibrary lib : manifestData.getUsesLibraries()) {
+ if (AdtConstants.LIBRARY_TEST_RUNNER.equals(lib.getName())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return the set of instrumentation names for the Android project.
+ *
+ * @return array of instrumentation class names, possibly empty
+ */
+ @NonNull
+ String[] getInstrumentationNames() {
+ return mInstrumentationNames;
+ }
+
+ /**
+ * Helper method to get the first instrumentation that can be used as a test runner.
+ *
+ * @return fully qualified instrumentation class name. <code>null</code> if no valid
+ * instrumentation can be found.
+ */
+ @Nullable
+ String getValidInstrumentationTestRunner() {
+ for (String instrumentation : getInstrumentationNames()) {
+ if (validateInstrumentationRunner(instrumentation) == INSTRUMENTATION_OK) {
+ return instrumentation;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper method to determine if specified instrumentation can be used as a test runner
+ *
+ * @param instrumentation the instrumentation class name to validate. Assumes this
+ * instrumentation is one of {@link #getInstrumentationNames()}
+ * @return <code>INSTRUMENTATION_OK</code> if valid, otherwise returns error message
+ */
+ String validateInstrumentationRunner(String instrumentation) {
+ if (!mHasRunnerLibrary) {
+ return String.format(LaunchMessages.InstrValidator_NoTestLibMsg_s,
+ AdtConstants.LIBRARY_TEST_RUNNER);
+ }
+ // check if this instrumentation is the standard test runner
+ if (!instrumentation.equals(SdkConstants.CLASS_INSTRUMENTATION_RUNNER)) {
+ // Ideally, we'd check if the class extends instrumentation test runner.
+ // However, the Google Instrumentation Test Runner extends Google Instrumentation, and not a test runner,
+ // so we just check that the super class is Instrumentation.
+ String result = BaseProjectHelper.testClassForManifest(mJavaProject,
+ instrumentation, SdkConstants.CLASS_INSTRUMENTATION, true);
+ if (result != BaseProjectHelper.TEST_CLASS_OK) {
+ return String.format(
+ LaunchMessages.InstrValidator_WrongRunnerTypeMsg_s,
+ SdkConstants.CLASS_INSTRUMENTATION);
+ }
+ }
+ return INSTRUMENTATION_OK;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidJUnitLaunchInfo.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidJUnitLaunchInfo.java
new file mode 100644
index 000000000..08702f4e2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidJUnitLaunchInfo.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.launch.junit.runtime;
+
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner.TestSize;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.debug.core.ILaunch;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * Contains info about Android JUnit launch
+ */
+public class AndroidJUnitLaunchInfo {
+ private final IProject mProject;
+ private final String mAppPackage;
+ private final String mRunner;
+
+ private boolean mDebugMode = false;
+ private Collection<IDevice> mDevices = Collections.EMPTY_LIST;
+ private String mTestPackage = null;
+ private String mTestClass = null;
+ private String mTestMethod = null;
+ private ILaunch mLaunch = null;
+ private TestSize mTestSize = null;
+
+ public AndroidJUnitLaunchInfo(IProject project, String appPackage, String runner) {
+ mProject = project;
+ mAppPackage = appPackage;
+ mRunner = runner;
+ }
+
+ public IProject getProject() {
+ return mProject;
+ }
+
+ public String getAppPackage() {
+ return mAppPackage;
+ }
+
+ public String getRunner() {
+ return mRunner;
+ }
+
+ public boolean isDebugMode() {
+ return mDebugMode;
+ }
+
+ public void setDebugMode(boolean debugMode) {
+ mDebugMode = debugMode;
+ }
+
+ public TestSize getTestSize() {
+ return mTestSize;
+ }
+
+ public void setTestSize(TestSize size) {
+ mTestSize = size;
+ }
+
+ public Collection<IDevice> getDevices() {
+ return mDevices;
+ }
+
+ public void setDevices(Collection<IDevice> devices) {
+ mDevices = devices;
+ }
+
+ /**
+ * Specify to run all tests within given package.
+ *
+ * @param testPackage fully qualified java package
+ */
+ public void setTestPackage(String testPackage) {
+ mTestPackage = testPackage;
+ }
+
+ /**
+ * Return the package of tests to run.
+ *
+ * @return fully qualified java package. <code>null</code> if not specified.
+ */
+ public String getTestPackage() {
+ return mTestPackage;
+ }
+
+ /**
+ * Sets the test class to run.
+ *
+ * @param testClass fully qualfied test class to run
+ * Expected format: x.y.x.testclass
+ */
+ public void setTestClass(String testClass) {
+ mTestClass = testClass;
+ }
+
+ /**
+ * Returns the test class to run.
+ *
+ * @return fully qualfied test class to run.
+ * <code>null</code> if not specified.
+ */
+ public String getTestClass() {
+ return mTestClass;
+ }
+
+ /**
+ * Sets the test method to run. testClass must also be set.
+ *
+ * @param testMethod test method to run
+ */
+ public void setTestMethod(String testMethod) {
+ mTestMethod = testMethod;
+ }
+
+ /**
+ * Returns the test method to run.
+ *
+ * @return test method to run. <code>null</code> if not specified.
+ */
+ public String getTestMethod() {
+ return mTestMethod;
+ }
+
+ public ILaunch getLaunch() {
+ return mLaunch;
+ }
+
+ public void setLaunch(ILaunch launch) {
+ mLaunch = launch;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidTestReference.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidTestReference.java
new file mode 100644
index 000000000..ec3104d91
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/AndroidTestReference.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch.junit.runtime;
+
+import org.eclipse.jdt.internal.junit.runner.ITestIdentifier;
+import org.eclipse.jdt.internal.junit.runner.ITestReference;
+import org.eclipse.jdt.internal.junit.runner.TestExecution;
+
+/**
+ * Base implementation of the Eclipse {@link ITestReference} and {@link ITestIdentifier} interfaces
+ * for Android tests.
+ * <p/>
+ * Provides generic equality/hashcode services
+ */
+@SuppressWarnings("restriction")
+abstract class AndroidTestReference implements ITestReference, ITestIdentifier {
+
+ /**
+ * Gets the {@link ITestIdentifier} for this test reference.
+ */
+ @Override
+ public ITestIdentifier getIdentifier() {
+ // this class serves as its own test identifier
+ return this;
+ }
+
+ /**
+ * Not supported.
+ */
+ @Override
+ public void run(TestExecution execution) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Compares {@link ITestIdentifier} using names
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof ITestIdentifier) {
+ ITestIdentifier testid = (ITestIdentifier) obj;
+ return getName().equals(testid.getName());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return getName().hashCode();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java
new file mode 100755
index 000000000..a48acd324
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/RemoteAdtTestRunner.java
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch.junit.runtime;
+
+import com.android.ddmlib.AdbCommandRejectedException;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.ShellCommandUnresponsiveException;
+import com.android.ddmlib.TimeoutException;
+import com.android.ddmlib.testrunner.IRemoteAndroidTestRunner.TestSize;
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.launch.LaunchMessages;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.internal.junit.runner.IListensToTestExecutions;
+import org.eclipse.jdt.internal.junit.runner.ITestReference;
+import org.eclipse.jdt.internal.junit.runner.MessageIds;
+import org.eclipse.jdt.internal.junit.runner.RemoteTestRunner;
+import org.eclipse.jdt.internal.junit.runner.TestExecution;
+import org.eclipse.jdt.internal.junit.runner.TestReferenceFailure;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Supports Eclipse JUnit execution of Android tests.
+ * <p/>
+ * Communicates back to a Eclipse JDT JUnit client via a socket connection.
+ *
+ * @see org.eclipse.jdt.internal.junit.runner.RemoteTestRunner for more details on the protocol
+ */
+@SuppressWarnings("restriction")
+public class RemoteAdtTestRunner extends RemoteTestRunner {
+
+ private static final String DELAY_MSEC_KEY = "delay_msec";
+ /** the delay between each test execution when in collecting test info */
+ private static final String COLLECT_TEST_DELAY_MS = "15";
+
+ private AndroidJUnitLaunchInfo mLaunchInfo;
+ private TestExecution mExecution;
+
+ /**
+ * Initialize the JDT JUnit test runner parameters from the {@code args}.
+ *
+ * @param args name-value pair of arguments to pass to parent JUnit runner.
+ * @param launchInfo the Android specific test launch info
+ */
+ protected void init(String[] args, AndroidJUnitLaunchInfo launchInfo) {
+ defaultInit(args);
+ mLaunchInfo = launchInfo;
+ }
+
+ /**
+ * Runs a set of tests, and reports back results using parent class.
+ * <p/>
+ * JDT Unit expects to be sent data in the following sequence:
+ * <ol>
+ * <li>The total number of tests to be executed.</li>
+ * <li>The test 'tree' data about the tests to be executed, which is composed of the set of
+ * test class names, the number of tests in each class, and the names of each test in the
+ * class.</li>
+ * <li>The test execution result for each test method. Expects individual notifications of
+ * the test execution start, any failures, and the end of the test execution.</li>
+ * <li>The end of the test run, with its elapsed time.</li>
+ * </ol>
+ * <p/>
+ * In order to satisfy this, this method performs two actual Android instrumentation runs.
+ * The first is a 'log only' run that will collect the test tree data, without actually
+ * executing the tests, and send it back to JDT JUnit. The second is the actual test execution,
+ * whose results will be communicated back in real-time to JDT JUnit.
+ *
+ * The tests are run concurrently on all devices. The overall structure is as follows:
+ * <ol>
+ * <li> First, a separate job per device is run to collect test tree data. A per device
+ * {@link TestCollector} records information regarding the tests run on the device.
+ * </li>
+ * <li> Once all the devices have finished collecting the test tree data, the tree info is
+ * collected from all of them and passed to the Junit UI </li>
+ * <li> A job per device is again launched to do the actual test run. A per device
+ * {@link TestRunListener} notifies the shared {@link TestResultsNotifier} of test
+ * status. </li>
+ * <li> As tests complete, the test run listener updates the Junit UI </li>
+ * </ol>
+ *
+ * @param testClassNames ignored - the AndroidJUnitLaunchInfo will be used to determine which
+ * tests to run.
+ * @param testName ignored
+ * @param execution used to report test progress
+ */
+ @Override
+ public void runTests(String[] testClassNames, String testName, TestExecution execution) {
+ // hold onto this execution reference so it can be used to report test progress
+ mExecution = execution;
+
+ List<IDevice> devices = new ArrayList<IDevice>(mLaunchInfo.getDevices());
+ List<RemoteAndroidTestRunner> runners =
+ new ArrayList<RemoteAndroidTestRunner>(devices.size());
+
+ for (IDevice device : devices) {
+ RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(
+ mLaunchInfo.getAppPackage(), mLaunchInfo.getRunner(), device);
+
+ if (mLaunchInfo.getTestClass() != null) {
+ if (mLaunchInfo.getTestMethod() != null) {
+ runner.setMethodName(mLaunchInfo.getTestClass(), mLaunchInfo.getTestMethod());
+ } else {
+ runner.setClassName(mLaunchInfo.getTestClass());
+ }
+ }
+
+ if (mLaunchInfo.getTestPackage() != null) {
+ runner.setTestPackageName(mLaunchInfo.getTestPackage());
+ }
+
+ TestSize size = mLaunchInfo.getTestSize();
+ if (size != null) {
+ runner.setTestSize(size);
+ }
+
+ runners.add(runner);
+ }
+
+ // Launch all test info collector jobs
+ List<TestTreeCollectorJob> collectorJobs =
+ new ArrayList<TestTreeCollectorJob>(devices.size());
+ List<TestCollector> perDeviceCollectors = new ArrayList<TestCollector>(devices.size());
+ for (int i = 0; i < devices.size(); i++) {
+ RemoteAndroidTestRunner runner = runners.get(i);
+ String deviceName = devices.get(i).getName();
+ TestCollector collector = new TestCollector(deviceName);
+ perDeviceCollectors.add(collector);
+
+ TestTreeCollectorJob job = new TestTreeCollectorJob(
+ "Test Tree Collector for " + deviceName,
+ runner, mLaunchInfo.isDebugMode(), collector);
+ job.setPriority(Job.INTERACTIVE);
+ job.schedule();
+
+ collectorJobs.add(job);
+ }
+
+ // wait for all test info collector jobs to complete
+ int totalTests = 0;
+ for (TestTreeCollectorJob job : collectorJobs) {
+ try {
+ job.join();
+ } catch (InterruptedException e) {
+ endTestRunWithError(e.getMessage());
+ return;
+ }
+
+ if (!job.getResult().isOK()) {
+ endTestRunWithError(job.getResult().getMessage());
+ return;
+ }
+
+ TestCollector collector = job.getCollector();
+ String err = collector.getErrorMessage();
+ if (err != null) {
+ endTestRunWithError(err);
+ return;
+ }
+
+ totalTests += collector.getTestCaseCount();
+ }
+
+ AdtPlugin.printToConsole(mLaunchInfo.getProject(), "Sending test information to Eclipse");
+ notifyTestRunStarted(totalTests);
+ sendTestTrees(perDeviceCollectors);
+
+ List<TestRunnerJob> instrumentationRunnerJobs =
+ new ArrayList<TestRunnerJob>(devices.size());
+
+ TestResultsNotifier notifier = new TestResultsNotifier(mExecution.getListener(),
+ devices.size());
+
+ // Spawn all instrumentation runner jobs
+ for (int i = 0; i < devices.size(); i++) {
+ RemoteAndroidTestRunner runner = runners.get(i);
+ String deviceName = devices.get(i).getName();
+ TestRunListener testRunListener = new TestRunListener(deviceName, notifier);
+ InstrumentationRunJob job = new InstrumentationRunJob(
+ "Test Tree Collector for " + deviceName,
+ runner, mLaunchInfo.isDebugMode(), testRunListener);
+ job.setPriority(Job.INTERACTIVE);
+ job.schedule();
+
+ instrumentationRunnerJobs.add(job);
+ }
+
+ // Wait for all jobs to complete
+ for (TestRunnerJob job : instrumentationRunnerJobs) {
+ try {
+ job.join();
+ } catch (InterruptedException e) {
+ endTestRunWithError(e.getMessage());
+ return;
+ }
+
+ if (!job.getResult().isOK()) {
+ endTestRunWithError(job.getResult().getMessage());
+ return;
+ }
+ }
+ }
+
+ /** Sends info about the test tree to be executed (ie the suites and their enclosed tests) */
+ private void sendTestTrees(List<TestCollector> perDeviceCollectors) {
+ for (TestCollector c : perDeviceCollectors) {
+ ITestReference ref = c.getDeviceSuite();
+ ref.sendTree(this);
+ }
+ }
+
+ private static abstract class TestRunnerJob extends Job {
+ private ITestRunListener mListener;
+ private RemoteAndroidTestRunner mRunner;
+ private boolean mIsDebug;
+
+ public TestRunnerJob(String name, RemoteAndroidTestRunner runner,
+ boolean isDebug, ITestRunListener listener) {
+ super(name);
+
+ mRunner = runner;
+ mIsDebug = isDebug;
+ mListener = listener;
+ }
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ try {
+ setupRunner();
+ mRunner.run(mListener);
+ } catch (TimeoutException e) {
+ return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID,
+ LaunchMessages.RemoteAdtTestRunner_RunTimeoutException,
+ e);
+ } catch (IOException e) {
+ return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(LaunchMessages.RemoteAdtTestRunner_RunIOException_s,
+ e.getMessage()),
+ e);
+ } catch (AdbCommandRejectedException e) {
+ return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format(
+ LaunchMessages.RemoteAdtTestRunner_RunAdbCommandRejectedException_s,
+ e.getMessage()),
+ e);
+ } catch (ShellCommandUnresponsiveException e) {
+ return new Status(Status.ERROR, AdtPlugin.PLUGIN_ID,
+ LaunchMessages.RemoteAdtTestRunner_RunTimeoutException,
+ e);
+ }
+
+ return Status.OK_STATUS;
+ }
+
+ public RemoteAndroidTestRunner getRunner() {
+ return mRunner;
+ }
+
+ public boolean isDebug() {
+ return mIsDebug;
+ }
+
+ public ITestRunListener getListener() {
+ return mListener;
+ }
+
+ protected abstract void setupRunner();
+ }
+
+ private static class TestTreeCollectorJob extends TestRunnerJob {
+ public TestTreeCollectorJob(String name, RemoteAndroidTestRunner runner, boolean isDebug,
+ TestCollector listener) {
+ super(name, runner, isDebug, listener);
+ }
+
+ @Override
+ protected void setupRunner() {
+ RemoteAndroidTestRunner runner = getRunner();
+
+ // set log only to just collect test case info,
+ // so Eclipse has correct test case count/tree info
+ runner.setLogOnly(true);
+
+ // add a small delay between each test. Otherwise for large test suites framework may
+ // report Binder transaction failures
+ runner.addInstrumentationArg(DELAY_MSEC_KEY, COLLECT_TEST_DELAY_MS);
+ }
+
+ public TestCollector getCollector() {
+ return (TestCollector) getListener();
+ }
+ }
+
+ private static class InstrumentationRunJob extends TestRunnerJob {
+ public InstrumentationRunJob(String name, RemoteAndroidTestRunner runner, boolean isDebug,
+ ITestRunListener listener) {
+ super(name, runner, isDebug, listener);
+ }
+
+ @Override
+ protected void setupRunner() {
+ RemoteAndroidTestRunner runner = getRunner();
+ runner.setLogOnly(false);
+ runner.removeInstrumentationArg(DELAY_MSEC_KEY);
+ if (isDebug()) {
+ runner.setDebug(true);
+ }
+ }
+ }
+
+ /**
+ * Main entry method to run tests
+ *
+ * @param programArgs JDT JUnit program arguments to be processed by parent
+ * @param junitInfo the {@link AndroidJUnitLaunchInfo} containing info about this test ru
+ */
+ public void runTests(String[] programArgs, AndroidJUnitLaunchInfo junitInfo) {
+ init(programArgs, junitInfo);
+ run();
+ }
+
+ /**
+ * Stop the current test run.
+ */
+ public void terminate() {
+ stop();
+ }
+
+ @Override
+ protected void stop() {
+ if (mExecution != null) {
+ mExecution.stop();
+ }
+ }
+
+ private void notifyTestRunEnded(long elapsedTime) {
+ // copy from parent - not ideal, but method is private
+ sendMessage(MessageIds.TEST_RUN_END + elapsedTime);
+ flush();
+ //shutDown();
+ }
+
+ /**
+ * @param errorMessage
+ */
+ private void reportError(String errorMessage) {
+ AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(),
+ String.format(LaunchMessages.RemoteAdtTestRunner_RunFailedMsg_s, errorMessage));
+ // is this needed?
+ //notifyTestRunStopped(-1);
+ }
+
+ private void endTestRunWithError(String message) {
+ reportError(message);
+ notifyTestRunEnded(0);
+ }
+
+ /**
+ * This class provides the interface to notify the JDT UI regarding the status of tests.
+ * When running tests on multiple devices, there is a {@link TestRunListener} that listens
+ * to results from each device. Rather than all such listeners directly notifying JDT
+ * from different threads, they all notify this class which notifies JDT. In addition,
+ * the {@link #testRunEnded(String, long)} method make sure that JDT is notified that the
+ * test run has completed only when tests on all devices have completed.
+ * */
+ private class TestResultsNotifier {
+ private final IListensToTestExecutions mListener;
+ private final int mDeviceCount;
+
+ private int mCompletedRuns;
+ private long mMaxElapsedTime;
+
+ public TestResultsNotifier(IListensToTestExecutions listener, int nDevices) {
+ mListener = listener;
+ mDeviceCount = nDevices;
+ }
+
+ public synchronized void testEnded(TestCaseReference ref) {
+ mListener.notifyTestEnded(ref);
+ }
+
+ public synchronized void testFailed(TestReferenceFailure ref) {
+ mListener.notifyTestFailed(ref);
+ }
+
+ public synchronized void testRunEnded(String mDeviceName, long elapsedTime) {
+ mCompletedRuns++;
+
+ if (elapsedTime > mMaxElapsedTime) {
+ mMaxElapsedTime = elapsedTime;
+ }
+
+ if (mCompletedRuns == mDeviceCount) {
+ notifyTestRunEnded(mMaxElapsedTime);
+ }
+ }
+
+ public synchronized void testStarted(TestCaseReference testId) {
+ mListener.notifyTestStarted(testId);
+ }
+ }
+
+ /**
+ * TestRunListener that communicates results in real-time back to JDT JUnit via the
+ * {@link TestResultsNotifier}.
+ * */
+ private class TestRunListener implements ITestRunListener {
+ private final String mDeviceName;
+ private TestResultsNotifier mNotifier;
+
+ /**
+ * Constructs a {@link ITestRunListener} that listens for test results on given device.
+ * @param deviceName device on which the tests are being run
+ * @param notifier notifier to inform of test status
+ */
+ public TestRunListener(String deviceName, TestResultsNotifier notifier) {
+ mDeviceName = deviceName;
+ mNotifier = notifier;
+ }
+
+ @Override
+ public void testEnded(TestIdentifier test, Map<String, String> ignoredTestMetrics) {
+ mNotifier.testEnded(new TestCaseReference(mDeviceName, test));
+ }
+
+ @Override
+ public void testFailed(TestIdentifier test, String trace) {
+ TestReferenceFailure failure =
+ new TestReferenceFailure(new TestCaseReference(mDeviceName, test),
+ MessageIds.TEST_FAILED, trace, null);
+ mNotifier.testFailed(failure);
+ }
+
+ @Override
+ public void testAssumptionFailure(TestIdentifier test, String trace) {
+ TestReferenceFailure failure =
+ new TestReferenceFailure(new TestCaseReference(mDeviceName, test),
+ MessageIds.TEST_FAILED, trace, null);
+ mNotifier.testFailed(failure);
+ }
+
+ @Override
+ public void testIgnored(TestIdentifier test) {
+ // TODO: implement me?
+ }
+
+ @Override
+ public synchronized void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
+ mNotifier.testRunEnded(mDeviceName, elapsedTime);
+ AdtPlugin.printToConsole(mLaunchInfo.getProject(),
+ LaunchMessages.RemoteAdtTestRunner_RunCompleteMsg);
+ }
+
+ @Override
+ public synchronized void testRunFailed(String errorMessage) {
+ reportError(errorMessage);
+ }
+
+ @Override
+ public synchronized void testRunStarted(String runName, int testCount) {
+ // ignore
+ }
+
+ @Override
+ public synchronized void testRunStopped(long elapsedTime) {
+ notifyTestRunStopped(elapsedTime);
+ AdtPlugin.printToConsole(mLaunchInfo.getProject(),
+ LaunchMessages.RemoteAdtTestRunner_RunStoppedMsg);
+ }
+
+ @Override
+ public synchronized void testStarted(TestIdentifier test) {
+ TestCaseReference testId = new TestCaseReference(mDeviceName, test);
+ mNotifier.testStarted(testId);
+ }
+ }
+
+ /** Override parent to get extra logs. */
+ @Override
+ protected boolean connect() {
+ boolean result = super.connect();
+ if (!result) {
+ AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(),
+ "Connect to Eclipse test result listener failed");
+ }
+ return result;
+ }
+
+ /** Override parent to dump error message to console. */
+ @Override
+ public void runFailed(String message, Exception exception) {
+ if (exception != null) {
+ AdtPlugin.logAndPrintError(exception, mLaunchInfo.getProject().getName(),
+ "Test launch failed: %s", message);
+ } else {
+ AdtPlugin.printErrorToConsole(mLaunchInfo.getProject(), "Test launch failed: %s",
+ message);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCaseReference.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCaseReference.java
new file mode 100644
index 000000000..e05e9b857
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCaseReference.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch.junit.runtime;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
+
+/**
+ * Reference for a single Android test method.
+ */
+@SuppressWarnings("restriction")
+class TestCaseReference extends AndroidTestReference {
+ private final String mClassName;
+ private final String mTestName;
+ private final String mDeviceName;
+
+ /**
+ * Creates a TestCaseReference from a {@link TestIdentifier}
+ * @param test
+ */
+ TestCaseReference(String deviceName, TestIdentifier test) {
+ mDeviceName = deviceName;
+ mClassName = test.getClassName();
+ mTestName = test.getTestName();
+ }
+
+ /**
+ * Returns a count of the number of test cases referenced. Is always one for this class.
+ */
+ @Override
+ public int countTestCases() {
+ return 1;
+ }
+
+ /**
+ * Sends test identifier and test count information for this test
+ *
+ * @param notified the {@link IVisitsTestTrees} to send test info to
+ */
+ @Override
+ public void sendTree(IVisitsTestTrees notified) {
+ notified.visitTreeEntry(getIdentifier(), false, countTestCases());
+ }
+
+ /**
+ * Returns the identifier of this test, in a format expected by JDT JUnit
+ */
+ @Override
+ public String getName() {
+ return String.format("%s (%s) [%s]", mTestName, mClassName, mDeviceName);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCollector.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCollector.java
new file mode 100644
index 000000000..32c5ef81e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestCollector.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch.junit.runtime;
+
+import com.android.ddmlib.testrunner.ITestRunListener;
+import com.android.ddmlib.testrunner.TestIdentifier;
+
+import java.util.Map;
+
+/**
+ * Collects info about tests to be executed by listening to the results of an Android test run.
+ */
+class TestCollector implements ITestRunListener {
+ private final String mDeviceName;
+ private final TestSuiteReference mDeviceSuiteRef;
+
+ private int mTotalTestCount;
+ /** test name to test suite reference map. */
+
+ private String mErrorMessage = null;
+
+ TestCollector(String deviceName) {
+ mDeviceName = deviceName;
+ mDeviceSuiteRef = new TestSuiteReference(deviceName);
+
+ mTotalTestCount = 0;
+ }
+
+ @Override
+ public synchronized void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testFailed(com.android.ddmlib.testrunner.TestIdentifier, java.lang.String)
+ */
+ @Override
+ public synchronized void testFailed(TestIdentifier test, String trace) {
+ // ignore - should be impossible since this is only collecting test information
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testIgnored(com.android.ddmlib.testrunner.TestIdentifier)
+ */
+ @Override
+ public synchronized void testIgnored(TestIdentifier test) {
+ // ignore - should be impossible since this is only collecting test information
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testAssumptionFailure(com.android.ddmlib.testrunner.TestIdentifier, java.lang.String)
+ */
+ @Override
+ public synchronized void testAssumptionFailure(TestIdentifier test, String trace) {
+ // ignore - should be impossible since this is only collecting test information
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunEnded(long, Map<String, String>)
+ */
+ @Override
+ public synchronized void testRunEnded(long elapsedTime, Map<String, String> runMetrics) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunFailed(java.lang.String)
+ */
+ @Override
+ public synchronized void testRunFailed(String errorMessage) {
+ mErrorMessage = errorMessage;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStarted(int)
+ */
+ @Override
+ public synchronized void testRunStarted(String ignoredRunName, int testCount) {
+ mTotalTestCount = testCount;
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testRunStopped(long)
+ */
+ @Override
+ public synchronized void testRunStopped(long elapsedTime) {
+ // ignore
+ }
+
+ /* (non-Javadoc)
+ * @see com.android.ddmlib.testrunner.ITestRunListener#testStarted(com.android.ddmlib.testrunner.TestIdentifier)
+ */
+ @Override
+ public synchronized void testStarted(TestIdentifier test) {
+ TestSuiteReference suiteRef = mDeviceSuiteRef.getTestSuite(test.getClassName());
+ if (suiteRef == null) {
+ suiteRef = new TestSuiteReference(test.getClassName());
+ mDeviceSuiteRef.addTest(suiteRef);
+ }
+
+ suiteRef.addTest(new TestCaseReference(mDeviceName, test));
+ }
+
+ /**
+ * Returns the total test count in the test run.
+ */
+ public synchronized int getTestCaseCount() {
+ return mTotalTestCount;
+ }
+
+ /**
+ * Returns the error message that was reported when collecting test info.
+ * Returns <code>null</code> if no error occurred.
+ */
+ public synchronized String getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ public TestSuiteReference getDeviceSuite() {
+ return mDeviceSuiteRef;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestSuiteReference.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestSuiteReference.java
new file mode 100644
index 000000000..dcc9f10ec
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/junit/runtime/TestSuiteReference.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.launch.junit.runtime;
+
+import org.eclipse.jdt.internal.junit.runner.IVisitsTestTrees;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Reference for an Android test suite aka class.
+ */
+@SuppressWarnings("restriction")
+class TestSuiteReference extends AndroidTestReference {
+
+ private final String mClassName;
+ private List<AndroidTestReference> mTests;
+
+ /**
+ * Creates a TestSuiteReference
+ *
+ * @param className the fully qualified name of the test class
+ */
+ TestSuiteReference(String className) {
+ mClassName = className;
+ mTests = new ArrayList<AndroidTestReference>();
+ }
+
+ /**
+ * Returns a count of the number of test cases included in this suite.
+ */
+ @Override
+ public int countTestCases() {
+ return mTests.size();
+ }
+
+ /**
+ * Sends test identifier and test count information for this test class, and all its included
+ * test methods.
+ *
+ * @param notified the {@link IVisitsTestTrees} to send test info too
+ */
+ @Override
+ public void sendTree(IVisitsTestTrees notified) {
+ notified.visitTreeEntry(getIdentifier(), true, countTestCases());
+ for (AndroidTestReference ref: mTests) {
+ ref.sendTree(notified);
+ }
+ }
+
+ /**
+ * Return the name of this test class.
+ */
+ @Override
+ public String getName() {
+ return mClassName;
+ }
+
+ /**
+ * Adds a test method to this suite.
+ *
+ * @param testRef the {@link TestCaseReference} to add
+ */
+ void addTest(AndroidTestReference testRef) {
+ mTests.add(testRef);
+ }
+
+ /** Returns the test suite of given name, null if no such test suite exists */
+ public TestSuiteReference getTestSuite(String name) {
+ for (AndroidTestReference ref: mTests) {
+ if (ref instanceof TestSuiteReference && ref.getName().equals(name)) {
+ return (TestSuiteReference) ref;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/messages.properties
new file mode 100644
index 000000000..b27a3654e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch/messages.properties
@@ -0,0 +1,43 @@
+#
+# Copyright (C) 2010 The Android Open Source Project
+#
+# Licensed under the Eclipse Public License, Version 1.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.eclipse.org/org/documents/epl-v10.php
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# generic messages that could be used by multiple classes
+LaunchDialogTitle=Android Launch
+NonAndroidProjectError=Specified project is not an Android project
+ParseFileFailure_s=Failed to parse %1$s file
+
+# specialized, class-specific messages
+AndroidJUnitLaunchAction_LaunchDesc_s=%1$s JUnit launch
+AndroidJUnitLaunchAction_LaunchFail=Failed to launch test
+AndroidJUnitLaunchAction_LaunchInstr_2s=Launching instrumentation %1$s on %2$s
+AndroidJUnitDelegate_NoRunnerConfigMsg_s=Warning: No instrumentation runner found for the launch, using %1$s
+AndroidJUnitDelegate_NoRunnerConsoleMsg_4s=%1$s does not specify a %2$s instrumentation or does not declare uses-library %3$s in its %4$s
+AndroidJUnitDelegate_NoRunnerMsg_s=%1$s is not configured correctly for running tests. See Console for details.
+AndroidJUnitDelegate_NoTargetMsg_3s=%1$s is not configured correctly for running tests:\nA targetPackage attribute for instrumentation %2$s in its %3$s could not be found\!
+AndroidJUnitTab_LoaderLabel=Instrumentation runner:
+AndroidJUnitTab_LoadInstrError_s=Failed to load instrumentations from %1$s
+AndroidJUnitTab_NoRunnerError=Instrumentation runner not specified
+AndroidJUnitTab_SizeLabel=Only run test methods annotated with:
+AndroidJUnitTab_TestContainerText=Run all tests in the selected project, or package
+InstrValidator_NoTestLibMsg_s=The application does not declare uses-library %1$s
+InstrValidator_WrongRunnerTypeMsg_s=The instrumentation runner must be of type %1$s
+RemoteAdtTestRunner_RunCompleteMsg=Test run finished
+RemoteAdtTestRunner_RunFailedMsg_s=Test run failed: %1$s
+RemoteAdtTestRunner_RunTimeoutException=Connection with device timed out.
+RemoteAdtTestRunner_RunIOException_s=Lost connection with device: %s
+RemoteAdtTestRunner_RunStoppedMsg=Test run stopped
+RemoteAdtTestRunner_RunAdbCommandRejectedException_s=Adb rejected command: %s
+RemoteAdtTestRunner_RunShellCommandUnresponsiveException=Device stopped sending output