blob: 7b47236298727b80ab8460d008c4496ace0bdf9c [file] [log] [blame]
/*
* Copyright (C) 2020 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.tools.idea.gradle.run;
import static com.android.ide.common.gradle.model.IdeAndroidProject.PROPERTY_APK_SELECT_CONFIG;
import static com.android.ide.common.gradle.model.IdeAndroidProject.PROPERTY_BUILD_ABI;
import static com.android.ide.common.gradle.model.IdeAndroidProject.PROPERTY_BUILD_API;
import static com.android.ide.common.gradle.model.IdeAndroidProject.PROPERTY_BUILD_API_CODENAME;
import static com.android.ide.common.gradle.model.IdeAndroidProject.PROPERTY_BUILD_DENSITY;
import static com.android.ide.common.gradle.model.IdeAndroidProject.PROPERTY_BUILD_WITH_STABLE_IDS;
import static com.android.ide.common.gradle.model.IdeAndroidProject.PROPERTY_DEPLOY_AS_INSTANT_APP;
import static com.android.ide.common.gradle.model.IdeAndroidProject.PROPERTY_EXTRACT_INSTANT_APK;
import static com.android.ide.common.gradle.model.IdeAndroidProject.PROPERTY_INJECTED_DYNAMIC_MODULES_LIST;
import static com.android.tools.idea.gradle.util.AndroidGradleSettings.createProjectProperty;
import static com.android.tools.idea.run.GradleApkProvider.POST_BUILD_MODEL;
import static com.android.tools.idea.run.editor.ProfilerState.ANDROID_ADVANCED_PROFILING_TRANSFORMS;
import static com.google.wireless.android.sdk.stats.GradleSyncStats.Trigger.TRIGGER_RUN_SYNC_NEEDED_BEFORE_RUNNING;
import static com.intellij.openapi.util.io.FileUtil.createTempFile;
import static com.intellij.openapi.util.text.StringUtil.isEmpty;
import static java.util.Collections.emptyList;
import com.android.ddmlib.IDevice;
import com.android.sdklib.AndroidVersion;
import com.android.tools.idea.gradle.project.BuildSettings;
import com.android.tools.idea.gradle.project.GradleProjectInfo;
import com.android.tools.idea.gradle.project.build.invoker.TestCompileType;
import com.android.tools.idea.gradle.project.facet.gradle.GradleFacet;
import com.android.tools.idea.gradle.project.model.AndroidModuleModel;
import com.android.tools.idea.gradle.project.model.GradleModuleModel;
import com.android.tools.idea.gradle.project.model.NdkModuleModel;
import com.android.tools.idea.gradle.project.sync.GradleSyncInvoker;
import com.android.tools.idea.gradle.project.sync.GradleSyncListener;
import com.android.tools.idea.gradle.project.sync.GradleSyncState;
import com.android.tools.idea.gradle.util.AndroidGradleSettings;
import com.android.tools.idea.gradle.util.BuildMode;
import com.android.tools.idea.gradle.util.DynamicAppUtils;
import com.android.tools.idea.gradle.util.EmbeddedDistributionPaths;
import com.android.tools.idea.gradle.util.GradleBuilds;
import com.android.tools.idea.project.AndroidProjectInfo;
import com.android.tools.idea.projectsystem.ProjectSystemUtil;
import com.android.tools.idea.projectsystem.gradle.GradleProjectSystem;
import com.android.tools.idea.run.AndroidDevice;
import com.android.tools.idea.run.AndroidDeviceSpec;
import com.android.tools.idea.run.AndroidRunConfiguration;
import com.android.tools.idea.run.AndroidRunConfigurationBase;
import com.android.tools.idea.run.DeviceFutures;
import com.android.tools.idea.run.PreferGradleMake;
import com.android.tools.idea.run.editor.ProfilerState;
import com.android.tools.idea.stats.RunStats;
import com.android.tools.idea.testartifacts.instrumented.AndroidTestRunConfiguration;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.intellij.compiler.options.CompileStepBeforeRun;
import com.intellij.execution.BeforeRunTaskProvider;
import com.intellij.execution.configurations.ModuleRunProfile;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.configurations.RunProfileWithCompileBeforeLaunchOption;
import com.intellij.execution.junit.JUnitConfiguration;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.util.ThreeState;
import icons.AndroidIcons;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import javax.swing.Icon;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Provides the "Gradle-aware Make" task for Run Configurations, which
* <ul>
* <li>is only available in Android Studio</li>
* <li>delegates to the regular "Make" if the project is not an Android Gradle project</li>
* <li>otherwise, invokes Gradle directly, to build the project</li>
* </ul>
*/
public class MakeBeforeRunTaskProvider extends BeforeRunTaskProvider<MakeBeforeRunTask> {
@NotNull public static final Key<MakeBeforeRunTask> ID = Key.create("Android.Gradle.BeforeRunTask");
public static final String TASK_NAME = "Gradle-aware Make";
@NotNull private final Project myProject;
@NotNull private final AndroidProjectInfo myAndroidProjectInfo;
@NotNull private final GradleProjectInfo myGradleProjectInfo;
@NotNull private final GradleTaskRunnerFactory myTaskRunnerFactory;
public MakeBeforeRunTaskProvider(@NotNull Project project) {
myProject = project;
myAndroidProjectInfo = AndroidProjectInfo.getInstance(project);
myGradleProjectInfo = GradleProjectInfo.getInstance(project);
myTaskRunnerFactory = new GradleTaskRunnerFactory(myProject);
}
@Override
public Key<MakeBeforeRunTask> getId() {
return ID;
}
@Nullable
@Override
public Icon getIcon() {
return AndroidIcons.Android;
}
@Nullable
@Override
public Icon getTaskIcon(MakeBeforeRunTask task) {
return AndroidIcons.Android;
}
@Override
public String getName() {
return TASK_NAME;
}
@Override
public String getDescription(MakeBeforeRunTask task) {
String goal = task.getGoal();
return isEmpty(goal) ? TASK_NAME : "gradle " + goal;
}
@Override
public boolean isConfigurable() {
return true;
}
@Nullable
@Override
public MakeBeforeRunTask createTask(@NotNull RunConfiguration runConfiguration) {
// "Gradle-aware Make" is only available in Android Studio.
if (configurationTypeIsSupported(runConfiguration)) {
MakeBeforeRunTask task = new MakeBeforeRunTask();
if (configurationTypeIsEnabledByDefault(runConfiguration)) {
// For Android configurations, we want to replace the default make, so this new task needs to be enabled.
// In AndroidRunConfigurationType#configureBeforeTaskDefaults we disable the default make, which is
// enabled by default. For other configurations we leave it disabled, so we don't end up with two different
// make steps executed by default. If the task is added to the run configuration manually, it will be
// enabled by the UI layer later.
task.setEnabled(true);
}
return task;
}
else {
return null;
}
}
public boolean configurationTypeIsSupported(@NotNull RunConfiguration runConfiguration) {
if (!(ProjectSystemUtil.getProjectSystem(runConfiguration.getProject()) instanceof GradleProjectSystem)) {
return false;
}
return runConfiguration instanceof PreferGradleMake || isUnitTestConfiguration(runConfiguration);
}
public boolean configurationTypeIsEnabledByDefault(@NotNull RunConfiguration runConfiguration) {
return runConfiguration instanceof PreferGradleMake;
}
private static boolean isUnitTestConfiguration(@NotNull RunConfiguration runConfiguration) {
return runConfiguration instanceof JUnitConfiguration ||
// Avoid direct dependency on the TestNG plugin:
runConfiguration.getClass().getSimpleName().equals("TestNGConfiguration");
}
@Override
public boolean configureTask(@NotNull RunConfiguration runConfiguration, @NotNull MakeBeforeRunTask task) {
GradleEditTaskDialog dialog = new GradleEditTaskDialog(myProject);
dialog.setGoal(task.getGoal());
dialog.setAvailableGoals(createAvailableTasks());
if (!dialog.showAndGet()) {
// since we allow tasks without any arguments (assumed to be equivalent to assembling the app),
// we need a way to specify that a task is not valid. This is because of the current restriction
// of this API, where the return value from configureTask is ignored.
task.setInvalid();
return false;
}
task.setGoal(dialog.getGoal());
return true;
}
@NotNull
private List<String> createAvailableTasks() {
ModuleManager moduleManager = ModuleManager.getInstance(myProject);
List<String> gradleTasks = new ArrayList<>();
for (Module module : moduleManager.getModules()) {
GradleFacet facet = GradleFacet.getInstance(module);
if (facet == null) {
continue;
}
GradleModuleModel gradleModuleModel = facet.getGradleModuleModel();
if (gradleModuleModel == null) {
continue;
}
gradleTasks.addAll(gradleModuleModel.getTaskNames());
}
return gradleTasks;
}
@Override
public boolean canExecuteTask(@NotNull RunConfiguration configuration, @NotNull MakeBeforeRunTask task) {
return task.isValid();
}
/**
* Execute the Gradle build task, returns {@code false} in case of any error.
*
* <p>Note: Error handling should be improved to notify user in case of {@code false} return value.
* Currently, the caller does not expect exceptions, and there is no notification mechanism to propagate an
* error message to the user. The current implementation uses logging (in idea.log) to report errors, whereas the
* UI behavior is to merely stop the execution without any other sort of notification, which far from ideal.
*/
@Override
public boolean executeTask(@NotNull DataContext context,
@NotNull RunConfiguration configuration,
@NotNull ExecutionEnvironment env,
@NotNull MakeBeforeRunTask task) {
if (Boolean.FALSE.equals(env.getUserData(GradleBuilds.BUILD_SHOULD_EXECUTE))) {
return true;
}
RunStats stats = RunStats.from(env);
try {
stats.beginBeforeRunTasks();
return doExecuteTask(context, configuration, env, task);
}
finally {
stats.endBeforeRunTasks();
}
}
@VisibleForTesting
enum SyncNeeded {
NOT_NEEDED(false),
SINGLE_VARIANT_SYNC_NEEDED(false),
FULL_SYNC_NEEDED(true),
NATIVE_VARIANTS_SYNC_NEEDED(false);
public final boolean isFullSync;
SyncNeeded(boolean isFullSync) {
this.isFullSync = isFullSync;
}
}
@VisibleForTesting
@NotNull
SyncNeeded isSyncNeeded(@NotNull DataContext context, @NotNull RunConfiguration configuration, Collection<String> abis) {
// Invoke Gradle Sync if build files have been changed since last sync, and Sync-before-build option is enabled OR post build sync
// if not supported. The later case requires Gradle Sync, because deploy relies on the models from Gradle Sync to get Apk locations.
if (GradleSyncState.getInstance(myProject).isSyncNeeded() != ThreeState.NO && !isPostBuildModelsSupported(context, configuration)) {
return SyncNeeded.SINGLE_VARIANT_SYNC_NEEDED;
}
// If the project has native modules, and there're any un-synced variants.
for (Module module : ModuleManager.getInstance(myProject).getModules()) {
NdkModuleModel ndkModel = NdkModuleModel.get(module);
AndroidModuleModel androidModel = AndroidModuleModel.get(module);
if (ndkModel != null && androidModel != null) {
String selectedVariantName = androidModel.getSelectedVariant().getName();
Set<String> availableAbis = ndkModel.getSyncedVariantAbis().stream()
.filter(it -> it.getVariant().equals(selectedVariantName))
.map(it -> it.getAbi())
.collect(Collectors.toSet());
if (!availableAbis.containsAll(abis)) {
return SyncNeeded.NATIVE_VARIANTS_SYNC_NEEDED;
}
}
}
return SyncNeeded.NOT_NEEDED;
}
@Nullable
private String runSync(@NotNull SyncNeeded syncNeeded,
@NotNull Set<@NotNull String> requestedAbis) {
if (syncNeeded == SyncNeeded.NATIVE_VARIANTS_SYNC_NEEDED) {
GradleSyncInvoker.getInstance().fetchAndMergeNativeVariants(myProject, requestedAbis);
return null;
}
else {
String result;
GradleSyncInvoker.Request request = new GradleSyncInvoker.Request(TRIGGER_RUN_SYNC_NEEDED_BEFORE_RUNNING);
request.runInBackground = false;
request.forceFullVariantsSync = syncNeeded.isFullSync;
AtomicReference<String> errorMsgRef = new AtomicReference<>();
GradleSyncInvoker.getInstance().requestProjectSync(myProject, request, new GradleSyncListener() {
@Override
public void syncFailed(@NotNull Project project, @NotNull String errorMessage) {
errorMsgRef.set(errorMessage);
}
});
result = errorMsgRef.get();
return result;
}
}
/**
* Returns true if there is no Android module, or all of Android modules support post build models (via sync or the build output file).
*/
private boolean isPostBuildModelsSupported(@NotNull DataContext context,
@NotNull RunConfiguration configuration) {
Module[] modules = getModules(context, configuration);
for (Module module : modules) {
AndroidModuleModel androidModuleModel = AndroidModuleModel.get(module);
if (androidModuleModel != null &&
!androidModuleModel.getFeatures().isPostBuildSyncSupported() &&
!androidModuleModel.getFeatures().isBuildOutputFileSupported()) {
return false;
}
}
return true;
}
private boolean doExecuteTask(DataContext context, RunConfiguration configuration, ExecutionEnvironment env, MakeBeforeRunTask task) {
AndroidRunConfigurationBase androidRunConfiguration =
configuration instanceof AndroidRunConfigurationBase ? (AndroidRunConfigurationBase)configuration : null;
if (!myAndroidProjectInfo.requiresAndroidModel()) {
CompileStepBeforeRun regularMake = new CompileStepBeforeRun(myProject);
return regularMake.executeTask(context, configuration, env, new CompileStepBeforeRun.MakeBeforeRunTask());
}
// Note: this run task provider may be invoked from a context such as Java unit tests, in which case it doesn't have
// the android run config context
DeviceFutures deviceFutures = env.getCopyableUserData(DeviceFutures.KEY);
List<AndroidDevice> targetDevices = deviceFutures == null ? emptyList() : deviceFutures.getDevices();
@Nullable AndroidDeviceSpec targetDeviceSpec = AndroidDeviceSpecUtil.createSpec(targetDevices);
// Some configurations (e.g. native attach) don't require a build while running the configuration
if (configuration instanceof RunProfileWithCompileBeforeLaunchOption &&
((RunProfileWithCompileBeforeLaunchOption)configuration).isExcludeCompileBeforeLaunchOption()) {
return true;
}
// Compute modules to build
Module[] modules = getModules(context, configuration);
List<String> cmdLineArgs;
try {
cmdLineArgs = getCommonArguments(modules, androidRunConfiguration, targetDeviceSpec);
}
catch (Exception e) {
getLog().warn("Error generating command line arguments for Gradle task", e);
return false;
}
AndroidVersion targetDeviceVersion = targetDeviceSpec != null ? targetDeviceSpec.getCommonVersion() : null;
BeforeRunBuilder builder = createBuilder(modules, configuration, targetDeviceVersion, task.getGoal());
GradleTaskRunner.DefaultGradleTaskRunner runner = myTaskRunnerFactory.createTaskRunner(configuration);
BuildSettings.getInstance(myProject).setRunConfigurationTypeId(configuration.getType().getId());
try {
boolean success = builder.build(runner, cmdLineArgs);
if (androidRunConfiguration != null) {
Object model = runner.getModel();
if (model instanceof OutputBuildAction.PostBuildProjectModels) {
androidRunConfiguration.putUserData(POST_BUILD_MODEL, new PostBuildModel((OutputBuildAction.PostBuildProjectModels)model));
}
else {
getLog().info("Couldn't get post build models.");
}
}
getLog().info("Gradle invocation complete, success = " + success);
// If the model needs a sync, we need to sync "synchronously" before running.
Set<String> targetAbis = new HashSet<>(targetDeviceSpec != null ? targetDeviceSpec.getAbis() : emptyList());
SyncNeeded syncNeeded = isSyncNeeded(context, configuration, targetAbis);
if (syncNeeded != SyncNeeded.NOT_NEEDED) {
String errorMsg = runSync(syncNeeded, targetAbis);
if (errorMsg != null) {
// Sync failed. There is no point on continuing, because most likely the model is either not there, or has stale information,
// including the path of the APK.
getLog().info("Unable to launch '" + TASK_NAME + "' task. Project sync failed with message: " + errorMsg);
return false;
}
}
if (myProject.isDisposed()) {
return false;
}
return success;
}
catch (InvocationTargetException e) {
getLog().info("Unexpected error while launching gradle before run tasks", e);
return false;
}
catch (InterruptedException e) {
getLog().info("Interrupted while launching gradle before run tasks");
Thread.currentThread().interrupt();
return false;
}
}
@NotNull
private static Logger getLog() {
return Logger.getInstance(MakeBeforeRunTask.class);
}
/**
* Returns the list of arguments to Gradle that are common to both instant and non-instant builds.
*/
@VisibleForTesting
@NotNull
static List<String> getCommonArguments(@NotNull Module[] modules,
@Nullable AndroidRunConfigurationBase configuration,
@Nullable AndroidDeviceSpec targetDeviceSpec) throws IOException {
List<String> cmdLineArgs = new ArrayList<>();
// Always build with stable IDs to avoid push-to-device overhead.
cmdLineArgs.add(createProjectProperty(PROPERTY_BUILD_WITH_STABLE_IDS, true));
if (configuration != null) {
cmdLineArgs.addAll(getDeviceSpecificArguments(modules, configuration, targetDeviceSpec));
cmdLineArgs.addAll(getProfilingOptions(configuration, targetDeviceSpec));
}
return cmdLineArgs;
}
@VisibleForTesting
@NotNull
static List<String> getDeviceSpecificArguments(@NotNull Module[] modules,
@NotNull AndroidRunConfigurationBase configuration,
@Nullable AndroidDeviceSpec deviceSpec) {
if (deviceSpec == null) {
return emptyList();
}
List<String> properties = new ArrayList<>(3);
if (useSelectApksFromBundleBuilder(modules, configuration, deviceSpec.getMinVersion())) {
// For the bundle tool, we create a temporary json file with the device spec and
// pass the file path to the gradle task.
boolean collectListOfLanguages = shouldCollectListOfLanguages(modules, configuration, deviceSpec.getMinVersion());
File deviceSpecFile = AndroidDeviceSpecUtil.writeToJsonTempFile(deviceSpec, collectListOfLanguages);
properties.add(createProjectProperty(PROPERTY_APK_SELECT_CONFIG, deviceSpecFile.getAbsolutePath()));
if (configuration instanceof AndroidRunConfiguration) {
AndroidRunConfiguration androidRunConfiguration = (AndroidRunConfiguration)configuration;
if (androidRunConfiguration.DEPLOY_AS_INSTANT) {
properties.add(createProjectProperty(PROPERTY_EXTRACT_INSTANT_APK, true));
}
if (androidRunConfiguration.DEPLOY_APK_FROM_BUNDLE) {
String featureList = getEnabledDynamicFeatureList(modules, androidRunConfiguration);
if (!featureList.isEmpty()) {
properties.add(createProjectProperty(PROPERTY_INJECTED_DYNAMIC_MODULES_LIST, featureList));
}
}
}
}
else {
// For non bundle tool deploy tasks, we have one argument per device spec property
AndroidVersion version = deviceSpec.getCommonVersion();
if (version != null) {
properties.add(createProjectProperty(PROPERTY_BUILD_API, Integer.toString(version.getApiLevel())));
if (version.getCodename() != null) {
properties.add(createProjectProperty(PROPERTY_BUILD_API_CODENAME, version.getCodename()));
}
}
if (deviceSpec.getDensity() != null) {
properties.add(createProjectProperty(PROPERTY_BUILD_DENSITY, deviceSpec.getDensity().getResourceValue()));
}
if (!deviceSpec.getAbis().isEmpty()) {
properties.add(createProjectProperty(PROPERTY_BUILD_ABI, Joiner.on(',').join(deviceSpec.getAbis())));
}
if (configuration instanceof AndroidRunConfiguration) {
if (((AndroidRunConfiguration)configuration).DEPLOY_AS_INSTANT) {
properties.add(createProjectProperty(PROPERTY_DEPLOY_AS_INSTANT_APP, true));
}
}
}
return properties;
}
@NotNull
private static String getEnabledDynamicFeatureList(@NotNull Module[] modules,
@NotNull AndroidRunConfiguration configuration) {
Set<String> disabledFeatures = new HashSet<>(configuration.getDisabledDynamicFeatures());
return Arrays.stream(modules)
.flatMap(module -> DynamicAppUtils.getDependentFeatureModulesForBase(module).stream())
.map(Module::getName)
.filter(name -> !disabledFeatures.contains(name))
.map(moduleName -> {
// e.g name = "MyApplication.dynamicfeature"
int index = moduleName.lastIndexOf('.');
return index < 0 ? moduleName : moduleName.substring(index + 1);
})
.collect(Collectors.joining(","));
}
@NotNull
private static List<String> getProfilingOptions(@NotNull AndroidRunConfigurationBase configuration,
@Nullable AndroidDeviceSpec targetDeviceSpec)
throws IOException {
if (targetDeviceSpec == null) {
return emptyList();
}
// Find the minimum API version in case both a pre-O and post-O devices are selected.
// TODO: if a post-O app happened to be transformed, the agent needs to account for that.
int minFeatureLevel = targetDeviceSpec.getMinVersion().getFeatureLevel();
List<String> arguments = new LinkedList<>();
ProfilerState state = configuration.getProfilerState();
if (state.ADVANCED_PROFILING_ENABLED && minFeatureLevel >= AndroidVersion.VersionCodes.LOLLIPOP &&
minFeatureLevel < AndroidVersion.VersionCodes.O) {
File file = EmbeddedDistributionPaths.getInstance().findEmbeddedProfilerTransform();
arguments.add(createProjectProperty(ANDROID_ADVANCED_PROFILING_TRANSFORMS, file.getAbsolutePath()));
Properties profilerProperties = state.toProperties();
File propertiesFile = createTempFile("profiler", ".properties");
propertiesFile.deleteOnExit(); // TODO: It'd be nice to clean this up sooner than at exit.
Writer writer = new OutputStreamWriter(new FileOutputStream(propertiesFile), Charsets.UTF_8);
profilerProperties.store(writer, "Android Studio Profiler Gradle Plugin Properties");
writer.close();
arguments.add(AndroidGradleSettings.createJvmArg("android.profiler.properties", propertiesFile.getAbsolutePath()));
}
return arguments;
}
@NotNull
private static BeforeRunBuilder createBuilder(@NotNull Module[] modules,
@NotNull RunConfiguration configuration,
@Nullable AndroidVersion targetDeviceVersion,
@Nullable String userGoal) {
if (modules.length == 0) {
throw new IllegalStateException("Unable to determine list of modules to build");
}
if (!isEmpty(userGoal)) {
ListMultimap<Path, String> tasks = ArrayListMultimap.create();
StreamEx.of(modules)
.map(module -> ExternalSystemApiUtil.getExternalRootProjectPath(module))
.nonNull()
.distinct()
.map(path -> Paths.get(path))
.forEach(path -> tasks.put(path, userGoal));
return new DefaultGradleBuilder(tasks, null);
}
GradleModuleTasksProvider gradleTasksProvider = new GradleModuleTasksProvider(modules);
TestCompileType testCompileType = TestCompileType.get(configuration.getType().getId());
if (testCompileType == TestCompileType.UNIT_TESTS) {
BuildMode buildMode = BuildMode.COMPILE_JAVA;
return new DefaultGradleBuilder(gradleTasksProvider.getUnitTestTasks(buildMode), buildMode);
}
// Use the "select apks from bundle" task if using a "AndroidRunConfigurationBase".
// Note: This is very ad-hoc, and it would be nice to have a better abstraction for this special case.
// NOTE: MakeBeforeRunTask is configured on unit-test and AndroidrunConfigurationBase run configurations only. Therefore,
// since testCompileType != TestCompileType.UNIT_TESTS it is safe to assume that configuration is
// AndroidRunConfigurationBase.
if (configuration instanceof AndroidRunConfigurationBase
&& useSelectApksFromBundleBuilder(modules, (AndroidRunConfigurationBase)configuration, targetDeviceVersion)) {
return new DefaultGradleBuilder(gradleTasksProvider.getTasksFor(BuildMode.APK_FROM_BUNDLE, testCompileType),
BuildMode.APK_FROM_BUNDLE);
}
return new DefaultGradleBuilder(gradleTasksProvider.getTasksFor(BuildMode.ASSEMBLE, testCompileType), BuildMode.ASSEMBLE);
}
private static boolean useSelectApksFromBundleBuilder(@NotNull Module[] modules,
@NotNull AndroidRunConfigurationBase configuration,
@Nullable AndroidVersion minTargetDeviceVersion) {
return Arrays.stream(modules)
.anyMatch(module -> DynamicAppUtils.useSelectApksFromBundleBuilder(module, configuration, minTargetDeviceVersion));
}
private static boolean shouldCollectListOfLanguages(@NotNull Module[] modules,
@NotNull AndroidRunConfigurationBase configuration,
@Nullable AndroidVersion targetDeviceVersion) {
// We should collect the list of languages only if *all* devices are verify the condition, otherwise we would
// end up deploying language split APKs to devices that don't support them.
return Arrays.stream(modules)
.allMatch(module -> DynamicAppUtils.shouldCollectListOfLanguages(module, configuration, targetDeviceVersion));
}
@NotNull
private Module[] getModules(@Nullable DataContext context, @Nullable RunConfiguration configuration) {
if (configuration instanceof ModuleRunProfile) {
// ModuleBasedConfiguration includes Android and JUnit run configurations, including "JUnit: Rerun Failed Tests",
// which is AbstractRerunFailedTestsAction.MyRunProfile.
return ((ModuleRunProfile)configuration).getModules();
}
else {
return myGradleProjectInfo.getModulesToBuildFromSelection(context);
}
}
@Nullable
public static IDevice getLaunchedDevice(@NotNull AndroidDevice device) {
if (!device.getLaunchedDevice().isDone()) {
// If we don't have access to the device (this happens if the AVD is still launching)
return null;
}
try {
return device.getLaunchedDevice().get(1, TimeUnit.MILLISECONDS);
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
catch (ExecutionException | TimeoutException e) {
return null;
}
}
@VisibleForTesting
static class GradleTaskRunnerFactory {
@NotNull private final Project myProject;
GradleTaskRunnerFactory(@NotNull Project project) {
myProject = project;
}
@NotNull
GradleTaskRunner.DefaultGradleTaskRunner createTaskRunner(@NotNull RunConfiguration configuration) {
if (configuration instanceof AndroidRunConfigurationBase) {
List<Module> modules = new ArrayList<>();
Module selectedModule = ((AndroidRunConfigurationBase)configuration).getConfigurationModule().getModule();
if (selectedModule != null) {
modules.add(selectedModule);
// Instrumented test support for Dynamic features: base-app module should be included explicitly,
// then corresponding post build models are generated. And in this case, dynamic feature module
// retrieved earlier is for test Apk.
if (configuration instanceof AndroidTestRunConfiguration) {
Module baseModule = DynamicAppUtils.getBaseFeature(selectedModule);
if (baseModule != null) {
modules.add(baseModule);
}
}
}
return GradleTaskRunner.newRunner(myProject, OutputBuildActionUtil.create(modules));
}
return GradleTaskRunner.newRunner(myProject, null);
}
}
}