/* * 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.motorola.studio.android.launch; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Status; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.ui.DebugUITools; import org.eclipse.debug.ui.IDebugUIConstants; import org.eclipse.debug.ui.ILaunchGroup; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.osgi.util.NLS; import org.eclipse.sequoyah.device.framework.model.IInstance; import org.eclipse.sequoyah.device.framework.model.handler.ServiceHandler; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.console.ConsolePlugin; import org.eclipse.ui.console.IConsole; import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.launch.AndroidLaunch; import com.android.ide.eclipse.adt.internal.launch.AndroidLaunchController; import com.android.ide.eclipse.adt.internal.launch.LaunchConfigDelegate; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.io.IFolderWrapper; import com.android.sdklib.xml.AndroidManifestParser; import com.android.sdklib.xml.ManifestData; import com.android.sdklib.xml.ManifestData.Activity; import com.motorola.studio.android.adt.DDMSFacade; import com.motorola.studio.android.adt.SdkUtils; import com.motorola.studio.android.adt.StudioAndroidEventManager; import com.motorola.studio.android.common.log.StudioLogger; import com.motorola.studio.android.common.preferences.DialogWithToggleUtils; import com.motorola.studio.android.emulator.EmulatorPlugin; import com.motorola.studio.android.emulator.core.devfrm.DeviceFrameworkManager; import com.motorola.studio.android.emulator.core.model.IAndroidEmulatorInstance; import com.motorola.studio.android.launch.i18n.LaunchNLS; import com.motorola.studio.android.launch.ui.StartedInstancesDialog; /** * DESCRIPTION: This class is responsible to execute the launch process <br> * RESPONSIBILITY: Perform application launch on a device. <br> * COLABORATORS: none <br> */ @SuppressWarnings("restriction") public class StudioAndroidConfigurationDelegate extends LaunchConfigDelegate { private static final String ERRONEOUS_LAUNCH_CONFIGURATION = "erroneous.launch.config.dialog"; private static final String NO_COMPATIBLE_DEVICE = "no.compatible.device.dialog"; IAndroidEmulatorInstance compatibleInstance = null; IAndroidEmulatorInstance initialEmulatorInstance = null; public List<Client> waitingDebugger = new ArrayList<Client>(); private class RunAsClientListener implements IClientChangeListener { /** * */ private final IAndroidEmulatorInstance instance; /** * */ private final String appToLaunch; /** * * @param instance */ RunAsClientListener(IAndroidEmulatorInstance instance, String appToLaunch) { this.instance = instance; this.appToLaunch = appToLaunch; } /* * (non-Javadoc) * @see com.android.ddmlib.AndroidDebugBridge.IClientChangeListener#clientChanged(com.android.ddmlib.Client, int) */ public void clientChanged(Client client, int changeMask) { 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)) { String serialNumber = client.getDevice().getSerialNumber(); String avdName = DDMSFacade.getNameBySerialNumber(serialNumber); if ((instance != null) && instance.getName().equals(avdName)) { StudioLogger.info(StudioAndroidConfigurationDelegate.class, "Delegating launch session to ADT... "); synchronized (StudioAndroidConfigurationDelegate.this) { StudioAndroidConfigurationDelegate.this.notify(); } } } Client removeClient = null; for (Client waiting : waitingDebugger) { int pid = waiting.getClientData().getPid(); if (pid == client.getClientData().getPid()) { client.getDebuggerListenPort(); synchronized (StudioAndroidConfigurationDelegate.this) { StudioAndroidConfigurationDelegate.this.notify(); } removeClient = waiting; break; } } if (removeClient != null) { waitingDebugger.remove(removeClient); } } } if ((changeMask & Client.CHANGE_DEBUGGER_STATUS) == Client.CHANGE_DEBUGGER_STATUS) { ClientData clientData = client.getClientData(); String applicationName = clientData.getClientDescription(); if (clientData.getDebuggerConnectionStatus() == ClientData.DebuggerStatus.DEFAULT) { if (((appToLaunch != null) && (applicationName != null)) && applicationName.equals(appToLaunch.substring(0, appToLaunch.lastIndexOf(".")))) { client.getDebuggerListenPort(); synchronized (StudioAndroidConfigurationDelegate.this) { StudioAndroidConfigurationDelegate.this.notify(); } } else if (appToLaunch != null) { waitingDebugger.add(client); } } } } } /* * (non-Javadoc) * @see org.eclipse.debug.core.model.LaunchConfigurationDelegate#preLaunchCheck(org.eclipse.debug.core.ILaunchConfiguration, java.lang.String, org.eclipse.core.runtime.IProgressMonitor) */ @Override public boolean preLaunchCheck(ILaunchConfiguration configuration, String mode, IProgressMonitor monitor) throws CoreException { initialEmulatorInstance = null; boolean isOk = super.preLaunchCheck(configuration, mode, monitor); if (isOk) { final String instanceName = configuration.getAttribute( ILaunchConfigurationConstants.ATTR_DEVICE_INSTANCE_NAME, (String) null); // we found an instance if ((instanceName != null) && (instanceName.length() > 0)) { IAndroidEmulatorInstance instance = DeviceFrameworkManager.getInstance().getInstanceByName(instanceName); if (instance == null) { String serialNumber = LaunchUtils.getSerialNumberForInstance(instanceName); if (!DDMSFacade.isDeviceOnline(serialNumber)) { isOk = false; handleErrorDuringLaunch(configuration, mode, instanceName); } } else { if (!instance.isAvailable()) { isOk = false; handleErrorDuringLaunch(configuration, mode, instanceName); } if (!instance.isStarted()) { initialEmulatorInstance = instance; //updates the compatible instance with user response isOk = checkForCompatibleRunningInstances(configuration); } } } else { isOk = false; handleErrorDuringLaunch(configuration, mode, null); } } // validate if the project isn't a library project if (isOk) { String projectName = configuration.getAttribute(ILaunchConfigurationConstants.ATTR_PROJECT_NAME, (String) null); if (projectName != null) { IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); if ((project != null) && SdkUtils.isLibraryProject(project)) { handleProjectError(configuration, project, mode); isOk = false; } } } return isOk; } private void handleProjectError(final ILaunchConfiguration config, final IProject project, final String mode) { PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { public void run() { Shell shell = LaunchUtils.getActiveWorkbenchShell(); String message = LaunchNLS.UI_LaunchConfigurationTab_ERR_PROJECT_IS_LIBRARY; String prefKey = ERRONEOUS_LAUNCH_CONFIGURATION; DialogWithToggleUtils.showInformation(prefKey, LaunchNLS.ERR_LaunchConfigurationShortcut_MsgTitle, message); StructuredSelection struturedSelection; String groupId = IDebugUIConstants.ID_RUN_LAUNCH_GROUP; ILaunchGroup group = DebugUITools.getLaunchGroup(config, mode); groupId = group.getIdentifier(); struturedSelection = new StructuredSelection(config); DebugUITools.openLaunchConfigurationDialogOnGroup(shell, struturedSelection, groupId); } }); } private void handleErrorDuringLaunch(final ILaunchConfiguration config, final String mode, final String instanceName) { PlatformUI.getWorkbench().getDisplay().asyncExec(new Runnable() { public void run() { Shell shell = LaunchUtils.getActiveWorkbenchShell(); String message = instanceName != null ? NLS.bind( LaunchNLS.ERR_LaunchDelegate_InvalidDeviceInstance, instanceName) : NLS.bind(LaunchNLS.ERR_LaunchDelegate_No_Compatible_Device, config.getName()); String prefKey = instanceName != null ? ERRONEOUS_LAUNCH_CONFIGURATION : NO_COMPATIBLE_DEVICE; DialogWithToggleUtils.showInformation(prefKey, LaunchNLS.ERR_LaunchConfigurationShortcut_MsgTitle, message); StructuredSelection struturedSelection; String groupId = IDebugUIConstants.ID_RUN_LAUNCH_GROUP; ILaunchGroup group = DebugUITools.getLaunchGroup(config, mode); groupId = group.getIdentifier(); struturedSelection = new StructuredSelection(config); DebugUITools.openLaunchConfigurationDialogOnGroup(shell, struturedSelection, groupId); } }); } /** * Launches an Android application based on the given launch configuration. */ @Override public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor) throws CoreException { //use a working copy because it can be changed and these changes should not be propagated to the original copy ILaunchConfigurationWorkingCopy configurationWorkingCopy = configuration.getWorkingCopy(); StudioLogger.info(StudioAndroidConfigurationDelegate.class, "Launch Android Application using Studio for Android wizard. Configuration: " + configurationWorkingCopy + " mode:" + mode + " launch: " + launch); try { String projectName = configurationWorkingCopy.getAttribute( ILaunchConfigurationConstants.ATTR_PROJECT_NAME, (String) null); int launchAction = configurationWorkingCopy.getAttribute( ILaunchConfigurationConstants.ATTR_LAUNCH_ACTION, ILaunchConfigurationConstants.ATTR_LAUNCH_ACTION_DEFAULT); String instanceName = configurationWorkingCopy.getAttribute( ILaunchConfigurationConstants.ATTR_DEVICE_INSTANCE_NAME, (String) null); if ((projectName != null) && (instanceName != null)) { IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); if (project == null) { IStatus status = new Status(Status.ERROR, LaunchPlugin.PLUGIN_ID, "Could not retrieve project: " + projectName); throw new CoreException(status); } String appToLaunch = null; if (launchAction == ILaunchConfigurationConstants.ATTR_LAUNCH_ACTION_DEFAULT) { ManifestData manifestParser = AndroidManifestParser.parse(new IFolderWrapper(project)); Activity launcherActivity = manifestParser.getLauncherActivity(); String activityName = null; if (launcherActivity != null) { activityName = launcherActivity.getName(); } // if there's no default activity. Then there's nothing to be launched. if (activityName != null) { appToLaunch = activityName; } } // case for a specific activity else if (launchAction == ILaunchConfigurationConstants.ATTR_LAUNCH_ACTION_ACTIVITY) { appToLaunch = configurationWorkingCopy.getAttribute( ILaunchConfigurationConstants.ATTR_ACTIVITY, (String) null); if ((appToLaunch == null) || "".equals(appToLaunch)) { IStatus status = new Status( Status.ERROR, LaunchPlugin.PLUGIN_ID, "Activity field cannot be empty. Specify an activity or use the default activity on launch configuration."); throw new CoreException(status); } } // for the do nothing case there is nothing to do IAndroidEmulatorInstance emuInstance = DeviceFrameworkManager.getInstance().getInstanceByName(instanceName); RunAsClientListener list = null; //if initialEmulatorInstance is not null it means that it was offline and user has interacted with StartedInstancesDialog. //The emuInstance variable should be overrided by the initialEmulatorInstance because the emuInstance can be the new //user choice (in case he has selected the check box in dialog asking to update the run configuration) if (initialEmulatorInstance != null) { emuInstance = initialEmulatorInstance; } try { if (appToLaunch != null) { list = new RunAsClientListener(emuInstance, appToLaunch); StudioAndroidEventManager.asyncAddClientChangeListener(list); } // The instance from the launch configuration is an emulator (because the query returned // something different from null) and is not started. if ((emuInstance != null) && (!emuInstance.isStarted())) { if (compatibleInstance != null) { emuInstance = compatibleInstance; instanceName = emuInstance.getName(); configurationWorkingCopy.setAttribute( ILaunchConfigurationConstants.ATTR_DEVICE_INSTANCE_NAME, emuInstance.getName()); configurationWorkingCopy.setAttribute( ILaunchConfigurationConstants.ATTR_ADT_DEVICE_INSTANCE_NAME, emuInstance.getName()); } else { startEmuInstance(emuInstance); } } StudioLogger.info(StudioAndroidConfigurationDelegate.class, "AVD where the application will be executed: " + instanceName); String serialNumber = LaunchUtils.getSerialNumberForInstance(instanceName); if (serialNumber == null) { IStatus status = new Status(Status.ERROR, LaunchPlugin.PLUGIN_ID, "Could not retrieve AVD instance: " + instanceName); throw new CoreException(status); } bringConsoleView(); // Determining if it is an emulator or handset and creating the description //to be used for usage data collection String descriptionToLog = ""; if (emuInstance != null) { descriptionToLog = StudioLogger.VALUE_EMULATOR; } else { if ((serialNumber != null) && (!serialNumber.equals(""))) { descriptionToLog = StudioLogger.VALUE_HANDSET; } } if (!descriptionToLog.equals("")) { descriptionToLog = StudioLogger.KEY_DEVICE_TYPE + descriptionToLog + StudioLogger.SEPARATOR; } descriptionToLog = descriptionToLog + StudioLogger.KEY_USE_VDL; descriptionToLog = descriptionToLog + StudioLogger.VALUE_NO; super.launch(configurationWorkingCopy, mode, launch, monitor); // Collecting usage data for statistical purposes try { String prjTarget = ""; if (project != null) { prjTarget = Sdk.getCurrent().getTarget(project).getName(); } if (!descriptionToLog.equals("")) { descriptionToLog = descriptionToLog + StudioLogger.SEPARATOR; } descriptionToLog = descriptionToLog + StudioLogger.KEY_PRJ_TARGET + prjTarget; if (emuInstance != null) { String emuTarget = emuInstance.getTarget(); descriptionToLog = descriptionToLog + StudioLogger.SEPARATOR; descriptionToLog = descriptionToLog + StudioLogger.KEY_TARGET + emuTarget; } StudioLogger.collectUsageData(mode, StudioLogger.KIND_APP_MANAGEMENT, descriptionToLog, LaunchPlugin.PLUGIN_ID, LaunchPlugin.getDefault() .getBundle().getVersion().toString()); } catch (Throwable e) { //Do nothing, but error on the log should never prevent app from working } } finally { if (list != null) { StudioAndroidEventManager.asyncRemoveClientChangeListener(list); } StudioAndroidEventManager.asyncAddClientChangeListener(AndroidLaunchController .getInstance()); } } else { throw new CoreException(new Status(IStatus.ERROR, LaunchPlugin.PLUGIN_ID, "Missing parameters for launch")); } } catch (CoreException e) { AndroidLaunch androidLaunch = (AndroidLaunch) launch; androidLaunch.stopLaunch(); StudioLogger.error(StudioAndroidConfigurationDelegate.class, "Error while lauching " + configurationWorkingCopy.getName(), e); throw e; } catch (Exception e) { StudioLogger.error(LaunchUtils.class, "An error occurred trying to parse AndroidManifest", e); } finally { if (mode.equals(ILaunchManager.RUN_MODE)) { AndroidLaunch androidLaunch = (AndroidLaunch) launch; androidLaunch.stopLaunch(); } } } /** * @param project * @param emuInstance * @throws CoreException */ private boolean checkForCompatibleRunningInstances(ILaunchConfiguration configuration) throws CoreException { IProject project = null; compatibleInstance = null; final String projectName = configuration.getAttribute(ILaunchConfigurationConstants.ATTR_PROJECT_NAME, (String) null); project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName); if (project == null) { IStatus status = new Status(Status.ERROR, LaunchPlugin.PLUGIN_ID, "Could not retrieve project: " + projectName); throw new CoreException(status); } //Check if there is a compatible instance running to launch the app Collection<IAndroidEmulatorInstance> startedInstances = DeviceFrameworkManager.getInstance().getAllStartedInstances(); final Collection<IAndroidEmulatorInstance> compatibleStartedInstances = new HashSet<IAndroidEmulatorInstance>(); boolean continueLaunch = true; for (IAndroidEmulatorInstance i : startedInstances) { IStatus resultStatus = LaunchUtils.isCompatible(project, i.getName()); if ((resultStatus.getSeverity() == Status.OK) || (resultStatus.getSeverity() == Status.WARNING)) { compatibleStartedInstances.add(i); } } if (compatibleStartedInstances.size() > 0) { //show a dialog with compatible running instances so the user can //choose one to run the app, or he can choose to run the preferred AVD StartedInstancesDialogProxy proxy = new StartedInstancesDialogProxy(compatibleStartedInstances, configuration, project); PlatformUI.getWorkbench().getDisplay().syncExec(proxy); compatibleInstance = proxy.getSelectedInstance(); continueLaunch = proxy.continueLaunch(); } return continueLaunch; } private class StartedInstancesDialogProxy implements Runnable { private IAndroidEmulatorInstance selectedInstance = null; private boolean continueLaunch = true; private final ILaunchConfiguration configuration; Collection<IAndroidEmulatorInstance> compatibleStartedInstances = null; IProject project = null; /** * */ public StartedInstancesDialogProxy( Collection<IAndroidEmulatorInstance> compatibleStartedInstances, ILaunchConfiguration configuration, IProject project) { this.compatibleStartedInstances = compatibleStartedInstances; this.configuration = configuration; this.project = project; } public void run() { Shell aShell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); Shell shell = new Shell(aShell); StartedInstancesDialog dialog; try { dialog = new StartedInstancesDialog(shell, compatibleStartedInstances, configuration, project); dialog.setBlockOnOpen(true); dialog.open(); selectedInstance = null; if (dialog.getReturnCode() == IDialogConstants.OK_ID) { selectedInstance = dialog.getSelectedInstance(); } else if (dialog.getReturnCode() == IDialogConstants.ABORT_ID) { continueLaunch = false; } } catch (CoreException e) { StudioLogger.error(StudioAndroidConfigurationDelegate.class, "It was not possible to open Started Instance Dialog", e); } } public IAndroidEmulatorInstance getSelectedInstance() { return selectedInstance; } public boolean continueLaunch() { return continueLaunch; } } /** * Bring Console View to the front and activate the appropriate stream * */ private void bringConsoleView() { IConsole activeConsole = null; IConsole[] consoles = ConsolePlugin.getDefault().getConsoleManager().getConsoles(); for (IConsole console : consoles) { if (console.getName().equals(ILaunchConfigurationConstants.ANDROID_CONSOLE_ID)) { activeConsole = console; } } // Bring Console View to the front if (activeConsole != null) { ConsolePlugin.getDefault().getConsoleManager().showConsoleView(activeConsole); } } /** * * @param instance * @throws CoreException */ private void startEmuInstance(IAndroidEmulatorInstance instance) throws CoreException { StudioLogger.info(StudioAndroidConfigurationDelegate.class, "Needs to Start the AVD instance before launching... "); ServiceHandler startHandler = EmulatorPlugin.getStartServiceHandler(); IStatus status = startHandler.run((IInstance) instance, null, new NullProgressMonitor()); StudioLogger.info(StudioAndroidConfigurationDelegate.class, "Status of the launch service: " + status); if (status.getSeverity() == Status.ERROR) { throw new CoreException(status); } else if (status.getSeverity() == Status.CANCEL) { StudioLogger.info(StudioAndroidConfigurationDelegate.class, "Abort launch session because the AVD start was canceled. "); return; } if (!instance.isStarted()) { status = new Status(Status.ERROR, LaunchPlugin.PLUGIN_ID, "The Android Virtual Device is not started: " + instance.getName()); throw new CoreException(status); } synchronized (this) { try { wait(); } catch (InterruptedException e) { StudioLogger.info("Could not wait: ", e.getMessage()); } } } }