blob: 413ccb6939321e3d35072dbbc502d82db25c8a6d [file] [log] [blame]
/*
* Copyright 2000-2010 JetBrains s.r.o.
*
* 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 org.jetbrains.android.run;
import com.android.builder.model.AndroidArtifactOutput;
import com.android.builder.model.Variant;
import com.android.ddmlib.IDevice;
import com.android.sdklib.internal.avd.AvdInfo;
import com.android.sdklib.internal.avd.AvdManager;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.project.AndroidGradleNotification;
import com.android.tools.idea.gradle.service.notification.hyperlink.NotificationHyperlink;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.android.tools.idea.model.AndroidModel;
import com.android.tools.idea.model.AndroidModuleInfo;
import com.android.tools.idea.run.CloudDebuggingTargetChooser;
import com.android.tools.idea.run.CloudTargetChooser;
import com.android.tools.idea.structure.gradle.AndroidProjectSettingsService;
import com.intellij.CommonBundle;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.Executor;
import com.intellij.execution.configurations.*;
import com.intellij.execution.executors.DefaultDebugExecutor;
import com.intellij.execution.executors.DefaultRunExecutor;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.configuration.ClasspathEditor;
import com.intellij.openapi.roots.ui.configuration.ModulesConfigurator;
import com.intellij.openapi.roots.ui.configuration.ProjectSettingsService;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.DefaultJDOMExternalizer;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.PathUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jdom.Element;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.facet.AndroidFacetConfiguration;
import org.jetbrains.android.facet.AndroidRootUtil;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.*;
import static com.android.tools.idea.gradle.util.Projects.requiredAndroidModelMissing;
import static com.android.tools.idea.run.CloudConfiguration.Kind.MATRIX;
import static com.android.tools.idea.run.CloudConfiguration.Kind.SINGLE_DEVICE;
public abstract class AndroidRunConfigurationBase extends ModuleBasedConfiguration<JavaRunConfigurationModule> {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.run.AndroidRunConfigurationBase");
private static final String GRADLE_SYNC_FAILED_ERR_MSG = "Gradle project sync failed. Please fix your project and try again.";
/**
* A map from launch configuration name to the state of devices at the time of the launch.
* We want this list of devices persisted across launches, but not across invocations of studio, so we use a static variable.
*/
private static Map<String, DeviceStateAtLaunch> ourLastUsedDevices = ContainerUtil.newConcurrentMap();
public String TARGET_SELECTION_MODE = TargetSelectionMode.EMULATOR.name();
public boolean USE_LAST_SELECTED_DEVICE = false;
public String PREFERRED_AVD = "";
public boolean USE_COMMAND_LINE = true;
public String COMMAND_LINE = "";
public boolean WIPE_USER_DATA = false;
public boolean DISABLE_BOOT_ANIMATION = false;
public String NETWORK_SPEED = "full";
public String NETWORK_LATENCY = "none";
public boolean CLEAR_LOGCAT = false;
public boolean SHOW_LOGCAT_AUTOMATICALLY = true;
public boolean FILTER_LOGCAT_AUTOMATICALLY = true;
public int SELECTED_CLOUD_MATRIX_CONFIGURATION_ID = 0;
public String SELECTED_CLOUD_MATRIX_PROJECT_ID = "";
public int SELECTED_CLOUD_DEVICE_CONFIGURATION_ID = 0;
public String SELECTED_CLOUD_DEVICE_PROJECT_ID = "";
public boolean IS_VALID_CLOUD_MATRIX_SELECTION = false; // indicates whether the selected matrix config + project combo is valid
public String INVALID_CLOUD_MATRIX_SELECTION_ERROR = ""; // specifies the error if the matrix config + project combo is invalid
public boolean IS_VALID_CLOUD_DEVICE_SELECTION = false; // indicates whether the selected cloud device config + project combo is valid
public String INVALID_CLOUD_DEVICE_SELECTION_ERROR = ""; // specifies the error if the cloud device config + project combo is invalid
public String CLOUD_DEVICE_SERIAL_NUMBER = "";
public AndroidRunConfigurationBase(final Project project, final ConfigurationFactory factory) {
super(new JavaRunConfigurationModule(project, false), factory);
}
@Override
public final void checkConfiguration() throws RuntimeConfigurationException {
JavaRunConfigurationModule configurationModule = getConfigurationModule();
configurationModule.checkForWarning();
Module module = configurationModule.getModule();
if (module == null) {
return;
}
Project project = module.getProject();
if (requiredAndroidModelMissing(project)) {
// This only shows an error message on the "Run Configuration" dialog, but does not prevent user from running app.
throw new RuntimeConfigurationException(GRADLE_SYNC_FAILED_ERR_MSG);
}
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet == null) {
throw new RuntimeConfigurationError(AndroidBundle.message("android.no.facet.error"));
}
if (facet.isLibraryProject()) {
Pair<Boolean, String> result = supportsRunningLibraryProjects(facet);
if (!result.getFirst()) {
throw new RuntimeConfigurationError(result.getSecond());
}
}
if (facet.getConfiguration().getAndroidPlatform() == null) {
throw new RuntimeConfigurationError(AndroidBundle.message("select.platform.error"));
}
if (facet.getManifest() == null) {
throw new RuntimeConfigurationError(AndroidBundle.message("android.manifest.not.found.error"));
}
if (PREFERRED_AVD.length() > 0) {
AvdManager avdManager = facet.getAvdManagerSilently();
if (avdManager == null) {
throw new RuntimeConfigurationError(AndroidBundle.message("avd.cannot.be.loaded.error"));
}
AvdInfo avdInfo = avdManager.getAvd(PREFERRED_AVD, false);
if (avdInfo == null) {
throw new RuntimeConfigurationError(AndroidBundle.message("avd.not.found.error", PREFERRED_AVD));
}
if (avdInfo.getStatus() != AvdInfo.AvdStatus.OK) {
String message = avdInfo.getErrorMessage();
message = AndroidBundle.message("avd.not.valid.error", PREFERRED_AVD) +
(message != null ? ": " + message: "") + ". Try to repair it through AVD manager";
throw new RuntimeConfigurationError(message);
}
}
checkConfiguration(facet);
}
/** Returns whether the configuration supports running library projects, and if it doesn't, then an explanation as to why it doesn't. */
protected abstract Pair<Boolean,String> supportsRunningLibraryProjects(@NotNull AndroidFacet facet);
protected abstract void checkConfiguration(@NotNull AndroidFacet facet) throws RuntimeConfigurationException;
@Override
public Collection<Module> getValidModules() {
final List<Module> result = new ArrayList<Module>();
Module[] modules = ModuleManager.getInstance(getProject()).getModules();
for (Module module : modules) {
if (AndroidFacet.getInstance(module) != null) {
result.add(module);
}
}
return result;
}
@NotNull
public TargetSelectionMode getTargetSelectionMode() {
try {
return TargetSelectionMode.valueOf(TARGET_SELECTION_MODE);
}
catch (IllegalArgumentException e) {
LOG.info(e);
return TargetSelectionMode.EMULATOR;
}
}
public void setTargetSelectionMode(@NotNull TargetSelectionMode mode) {
TARGET_SELECTION_MODE = mode.name();
}
public void setDevicesUsedInLaunch(@NotNull Set<IDevice> usedDevices, @NotNull Set<IDevice> availableDevices) {
ourLastUsedDevices.put(getName(), new DeviceStateAtLaunch(usedDevices, availableDevices));
}
@Nullable
public DeviceStateAtLaunch getDevicesUsedInLastLaunch() {
return ourLastUsedDevices.get(getName());
}
@Override
public AndroidRunningState getState(@NotNull final Executor executor, @NotNull ExecutionEnvironment env) throws ExecutionException {
final Module module = getConfigurationModule().getModule();
if (module == null) {
throw new ExecutionException("Module is not found");
}
final AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet == null) {
throw new ExecutionException(AndroidBundle.message("no.facet.error", module.getName()));
}
Project project = env.getProject();
if (requiredAndroidModelMissing(project)) {
// This prevents user from running the app.
throw new ExecutionException(GRADLE_SYNC_FAILED_ERR_MSG);
}
AndroidModel androidModel = facet.getAndroidModel();
if (androidModel != null) {
if (!androidModel.getMainArtifact().isSigned()) {
AndroidArtifactOutput output = GradleUtil.getOutput(androidModel.getMainArtifact());
String message = AndroidBundle.message("run.error.apk.not.signed", output.getMainOutputFile().getOutputFile().getName());
String title = CommonBundle.getErrorTitle();
NotificationHyperlink quickfix =
new NotificationHyperlink("open.sign.configuration", "Open Project Structure Dialog") {
@Override
protected void execute(@NotNull Project project) {
ProjectSettingsService service = ProjectSettingsService.getInstance(project);
if (service instanceof AndroidProjectSettingsService) {
((AndroidProjectSettingsService)service).openSigningConfiguration(module);
}
else {
service.openModuleSettings(module);
}
}
};
AndroidGradleNotification.getInstance(project).showBalloon(title, message, NotificationType.ERROR, quickfix);
return null;
}
}
AndroidFacetConfiguration configuration = facet.getConfiguration();
AndroidPlatform platform = configuration.getAndroidPlatform();
if (platform == null) {
Messages.showErrorDialog(project, AndroidBundle.message("specify.platform.error"), CommonBundle.getErrorTitle());
ModulesConfigurator.showDialog(project, module.getName(), ClasspathEditor.NAME);
return null;
}
boolean debug = DefaultDebugExecutor.EXECUTOR_ID.equals(executor.getId());
boolean nonDebuggableOnDevice = false;
if (debug) {
Boolean isDebuggable = AndroidModuleInfo.get(facet).isDebuggable();
nonDebuggableOnDevice = isDebuggable != null && !isDebuggable;
if (!AndroidSdkUtils.activateDdmsIfNecessary(facet.getModule().getProject())) {
return null;
}
}
if (AndroidSdkUtils.getDebugBridge(getProject()) == null) return null;
TargetChooser targetChooser = null;
switch (getTargetSelectionMode()) {
case SHOW_DIALOG:
targetChooser = new ManualTargetChooser();
break;
case EMULATOR:
targetChooser = new EmulatorTargetChooser(PREFERRED_AVD.length() > 0 ? PREFERRED_AVD : null);
break;
case USB_DEVICE:
targetChooser = new UsbDeviceTargetChooser();
break;
case CLOUD_MATRIX_TEST:
targetChooser = new CloudTargetChooser(MATRIX, SELECTED_CLOUD_MATRIX_CONFIGURATION_ID, SELECTED_CLOUD_MATRIX_PROJECT_ID);
break;
case CLOUD_DEVICE_LAUNCH:
targetChooser = new CloudTargetChooser(SINGLE_DEVICE, SELECTED_CLOUD_DEVICE_CONFIGURATION_ID, SELECTED_CLOUD_DEVICE_PROJECT_ID);
break;
case CLOUD_DEVICE_DEBUGGING:
targetChooser = new CloudDebuggingTargetChooser(CLOUD_DEVICE_SERIAL_NUMBER);
break;
default:
assert false : "Unknown target selection mode " + TARGET_SELECTION_MODE;
break;
}
final boolean supportMultipleDevices = supportMultipleDevices() && executor.getId().equals(DefaultRunExecutor.EXECUTOR_ID);
return new AndroidRunningState(env, facet, getApkProvider(), targetChooser, computeCommandLine(), getApplicationLauncher(facet),
supportMultipleDevices, CLEAR_LOGCAT, this, nonDebuggableOnDevice);
}
@NotNull
protected abstract ApkProvider getApkProvider();
@Nullable
protected static Pair<File, String> getCopyOfCompilerManifestFile(@NotNull AndroidFacet facet) throws IOException {
final VirtualFile manifestFile = AndroidRootUtil.getCustomManifestFileForCompiler(facet);
if (manifestFile == null) {
return null;
}
File tmpDir = null;
try {
tmpDir = FileUtil.createTempDirectory("android_manifest_file_for_execution", "tmp");
final File manifestCopy = new File(tmpDir, manifestFile.getName());
FileUtil.copy(new File(manifestFile.getPath()), manifestCopy);
//noinspection ConstantConditions
return Pair.create(manifestCopy, PathUtil.getLocalPath(manifestFile));
}
catch (IOException e) {
LOG.info(e);
if (tmpDir != null) {
FileUtil.delete(tmpDir);
}
throw e;
}
}
private String computeCommandLine() {
StringBuilder result = new StringBuilder();
result.append("-netspeed ").append(NETWORK_SPEED).append(' ');
result.append("-netdelay ").append(NETWORK_LATENCY).append(' ');
if (WIPE_USER_DATA) {
result.append("-wipe-data ");
}
if (DISABLE_BOOT_ANIMATION) {
result.append("-no-boot-anim ");
}
if (USE_COMMAND_LINE) {
result.append(COMMAND_LINE);
}
int last = result.length() - 1;
if (result.charAt(last) == ' ') {
result.deleteCharAt(last);
}
return result.toString();
}
@NotNull
protected abstract ConsoleView attachConsole(AndroidRunningState state, Executor executor) throws ExecutionException;
@NotNull
protected abstract AndroidApplicationLauncher getApplicationLauncher(AndroidFacet facet);
protected abstract boolean supportMultipleDevices();
@Override
public void readExternal(Element element) throws InvalidDataException {
super.readExternal(element);
readModule(element);
DefaultJDOMExternalizer.readExternal(this, element);
}
@Override
public void writeExternal(Element element) throws WriteExternalException {
super.writeExternal(element);
writeModule(element);
DefaultJDOMExternalizer.writeExternal(this, element);
}
public boolean usesSimpleLauncher() {
return true;
}
}