diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/launch')
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 |