/* * Copyright 2016 The Bazel Authors. All rights reserved. * * 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.google.idea.blaze.android.run.binary; import static com.android.tools.idea.run.deployment.DeviceAndSnapshotComboBoxAction.DEPLOYS_TO_LOCAL_DEVICE; import com.android.annotations.VisibleForTesting; import com.android.tools.idea.run.ValidationError; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState; import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationHandler; import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationValidationUtil; import com.google.idea.blaze.android.run.binary.AndroidBinaryLaunchMethodsUtils.AndroidBinaryLaunchMethod; import com.google.idea.blaze.android.run.binary.mobileinstall.BlazeAndroidBinaryMobileInstallRunContext; import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationRunner; import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext; import com.google.idea.blaze.android.sync.projectstructure.BlazeAndroidProjectStructureSyncer; import com.google.idea.blaze.base.command.BlazeCommandName; import com.google.idea.blaze.base.command.BlazeInvocationContext; import com.google.idea.blaze.base.logging.EventLoggingService; import com.google.idea.blaze.base.model.primitives.Label; import com.google.idea.blaze.base.model.primitives.TargetExpression; import com.google.idea.blaze.base.projectview.ProjectViewManager; import com.google.idea.blaze.base.projectview.ProjectViewSet; import com.google.idea.blaze.base.run.BlazeCommandRunConfiguration; import com.google.idea.blaze.base.run.BlazeCommandRunConfigurationType; import com.google.idea.blaze.base.run.BlazeConfigurationNameBuilder; import com.google.idea.blaze.base.run.ExecutorType; import com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationRunner; import com.google.idea.blaze.base.run.state.RunConfigurationState; import com.google.idea.blaze.base.settings.Blaze; import com.intellij.execution.ExecutionException; import com.intellij.execution.Executor; import com.intellij.execution.RunManager; import com.intellij.execution.configurations.RunConfiguration; import com.intellij.execution.configurations.RuntimeConfigurationException; import com.intellij.execution.runners.ExecutionEnvironment; import com.intellij.ide.util.PropertiesComponent; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import java.util.List; import java.util.concurrent.TimeUnit; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.annotations.Nullable; /** * {@link com.google.idea.blaze.base.run.confighandler.BlazeCommandRunConfigurationHandler} for * android_binary targets. */ public class BlazeAndroidBinaryRunConfigurationHandler implements BlazeAndroidRunConfigurationHandler { private static final Logger LOG = Logger.getInstance(BlazeAndroidBinaryRunConfigurationHandler.class); private final BlazeCommandRunConfiguration configuration; private final BlazeAndroidBinaryRunConfigurationState configState; // Keys to store state for the MI migration prompt private static final String MI_LAST_PROMPT = "MI_MIGRATE_LAST_PROMPT"; static final String MI_NEVER_ASK_AGAIN = "MI_MIGRATE_NEVER_AGAIN"; private static final Long MI_TIMEOUT_MS = TimeUnit.HOURS.toMillis(20); // 20 hours @VisibleForTesting protected BlazeAndroidBinaryRunConfigurationHandler(BlazeCommandRunConfiguration configuration) { this.configuration = configuration; configState = new BlazeAndroidBinaryRunConfigurationState( Blaze.buildSystemName(configuration.getProject())); configuration.putUserData(DEPLOYS_TO_LOCAL_DEVICE, true); } @Override public BlazeAndroidBinaryRunConfigurationState getState() { return configState; } @Override public BlazeAndroidRunConfigurationCommonState getCommonState() { return configState.getCommonState(); } @Override @Nullable public Label getLabel() { TargetExpression target = configuration.getSingleTarget(); return target instanceof Label ? (Label) target : null; } @Nullable public Module getModule() { Label target = getLabel(); return target != null ? BlazeAndroidProjectStructureSyncer.ensureRunConfigurationModule( configuration.getProject(), target) : null; } @Override public BlazeCommandRunConfigurationRunner createRunner( Executor executor, ExecutionEnvironment env) throws ExecutionException { Project project = env.getProject(); // This is a workaround for b/134587683 // Due to the way blaze run configuration editors update the underlying configuration state, // it's possible for the configuration referenced in this handler to be out of date. This can // cause tricky side-effects such as incorrect build target and target validation settings. // Fortunately, the only field that can come out of sync is the target label and it's target // kind. The handlers are designed to only handle their supported target kinds, so we can // safely ignore all fields other than target label itself and extract an up to date target // label from the execution environment. // Validation of the updated target label is not needed here because: // 1. The target kind is guaranteed to be an android instrumentation test kind or else this // specific handler will not be used. // 2. Any other validation is done during edit-time of the run configuration before saving. BlazeCommandRunConfiguration configFromEnv = BlazeAndroidRunConfigurationHandler.getCommandConfig(env); configuration.setTarget(configFromEnv.getSingleTarget()); Module module = getModule(); AndroidFacet facet = module != null ? AndroidFacet.getInstance(module) : null; ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet(); BlazeAndroidRunConfigurationValidationUtil.validateExecution(module, facet, projectViewSet); ImmutableList<String> blazeFlags = configState .getCommonState() .getExpandedBuildFlags( project, projectViewSet, BlazeCommandName.RUN, BlazeInvocationContext.runConfigContext( ExecutorType.fromExecutor(env.getExecutor()), configuration.getType(), false)); ImmutableList<String> exeFlags = ImmutableList.copyOf( configState.getCommonState().getExeFlagsState().getFlagsForExternalProcesses()); BlazeAndroidRunContext runContext = createRunContext(project, facet, env, blazeFlags, exeFlags); EventLoggingService.getInstance() .logEvent( BlazeAndroidBinaryRunConfigurationHandler.class, "BlazeAndroidBinaryRun", ImmutableMap.of( "launchMethod", configState.getLaunchMethod().name(), "executorId", env.getExecutor().getId(), "targetLabel", configuration.getSingleTarget().toString(), "nativeDebuggingEnabled", Boolean.toString(configState.getCommonState().isNativeDebuggingEnabled()))); return new BlazeAndroidRunConfigurationRunner( module, runContext, getCommonState().getDeployTargetManager(), getCommonState().getDebuggerManager(), configuration); } private BlazeAndroidRunContext createRunContext( Project project, AndroidFacet facet, ExecutionEnvironment env, ImmutableList<String> blazeFlags, ImmutableList<String> exeFlags) { switch (configState.getLaunchMethod()) { case NON_BLAZE: if (!maybeShowMobileInstallOptIn(project, configuration)) { return new BlazeAndroidBinaryNormalBuildRunContext( project, facet, configuration, env, configState, getLabel(), blazeFlags); } // fall through case MOBILE_INSTALL_V2: // Standardize on a single mobile-install launch method configState.setLaunchMethod(AndroidBinaryLaunchMethod.MOBILE_INSTALL); // fall through case MOBILE_INSTALL: return new BlazeAndroidBinaryMobileInstallRunContext( project, facet, configuration, env, configState, getLabel(), blazeFlags, exeFlags); } throw new AssertionError(); } @Override public final void checkConfiguration() throws RuntimeConfigurationException { BlazeAndroidRunConfigurationValidationUtil.throwTopConfigurationError(validate()); } /** * 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. */ private List<ValidationError> validate() { List<ValidationError> errors = Lists.newArrayList(); Module module = getModule(); errors.addAll(BlazeAndroidRunConfigurationValidationUtil.validateModule(module)); AndroidFacet facet = null; if (module != null) { facet = AndroidFacet.getInstance(module); errors.addAll(BlazeAndroidRunConfigurationValidationUtil.validateFacet(facet, module)); } errors.addAll(configState.validate(facet)); return errors; } @Override @Nullable public String suggestedName(BlazeCommandRunConfiguration configuration) { Label target = getLabel(); if (target == null) { return null; } // buildSystemName and commandName are intentionally omitted. return new BlazeConfigurationNameBuilder().setTargetString(target).build(); } @Override @Nullable public BlazeCommandName getCommandName() { return BlazeCommandName.RUN; } @Override public String getHandlerName() { return "Android Binary Handler"; } /** * Maybe shows the mobile-install optin dialog, and migrates project as appropriate. * * <p>Will only be shown once per project in a 20 hour window, with the ability to permanently * dismiss for this project. * * <p>If the user selects "Yes", all BlazeAndroidBinaryRunConfigurations in this project will be * migrated to use mobile-install. * * @return true if dialog was shown and user migrated, otherwise false */ private boolean maybeShowMobileInstallOptIn( Project project, BlazeCommandRunConfiguration configuration) { long lastPrompt = PropertiesComponent.getInstance(project).getOrInitLong(MI_LAST_PROMPT, 0L); boolean neverAsk = PropertiesComponent.getInstance(project).getBoolean(MI_NEVER_ASK_AGAIN, false); if (neverAsk || (System.currentTimeMillis() - lastPrompt) < MI_TIMEOUT_MS) { return false; } // Add more logging on why the MI opt-in dialog is shown. There exists a bug there a user // is shown the mobile-install opt-in dialog every time they switch clients. The only way for // this to happen is if a new target is created or if the timeouts are not behaving as expected. // TODO Remove once b/130327673 is resolved. LOG.info( "Showing mobile install opt-in dialog.\n" + "Run target: " + configuration.getSingleTarget() + "\n" + "Time since last prompt: " + (System.currentTimeMillis() - lastPrompt)); PropertiesComponent.getInstance(project) .setValue(MI_LAST_PROMPT, String.valueOf(System.currentTimeMillis())); int choice = Messages.showYesNoCancelDialog( project, "Blaze mobile-install (go/blaze-mi) introduces fast, incremental builds and deploys " + "for Android development.\nBlaze mobile-install is the default for new Android " + "Studio projects, but you're still using Blaze build.\n\nSwitch all run " + "configurations in this project to use Blaze mobile-install?", "Switch to Blaze mobile-install?", "Yes", "Not now", "Never ask again for this project", Messages.getQuestionIcon()); if (choice == Messages.YES) { Messages.showInfoMessage( String.format( "Successfully migrated %d run configuration(s) to mobile-install", doMigrate(project)), "Success!"); } else if (choice == Messages.NO) { // Do nothing, dialog will not be shown until the wait period has elapsed } else if (choice == Messages.CANCEL) { PropertiesComponent.getInstance(project).setValue(MI_NEVER_ASK_AGAIN, true); } EventLoggingService.getInstance() .logEvent( getClass(), "mi_migrate_prompt", ImmutableMap.of("choice", choiceToString(choice))); return choice == Messages.YES; } private int doMigrate(Project project) { int count = 0; for (RunConfiguration runConfig : RunManager.getInstance(project) .getConfigurationsList(BlazeCommandRunConfigurationType.getInstance())) { if (runConfig instanceof BlazeCommandRunConfiguration) { RunConfigurationState state = ((BlazeCommandRunConfiguration) runConfig).getHandler().getState(); if (state instanceof BlazeAndroidBinaryRunConfigurationState) { ((BlazeAndroidBinaryRunConfigurationState) state) .setLaunchMethod(AndroidBinaryLaunchMethod.MOBILE_INSTALL); count++; } } } return count; } private String choiceToString(int choice) { if (choice == Messages.YES) { return "yes"; } else if (choice == Messages.NO) { return "not_now"; } else if (choice == Messages.CANCEL) { return "never_for_project"; } else { return "unknown"; } } }