package com.mobilesorcery.sdk.builder.android.launch;
import java.io.File;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.eclipse.cdt.core.IBinaryParser.IBinaryObject;
import org.eclipse.cdt.debug.core.cdi.ICDISession;
import org.eclipse.cdt.debug.mi.core.IMILaunchConfigurationConstants;
import org.eclipse.cdt.internal.core.model.CModelManager;
import org.eclipse.core.resources.IFile;
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.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.PlatformUI;
import com.mobilesorcery.sdk.builder.android.Activator;
import com.mobilesorcery.sdk.builder.android.AndroidPackager;
import com.mobilesorcery.sdk.builder.android.PropertyInitializer;
import com.mobilesorcery.sdk.builder.android.launch.ADB.ProcessKiller;
import com.mobilesorcery.sdk.builder.android.launch.Emulator.IAndroidEmulatorProcess;
import com.mobilesorcery.sdk.builder.android.ui.dialogs.ConfigureAndroidSDKDialog;
import com.mobilesorcery.sdk.core.AbstractPackager;
import com.mobilesorcery.sdk.core.CoreMoSyncPlugin;
import com.mobilesorcery.sdk.core.IBuildResult;
import com.mobilesorcery.sdk.core.IBuildVariant;
import com.mobilesorcery.sdk.core.ILaunchConstants;
import com.mobilesorcery.sdk.core.IPackager;
import com.mobilesorcery.sdk.core.MoSyncProject;
import com.mobilesorcery.sdk.core.Util;
import com.mobilesorcery.sdk.core.launch.AbstractEmulatorLauncher;
import com.mobilesorcery.sdk.core.launch.IEmulatorLauncher;
import com.mobilesorcery.sdk.internal.debug.MoSyncCDebugTarget;
import com.mobilesorcery.sdk.internal.launch.EmulatorLaunchConfigurationDelegate;
import com.mobilesorcery.sdk.ui.internal.launch.MoreLaunchShortCut;
import com.mobilesorcery.sdk.ui.targetphone.ITargetPhone;
import com.mobilesorcery.sdk.ui.targetphone.TargetPhonePlugin;
import com.mobilesorcery.sdk.ui.targetphone.TargetPhoneTransportEvent;
import com.mobilesorcery.sdk.ui.targetphone.android.AndroidTargetPhone;
import com.mobilesorcery.sdk.ui.targetphone.android.AndroidTargetPhoneTransport;
public class AndroidEmulatorLauncher extends AbstractEmulatorLauncher {
public static final String SERIALNO = "serialno";
public static final String AVD_NAME = "avd";
public static final String AUTO_SELECT_AVD = "avd.auto.select";
public static final String AUTO_SELECT_DEVICE = "device.auto.select";
public static final String ID = "com.mobilesorcery.sdk.builder.android.launcher";
public AndroidEmulatorLauncher() {
super("Android Emulator");
}
@Override
public int isLaunchable(ILaunchConfiguration launchConfiguration, String mode) {
if (!isCorrectPackager(launchConfiguration)) {
return IEmulatorLauncher.UNLAUNCHABLE;
} else if (shouldAskUserForLauncher(launchConfiguration, mode)) {
return IEmulatorLauncher.REQUIRES_CONFIGURATION;
} else if (!isCorrectlyInstalled()) {
IEmulatorLauncher preferredLauncher = CoreMoSyncPlugin.getDefault().getPreferredLauncher(AndroidPackager.ID);
boolean useOtherLauncher = !shouldAskUserForLauncher(AndroidPackager.ID) && !Util.equals(preferredLauncher.getId(), ID);
return isAutoSelectLaunch(launchConfiguration, mode) && useOtherLauncher ?
IEmulatorLauncher.UNLAUNCHABLE :
IEmulatorLauncher.REQUIRES_CONFIGURATION;
} else if (hasNoAVDs()) {
return IEmulatorLauncher.REQUIRES_CONFIGURATION;
} else {
return super.isLaunchable(launchConfiguration, mode);
}
}
private boolean shouldAskUserForLauncher(ILaunchConfiguration launchConfiguration, String mode) {
return !isOnDevice(launchConfiguration) && isCorrectlyInstalled() && isAutoSelectLaunch(launchConfiguration, mode) && shouldAskUserForLauncher(AndroidPackager.ID);
}
protected boolean isCorrectlyInstalled() {
return ADB.getExternal().isValid() && Emulator.getExternal().isValid() && Android.getExternal().isValid();
}
protected boolean hasNoAVDs() {
try {
return Android.getExternal().listAVDs().isEmpty();
} catch (CoreException e) {
CoreMoSyncPlugin.getDefault().log(e);
return true;
}
}
@Override
public void launch(ILaunchConfiguration launchConfig, String mode,
ILaunch launch, int emulatorId, IProgressMonitor monitor)
throws CoreException {
ADB adb = ADB.getExternal();
String serialNumberOfDevice = null;
if (shouldLaunchOnDevice(launchConfig)) {
serialNumberOfDevice = launchOnDevice(adb, launchConfig, mode, launch, monitor);
} else {
serialNumberOfDevice = launchOnEmulator(adb, launchConfig, mode, launch, monitor);
}
if (monitor.isCanceled()) {
return;
}
startLogCat(adb);
if ("debug".equals(mode)) {
IProject project = EmulatorLaunchConfigurationDelegate.getProject(launchConfig);
setupDebugging(adb, MoSyncProject.create(project), serialNumberOfDevice, launch, mode);
}
}
private boolean shouldLaunchOnDevice(ILaunchConfiguration launchConfig) throws CoreException {
return launchConfig.getAttribute(ILaunchConstants.ON_DEVICE, false);
}
private void setupDebugging(ADB adb, MoSyncProject project, String serialNumberOfDevice, ILaunch launch, String mode) throws CoreException {
ILaunchConfiguration launchConfig = launch.getLaunchConfiguration();
fixLaunchConfig(launchConfig, serialNumberOfDevice);
String arch = adb.matchAbi(serialNumberOfDevice, ADB.ABIS);
IBuildVariant variant = EmulatorLaunchConfigurationDelegate.getVariant(launchConfig, mode);
File executable = project.getBuildState(variant).getBuildResult().getBuildResult().get(IBuildResult.MAIN).get(0);
boolean debug = AbstractPackager.shouldUseDebugRuntimes(project, variant);
// We need to pull some things to help the debugger.
String libPath = AndroidPackager.computeNativeDebugLib(project, variant, arch).getParentFile().getAbsolutePath();
File binary = new File(AndroidPackager.computeNativeBuildResult(project, variant, arch, debug).getParent(), "app_process");
adb.pull(serialNumberOfDevice, "/system/bin/app_process", libPath);
adb.pull(serialNumberOfDevice, "/system/bin/linker", libPath);
adb.pull(serialNumberOfDevice, "/system/lib/libc.so", libPath);
AndroidNDKDebugger dbg = new AndroidNDKDebugger();
dbg.setSerialNumber(serialNumberOfDevice);
ICDISession targetSession = dbg.createSession(launch, executable, new NullProgressMonitor());
IFile[] binaryFiles = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocation(new Path(binary.getAbsolutePath()));
IFile binaryFile = null;
for (int i = 0; binaryFile == null && i < binaryFiles.length; i++) {
if (binaryFiles[i].getProject().equals(project.getWrappedProject())) {
binaryFile = binaryFiles[i];
}
}
IBinaryObject binaryObject = (IBinaryObject) CModelManager.getDefault().createBinaryFile(binaryFile);
IDebugTarget debugTarget = MoSyncCDebugTarget.newDebugTarget(launch, project.getWrappedProject(),
targetSession.getTargets()[0], launch.getLaunchConfiguration().getName(),
null, binaryObject, true, false, null, true);
}
private String launchOnDevice(ADB adb, ILaunchConfiguration launchConfig, String mode,
ILaunch launch, IProgressMonitor monitor) throws CoreException {
String serialNumberOfDevice = getSerialNumber(adb, launchConfig);
MoSyncProject project = MoSyncProject.create(EmulatorLaunchConfigurationDelegate.getProject(launchConfig));
doLaunch(adb, project, serialNumberOfDevice, launchConfig, mode, monitor);
return serialNumberOfDevice;
}
private String launchOnEmulator(ADB adb, ILaunchConfiguration launchConfig,
String mode, ILaunch launch, IProgressMonitor monitor) throws CoreException {
Android android = Android.getExternal();
android.refresh();
Emulator emulator = Emulator.getExternal();
emulator.assertValid();
String avd = getAVD(android, launchConfig);
if (Util.isEmpty(avd)) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, MessageFormat.format("No AVD specified (modify your launch configuration).", avd)));
}
if (!android.hasAVD(avd)) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, MessageFormat.format("No AVD found with name {0} (modify your launch configuration).", avd)));
}
boolean mightNeedADBRestart = mightNeedADBRestart(adb, emulator);
if (mightNeedADBRestart) {
adb.killServer();
}
List<IAndroidEmulatorProcess> runningEmulators = emulator.getRunningProcesses(avd);
IAndroidEmulatorProcess process = runningEmulators.size() > 0 ? runningEmulators.get(0) : null;
if (process == null) {
process = emulator.start(avd, true);
}
DebugPlugin.newProcess(launch, process.getNativeProcess(), MessageFormat.format("Android Emulator ''{0}''", avd));
// We need to wait until we're started.
process.awaitEmulatorStarted(2, TimeUnit.MINUTES);
IProject project = EmulatorLaunchConfigurationDelegate.getProject(launchConfig);
String serialNumberOfDevice = process.getEmulatorId();
doLaunch(adb, MoSyncProject.create(project), serialNumberOfDevice, launchConfig, mode, monitor);
return serialNumberOfDevice;
}
private void doLaunch(ADB adb, MoSyncProject project, String serialNumberOfDevice, ILaunchConfiguration launchConfig, String mode, IProgressMonitor monitor) throws CoreException {
File packageToInstall = getPackageToInstall(launchConfig, mode);
if (packageToInstall != null) {
adb.install(packageToInstall, project.getProperty(PropertyInitializer.ANDROID_PACKAGE_NAME), serialNumberOfDevice, new ProcessKiller(monitor));
if (!monitor.isCanceled()) {
if (shouldLaunchOnDevice(launchConfig)) {
// TODO: This one is here because some plugins rely on this event, and only for devices.
// I think we want a better mechanism though.
IBuildVariant variant = EmulatorLaunchConfigurationDelegate.getVariant(launchConfig, mode);
AndroidTargetPhone phone = getDevice(serialNumberOfDevice);
TargetPhonePlugin.getDefault().notifyListeners(new TargetPhoneTransportEvent(TargetPhoneTransportEvent.ABOUT_TO_LAUNCH, phone, project, variant));
}
adb.launch(Activator.getAndroidComponentName(project), serialNumberOfDevice, new ProcessKiller(monitor));
}
} else {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "Project not built or build failed"));
}
}
private AndroidTargetPhone getDevice(String serialNumberOfDevice) {
List<ITargetPhone> history = TargetPhonePlugin.getDefault().getSelectedTargetPhoneHistory();
for (ITargetPhone phone : history) {
if (phone instanceof AndroidTargetPhone) {
AndroidTargetPhone androidPhone = (AndroidTargetPhone) phone;
if (serialNumberOfDevice.equals(androidPhone.getSerialNumber())) {
return androidPhone;
}
}
}
return new AndroidTargetPhone(serialNumberOfDevice, serialNumberOfDevice, AndroidTargetPhoneTransport.ID);
}
private String getSerialNumber(ADB adb, ILaunchConfiguration launchConfig) throws CoreException {
boolean autoSelectDevice = launchConfig.getAttribute(AUTO_SELECT_DEVICE, false);
if (autoSelectDevice) {
List<String> serialNumbers = adb.listDeviceSerialNumbers(false);
if (serialNumbers.isEmpty()) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "No devices connected"));
}
if (serialNumbers.size() > 1) {
throw new CoreException(new Status(IStatus.ERROR, Activator.PLUGIN_ID, "More than one device connected"));
}
return serialNumbers.get(0);
}
String serialNumberOfDevice = launchConfig.getAttribute(SERIALNO, "none");
return serialNumberOfDevice;
}
private boolean mightNeedADBRestart(ADB adb, Emulator emulator) throws CoreException {
// Sometimes we loose the adb connection and then we will not see any emulators.
// But since we keep track of emulator processes we can detect this condition
// if at least one process is running. Which is ok, since that's usually when
// the problem manifests itself
List<String> adbEmulators = adb.listEmulators(false);
List<IAndroidEmulatorProcess> runningProcesses = emulator.getAllRunningProcesses();
// So... if no emulators according to adb but process according to ourselves -> restart!
if (CoreMoSyncPlugin.getDefault().isDebugging()) {
CoreMoSyncPlugin.trace("ADB processes: {0}\nEmulator tracked processes: {1}", adbEmulators, runningProcesses);
}
return (adbEmulators.isEmpty() && !runningProcesses.isEmpty());
}
private String getAVD(Android android, ILaunchConfiguration launchConfig) throws CoreException {
boolean autoSelect = launchConfig.getAttribute(AUTO_SELECT_AVD, true);
if (autoSelect) {
List<AVD> avds = android.listAVDs();
AVD bestMatch = null;
for (AVD avd : avds) {
int apiLevel = avd.getAPILevel();
if (bestMatch == null || apiLevel > bestMatch.getAPILevel()) {
bestMatch = avd;
}
}
return bestMatch == null ? null : bestMatch.getName();
} else {
return launchConfig.getAttribute(AVD_NAME, "");
}
}
private void startLogCat(ADB adb) throws CoreException {
adb.startLogCat();
}
@Override
public void setDefaultAttributes(ILaunchConfigurationWorkingCopy wc) {
wc.setAttribute(AUTO_SELECT_AVD, true);
}
@Override
public IEmulatorLauncher configure(ILaunchConfiguration config, String mode) {
Display d = PlatformUI.getWorkbench().getDisplay();
// So any changes to the AVDs will be propagated.
Android.getExternal().refresh();
// If we are not auto-select, don't fallback to MoRe.
final boolean isAutomaticLaunch = isAutoSelectLaunch(config, mode);
// And if we are supposed to ask the user, we do not really need to configure anything.
final boolean needsConfig = !shouldAskUserForLauncher(config, mode);
final IEmulatorLauncher[] result = new IEmulatorLauncher[] { null };
d.syncExec(new Runnable() {
@Override
public void run() {
// OK, figure out after 2.6 release where to really put this ui stuff!
Shell shell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell();
if (needsConfig && isCorrectlyInstalled() && hasNoAVDs()) {
result[0] = showAutoCreateAVD(shell);
} else {
result[0] = showConfigureDialog(shell, isAutomaticLaunch, needsConfig);
}
}
});
return result[0];
}
protected IEmulatorLauncher showAutoCreateAVD(Shell shell) {
boolean goAhead = MessageDialog.openConfirm(shell, "No AVD installed",
"No AVD (Android Virtual Device) has been installed into your Android SDK.\n" +
"Would you like to launch the Android AVD manager to help you create an AVD?");
if (goAhead) {
try {
Android.getExternal().launchUI(true);
} catch (CoreException e) {
CoreMoSyncPlugin.getDefault().log(e);
}
}
return null; // Cancel execution
}
protected IEmulatorLauncher showConfigureDialog(Shell shell, boolean showFallbackAlternative, boolean needsConfig) {
ConfigureAndroidSDKDialog dialog = new ConfigureAndroidSDKDialog(shell);
dialog.setIsAutomaticSelection(showFallbackAlternative);
dialog.setNeedsConfig(needsConfig);
dialog.open();
return dialog.getSelectedLauncher();
}
@Override
public int getLaunchType(IPackager packager) {
return Util.equals(packager.getId(), AndroidPackager.ID) ? LAUNCH_TYPE_NATIVE : LAUNCH_TYPE_NONE;
}
public static ILaunchConfiguration findLaunchConfiguration(
MoSyncProject project, IBuildVariant variant,
String mode, boolean onDevice, String serialNumberOfDevice) throws CoreException {
final HashMap<String, Object> params = new HashMap<String, Object>();
params.put(AndroidEmulatorLauncher.SERIALNO, serialNumberOfDevice);
params.put(IDebugUIConstants.ATTR_PRIVATE, true);
params.put(ILaunchConstants.ON_DEVICE, onDevice);
// It seems we have to have this one here, if we create our own
// library initialization in the AndroidNDKDebugger class, we'll
// get mysterious errors.
params.put(IMILaunchConfigurationConstants.ATTR_DEBUGGER_SOLIB_PATH,
AndroidNDKDebugger.getLibraryPaths(project, variant, serialNumberOfDevice));
return MoreLaunchShortCut.getDefault().findLaunchConfiguration(project.getWrappedProject(), mode, params);
}
@Deprecated
private void fixLaunchConfig(ILaunchConfiguration launchConfig, String serialNumberOfDevice) throws CoreException {
// TODO: Separate the shared launch config into separate ones, so
// we do not have to resort to this kind of hack.
// Also note that the ATTR_DEBUGGER_SOLIB_PATH varies depending on platform, etc,
// so we really want separate launch configs the minute we start supporting
// native debugging for other platforms.
List solibPath = launchConfig.getAttribute(IMILaunchConfigurationConstants.ATTR_DEBUGGER_SOLIB_PATH,
Collections.EMPTY_LIST);
if (solibPath.isEmpty()) {
ILaunchConfigurationWorkingCopy wc = launchConfig.getWorkingCopy();
MoSyncProject project = MoSyncProject.create(EmulatorLaunchConfigurationDelegate.getProject(launchConfig));
IBuildVariant variant = EmulatorLaunchConfigurationDelegate.getVariant(launchConfig, "debug");
wc.setAttribute(IMILaunchConfigurationConstants.ATTR_DEBUGGER_SOLIB_PATH,
AndroidNDKDebugger.getLibraryPaths(project, variant, serialNumberOfDevice));
wc.doSave();
}
}
}