| // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. |
| |
| package com.android.tools.idea.run; |
| |
| import static com.android.AndroidProjectTypes.PROJECT_TYPE_APP; |
| import static com.android.AndroidProjectTypes.PROJECT_TYPE_DYNAMIC_FEATURE; |
| import static com.android.AndroidProjectTypes.PROJECT_TYPE_FEATURE; |
| import static com.android.AndroidProjectTypes.PROJECT_TYPE_INSTANTAPP; |
| import static com.android.AndroidProjectTypes.PROJECT_TYPE_LIBRARY; |
| import static com.android.AndroidProjectTypes.PROJECT_TYPE_TEST; |
| |
| import com.android.ddmlib.IDevice; |
| import com.android.tools.idea.flags.StudioFlags; |
| import com.android.tools.idea.gradle.run.AndroidDeviceSpecUtil; |
| import com.android.tools.idea.project.AndroidProjectInfo; |
| import com.android.tools.idea.projectsystem.ProjectSystemUtil; |
| import com.android.tools.idea.run.editor.AndroidDebugger; |
| import com.android.tools.idea.run.editor.AndroidDebuggerContext; |
| import com.android.tools.idea.run.editor.AndroidDebuggerState; |
| import com.android.tools.idea.run.editor.AndroidJavaDebugger; |
| import com.android.tools.idea.run.editor.DeployTarget; |
| import com.android.tools.idea.run.editor.DeployTargetContext; |
| import com.android.tools.idea.run.editor.DeployTargetProvider; |
| import com.android.tools.idea.run.editor.DeployTargetState; |
| import com.android.tools.idea.run.editor.ProfilerState; |
| import com.android.tools.idea.run.tasks.LaunchTask; |
| import com.android.tools.idea.run.util.LaunchStatus; |
| import com.android.tools.idea.run.util.LaunchUtils; |
| import com.android.tools.idea.stats.RunStats; |
| import com.android.tools.idea.stats.RunStatsService; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Ordering; |
| import com.google.common.util.concurrent.Futures; |
| import com.intellij.execution.ExecutionException; |
| import com.intellij.execution.Executor; |
| import com.intellij.execution.configurations.ConfigurationFactory; |
| import com.intellij.execution.configurations.JavaRunConfigurationModule; |
| import com.intellij.execution.configurations.ModuleBasedConfiguration; |
| import com.intellij.execution.configurations.RunProfileState; |
| import com.intellij.execution.configurations.RuntimeConfigurationError; |
| import com.intellij.execution.configurations.RuntimeConfigurationException; |
| import com.intellij.execution.configurations.RuntimeConfigurationWarning; |
| import com.intellij.execution.executors.DefaultDebugExecutor; |
| import com.intellij.execution.runners.ExecutionEnvironment; |
| 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.util.DefaultJDOMExternalizer; |
| import com.intellij.openapi.util.InvalidDataException; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.WriteExternalException; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import org.jdom.Element; |
| import org.jetbrains.android.dom.manifest.Manifest; |
| import org.jetbrains.android.facet.AndroidFacet; |
| import org.jetbrains.android.sdk.AndroidPlatform; |
| import org.jetbrains.android.util.AndroidBundle; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| public abstract class AndroidRunConfigurationBase extends ModuleBasedConfiguration<JavaRunConfigurationModule, Element> |
| implements PreferGradleMake { |
| |
| private static final String GRADLE_SYNC_FAILED_ERR_MSG = "Gradle project sync failed. Please fix your project and try again."; |
| |
| /** |
| * Element name used to group the {@link ProfilerState} settings |
| */ |
| private static final String PROFILERS_ELEMENT_NAME = "Profilers"; |
| |
| public boolean CLEAR_LOGCAT = false; |
| public boolean SHOW_LOGCAT_AUTOMATICALLY = false; |
| public boolean SKIP_NOOP_APK_INSTALLATIONS = true; // skip installation if the APK hasn't hasn't changed |
| public boolean FORCE_STOP_RUNNING_APP = true; // if no new apk is being installed, then stop the app before launching it again |
| |
| private final ProfilerState myProfilerState; |
| |
| private final boolean myAndroidTests; |
| |
| private final DeployTargetContext myDeployTargetContext = new DeployTargetContext(); |
| private final AndroidDebuggerContext myAndroidDebuggerContext = new AndroidDebuggerContext(AndroidJavaDebugger.ID); |
| |
| public AndroidRunConfigurationBase(final Project project, final ConfigurationFactory factory, boolean androidTests) { |
| super(new JavaRunConfigurationModule(project, false), factory); |
| |
| myProfilerState = new ProfilerState(); |
| myAndroidTests = androidTests; |
| |
| if (StudioFlags.MULTIDEVICE_INSTRUMENTATION_TESTS.get()) { |
| getOptions().setAllowRunningInParallel(true); |
| } else { |
| getOptions().setAllowRunningInParallel(!androidTests); |
| } |
| } |
| |
| @Override |
| public final void checkConfiguration() throws RuntimeConfigurationException { |
| List<ValidationError> errors = validate(null); |
| if (errors.isEmpty()) { |
| return; |
| } |
| // TODO: Do something with the extra error information? Error count? |
| ValidationError topError = Ordering.natural().max(errors); |
| switch (topError.getSeverity()) { |
| case FATAL: |
| throw new RuntimeConfigurationError(topError.getMessage(), topError.getQuickfix()); |
| case WARNING: |
| throw new RuntimeConfigurationWarning(topError.getMessage(), topError.getQuickfix()); |
| case INFO: |
| default: |
| break; |
| } |
| } |
| |
| /** |
| * We collect errors rather than throwing to avoid missing fatal errors by exiting early for a warning. |
| * We use a separate method for the collection so the compiler prevents us from accidentally throwing. |
| */ |
| public List<ValidationError> validate(@Nullable Executor executor) { |
| List<ValidationError> errors = Lists.newArrayList(); |
| JavaRunConfigurationModule configurationModule = getConfigurationModule(); |
| try { |
| configurationModule.checkForWarning(); |
| } |
| catch (RuntimeConfigurationException e) { |
| errors.add(ValidationError.fromException(e)); |
| } |
| final Module module = configurationModule.getModule(); |
| if (module == null) { |
| // Can't proceed, and fatal error has been caught in ConfigurationModule#checkForWarnings |
| return errors; |
| } |
| |
| final Project project = module.getProject(); |
| if (AndroidProjectInfo.getInstance(project).requiredAndroidModelMissing()) { |
| errors.add(ValidationError.fatal(GRADLE_SYNC_FAILED_ERR_MSG)); |
| } |
| |
| AndroidFacet facet = AndroidFacet.getInstance(module); |
| if (facet == null) { |
| // Can't proceed. |
| return ImmutableList.of(ValidationError.fatal(AndroidBundle.message("no.facet.error", module.getName()))); |
| } |
| |
| switch (facet.getConfiguration().getProjectType()) { |
| // Supported project types. |
| case PROJECT_TYPE_APP: |
| case PROJECT_TYPE_INSTANTAPP: |
| case PROJECT_TYPE_TEST: |
| break; |
| |
| // Project types that need further check for the eligibility. |
| case PROJECT_TYPE_LIBRARY: |
| case PROJECT_TYPE_FEATURE: |
| case PROJECT_TYPE_DYNAMIC_FEATURE: |
| Pair<Boolean, String> result = supportsRunningLibraryProjects(facet); |
| if (!result.getFirst()) { |
| errors.add(ValidationError.fatal(result.getSecond())); |
| } |
| break; |
| |
| // Unsupported types. |
| default: |
| errors.add(ValidationError.fatal(AndroidBundle.message("run.error.apk.not.valid"))); |
| return errors; |
| } |
| |
| if (AndroidPlatform.getInstance(facet.getModule()) == null) { |
| errors.add(ValidationError.fatal(AndroidBundle.message("select.platform.error"))); |
| } |
| if (Manifest.getMainManifest(facet) == null && facet.getConfiguration().getProjectType() != PROJECT_TYPE_INSTANTAPP) { |
| errors.add(ValidationError.fatal(AndroidBundle.message("android.manifest.not.found.error"))); |
| } |
| errors.addAll(getDeployTargetContext().getCurrentDeployTargetState().validate(facet)); |
| |
| ApkProvider apkProvider = getApkProvider(facet, null); |
| if (apkProvider != null) { |
| errors.addAll(apkProvider.validate()); |
| } |
| else { |
| errors.add(ValidationError.fatal(AndroidBundle.message("android.run.configuration.not.supported", getId()))); |
| } |
| |
| errors.addAll(checkConfiguration(facet)); |
| AndroidDebuggerState androidDebuggerState = myAndroidDebuggerContext.getAndroidDebuggerState(); |
| if (androidDebuggerState != null) { |
| errors.addAll(androidDebuggerState.validate(facet, executor)); |
| } |
| |
| errors.addAll(myProfilerState.validate()); |
| |
| return errors; |
| } |
| |
| /** |
| * 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); |
| |
| @NotNull |
| protected abstract List<ValidationError> checkConfiguration(@NotNull AndroidFacet facet); |
| |
| /** |
| * Subclasses should override to adjust the launch options. |
| */ |
| @NotNull |
| protected LaunchOptions.Builder getLaunchOptions() { |
| return LaunchOptions.builder() |
| .setClearLogcatBeforeStart(CLEAR_LOGCAT) |
| .setSkipNoopApkInstallations(SKIP_NOOP_APK_INSTALLATIONS) |
| .setForceStopRunningApp(FORCE_STOP_RUNNING_APP); |
| } |
| |
| @Override |
| public Collection<Module> getValidModules() { |
| final List<Module> result = new ArrayList<>(); |
| Module[] modules = ModuleManager.getInstance(getProject()).getModules(); |
| for (Module module : modules) { |
| if (AndroidFacet.getInstance(module) != null) { |
| result.add(module); |
| } |
| } |
| return result; |
| } |
| |
| @NotNull |
| public List<DeployTargetProvider> getApplicableDeployTargetProviders() { |
| return getDeployTargetContext().getApplicableDeployTargetProviders(myAndroidTests); |
| } |
| |
| protected void validateBeforeRun(@NotNull Executor executor) throws ExecutionException { |
| List<ValidationError> errors = validate(executor); |
| ValidationUtil.promptAndQuickFixErrors(getProject(), errors); |
| } |
| |
| @Override |
| public RunProfileState getState(@NotNull final Executor executor, @NotNull ExecutionEnvironment env) throws ExecutionException { |
| RunStats stats = RunStatsService.get(getProject()).create(); |
| try { |
| stats.start(); |
| RunProfileState state = doGetState(executor, env, stats); |
| stats.markStateCreated(); |
| return state; |
| } |
| catch (Throwable t) { |
| stats.abort(); |
| throw t; |
| } |
| } |
| |
| @Nullable |
| public RunProfileState doGetState(@NotNull Executor executor, |
| @NotNull ExecutionEnvironment env, |
| @NotNull RunStats stats) throws ExecutionException { |
| validateBeforeRun(executor); |
| |
| final Module module = getConfigurationModule().getModule(); |
| assert module != null : "Enforced by fatal validation check in checkConfiguration."; |
| final AndroidFacet facet = AndroidFacet.getInstance(module); |
| assert facet != null : "Enforced by fatal validation check in checkConfiguration."; |
| |
| stats.setDebuggable(LaunchUtils.canDebugApp(facet)); |
| stats.setExecutor(executor.getId()); |
| |
| updateExtraRunStats(stats); |
| |
| final boolean isDebugging = executor instanceof DefaultDebugExecutor; |
| DeployTargetContext context = getDeployTargetContext(); |
| stats.setUserSelectedTarget(context.getCurrentDeployTargetProvider().requiresRuntimePrompt(facet.getModule().getProject())); |
| |
| // Figure out deploy target, prompt user if needed (ignore completely if user chose to hotswap). |
| DeployTarget deployTarget = getDeployTarget(executor, env, isDebugging, facet); |
| if (deployTarget == null) { // if user doesn't select a deploy target from the dialog |
| return null; |
| } |
| |
| DeployTargetState deployTargetState = context.getCurrentDeployTargetState(); |
| if (deployTarget.hasCustomRunProfileState(executor)) { |
| return deployTarget.getRunProfileState(executor, env, deployTargetState); |
| } |
| |
| DeviceFutures deviceFutures = deployTarget.getDevices(deployTargetState, facet, getDeviceCount(isDebugging), isDebugging, hashCode()); |
| if (deviceFutures == null) { |
| // The user deliberately canceled, or some error was encountered and exposed by the chooser. Quietly exit. |
| return null; |
| } |
| |
| // Record stat if we launched a device. |
| stats.setLaunchedDevices(deviceFutures.getDevices().stream().anyMatch(device -> device instanceof LaunchableAndroidDevice)); |
| |
| if (deviceFutures.get().isEmpty()) { |
| throw new ExecutionException(AndroidBundle.message("deployment.target.not.found")); |
| } |
| |
| if (isDebugging) { |
| String error = canDebug(deviceFutures, facet, module.getName()); |
| if (error != null) { |
| throw new ExecutionException(error); |
| } |
| } |
| |
| // Store the chosen target on the execution environment so before-run tasks can access it. |
| env.putCopyableUserData(DeviceFutures.KEY, deviceFutures); |
| |
| // Save the stats so that before-run task can access it |
| env.putUserData(RunStats.KEY, stats); |
| |
| ApplicationIdProvider applicationIdProvider = getApplicationIdProvider(facet); |
| |
| LaunchOptions.Builder launchOptions = getLaunchOptions() |
| .setDebug(isDebugging); |
| |
| if (executor instanceof LaunchOptionsProvider) { |
| launchOptions.addExtraOptions(((LaunchOptionsProvider)executor).getLaunchOptions()); |
| } |
| |
| // NOTE: getApkProvider() ignores the second argument and operates on the device specification passes to getApks() method later. |
| ApkProvider apkProvider = getApkProvider(facet, null); |
| if (apkProvider == null) return null; |
| AndroidLaunchTasksProvider launchTasksProvider = |
| new AndroidLaunchTasksProvider(this, env, facet, applicationIdProvider, apkProvider, launchOptions.build()); |
| |
| return new AndroidRunState(env, getName(), module, applicationIdProvider, |
| getConsoleProvider(deviceFutures.getDevices().size() > 1), deviceFutures, launchTasksProvider); |
| } |
| |
| private static String canDebug(@NotNull DeviceFutures deviceFutures, @NotNull AndroidFacet facet, @NotNull String moduleName) { |
| // If we are debugging on a device, then the app needs to be debuggable |
| for (AndroidDevice androidDevice : deviceFutures.getDevices()) { |
| if (!androidDevice.isDebuggable() && !LaunchUtils.canDebugApp(facet)) { |
| String deviceName; |
| if (!androidDevice.getLaunchedDevice().isDone()) { |
| deviceName = androidDevice.getName(); |
| } |
| else { |
| @SuppressWarnings("UnstableApiUsage") |
| IDevice device = Futures.getUnchecked(androidDevice.getLaunchedDevice()); |
| deviceName = device.getName(); |
| } |
| return AndroidBundle.message("android.cannot.debug.noDebugPermissions", moduleName, deviceName); |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| private DeployTarget getDeployTarget(@NotNull Executor executor, |
| @NotNull ExecutionEnvironment env, |
| boolean debug, |
| @NotNull AndroidFacet facet) { |
| DeployTargetProvider currentTargetProvider = getDeployTargetContext().getCurrentDeployTargetProvider(); |
| Project project = getProject(); |
| |
| DeployTarget deployTarget; |
| |
| if (currentTargetProvider.requiresRuntimePrompt(project)) { |
| deployTarget = |
| currentTargetProvider.showPrompt( |
| executor, |
| env, |
| facet, |
| getDeviceCount(debug), |
| myAndroidTests, |
| getDeployTargetContext().getDeployTargetStates(), |
| hashCode(), |
| LaunchCompatibilityCheckerImpl.create(facet, env, this) |
| ); |
| if (deployTarget == null) { |
| return null; |
| } |
| } |
| else { |
| deployTarget = currentTargetProvider.getDeployTarget(project); |
| } |
| |
| return deployTarget; |
| } |
| |
| @Nullable |
| public ApplicationIdProvider getApplicationIdProvider() { |
| final Module module = getConfigurationModule().getModule(); |
| if (module == null) { |
| return null; |
| } |
| |
| final AndroidFacet facet = AndroidFacet.getInstance(module); |
| if (facet == null) { |
| return null; |
| } |
| |
| if (facet.isDisposed()) { |
| Logger.getInstance(AndroidRunConfigurationBase.class).warn("Can't get application ID: Facet already disposed"); |
| return null; |
| } |
| |
| return getApplicationIdProvider(facet); |
| } |
| |
| @NotNull |
| public ApplicationIdProvider getApplicationIdProvider(@NotNull AndroidFacet facet) { |
| return ProjectSystemUtil.getModuleSystem(facet).getApplicationIdProvider(this); |
| } |
| |
| @Nullable |
| protected ApkProvider getApkProvider(@NotNull AndroidFacet facet, @Nullable AndroidDeviceSpec targetDeviceSpec) { |
| return ProjectSystemUtil.getModuleSystem(facet).getApkProvider(this, targetDeviceSpec); |
| } |
| |
| public abstract boolean isTestConfiguration(); |
| |
| @NotNull |
| protected abstract ConsoleProvider getConsoleProvider(boolean runOnMultipleDevices); |
| |
| @Nullable |
| protected abstract LaunchTask getApplicationLaunchTask(@NotNull ApplicationIdProvider applicationIdProvider, |
| @NotNull AndroidFacet facet, |
| @NotNull String contributorsAmStartOptions, |
| boolean waitForDebugger, |
| @NotNull LaunchStatus launchStatus, |
| @NotNull ApkProvider apkProvider); |
| |
| public final DeviceCount getDeviceCount(boolean debug) { |
| return DeviceCount.fromBoolean(supportMultipleDevices() && !debug); |
| } |
| |
| /** |
| * @return true iff this run configuration supports deploying to multiple devices. |
| */ |
| protected abstract boolean supportMultipleDevices(); |
| |
| public void updateExtraRunStats(RunStats runStats) { |
| |
| } |
| |
| @Override |
| public void readExternal(@NotNull Element element) throws InvalidDataException { |
| super.readExternal(element); |
| DefaultJDOMExternalizer.readExternal(this, element); |
| |
| myDeployTargetContext.readExternal(element); |
| myAndroidDebuggerContext.readExternal(element); |
| |
| Element profilersElement = element.getChild(PROFILERS_ELEMENT_NAME); |
| if (profilersElement != null) { |
| myProfilerState.readExternal(profilersElement); |
| } |
| } |
| |
| @Override |
| public void writeExternal(@NotNull Element element) throws WriteExternalException { |
| super.writeExternal(element); |
| DefaultJDOMExternalizer.writeExternal(this, element); |
| |
| myDeployTargetContext.writeExternal(element); |
| myAndroidDebuggerContext.writeExternal(element); |
| |
| Element profilersElement = new Element(PROFILERS_ELEMENT_NAME); |
| element.addContent(profilersElement); |
| myProfilerState.writeExternal(profilersElement); |
| } |
| |
| public boolean isNativeLaunch() { |
| AndroidDebugger<?> androidDebugger = myAndroidDebuggerContext.getAndroidDebugger(); |
| if (androidDebugger == null) { |
| return false; |
| } |
| return !androidDebugger.getId().equals(AndroidJavaDebugger.ID); |
| } |
| |
| @NotNull |
| public DeployTargetContext getDeployTargetContext() { |
| return myDeployTargetContext; |
| } |
| |
| @NotNull |
| public AndroidDebuggerContext getAndroidDebuggerContext() { |
| return myAndroidDebuggerContext; |
| } |
| |
| /** |
| * Returns the current {@link ProfilerState} for this configuration. |
| */ |
| @NotNull |
| public ProfilerState getProfilerState() { |
| return myProfilerState; |
| } |
| |
| /** |
| * Returns whether this configuration can run in Android Profiler. |
| */ |
| public boolean isProfilable() { |
| return true; |
| } |
| } |