/*
* 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.SdkConstants;
import com.android.annotations.VisibleForTesting;
import com.android.ddmlib.*;
import com.android.tools.idea.model.ManifestInfo;
import com.google.common.base.Predicates;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.Executor;
import com.intellij.execution.JavaExecutionUtil;
import com.intellij.execution.configurations.*;
import com.intellij.execution.filters.TextConsoleBuilder;
import com.intellij.execution.filters.TextConsoleBuilderFactory;
import com.intellij.execution.junit.RefactoringListeners;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.options.SettingsEditor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.ProjectScope;
import com.intellij.refactoring.listeners.RefactoringElementListener;
import org.jetbrains.android.dom.AndroidDomUtil;
import org.jetbrains.android.dom.manifest.*;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.util.AndroidBundle;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.List;
import static com.intellij.execution.process.ProcessOutputTypes.STDERR;
import static com.intellij.execution.process.ProcessOutputTypes.STDOUT;
/**
* @author Eugene.Kudelevsky
*/
public class AndroidRunConfiguration extends AndroidRunConfigurationBase implements RefactoringListenerProvider {
@NonNls public static final String LAUNCH_DEFAULT_ACTIVITY = "default_activity";
@NonNls public static final String LAUNCH_SPECIFIC_ACTIVITY = "specific_activity";
@NonNls public static final String DO_NOTHING = "do_nothing";
public String ACTIVITY_CLASS = "";
public String MODE = LAUNCH_DEFAULT_ACTIVITY;
public boolean DEPLOY = true;
public String ARTIFACT_NAME = "";
public AndroidRunConfiguration(Project project, ConfigurationFactory factory) {
super(project, factory);
}
@Override
protected Pair<Boolean, String> supportsRunningLibraryProjects(@NotNull AndroidFacet facet) {
return Pair.create(Boolean.FALSE, AndroidBundle.message("android.cannot.run.library.project.error"));
}
@Override
protected void checkConfiguration(@NotNull AndroidFacet facet) throws RuntimeConfigurationException {
final boolean packageContainMavenProperty = doesPackageContainMavenProperty(facet);
final JavaRunConfigurationModule configurationModule = getConfigurationModule();
Module module = facet.getModule();
if (MODE.equals(LAUNCH_SPECIFIC_ACTIVITY)) {
Project project = configurationModule.getProject();
final JavaPsiFacade facade = JavaPsiFacade.getInstance(project);
PsiClass activityClass = facade.findClass(AndroidUtils.ACTIVITY_BASE_CLASS_NAME, ProjectScope.getAllScope(project));
if (activityClass == null) {
throw new RuntimeConfigurationError(AndroidBundle.message("cant.find.activity.class.error"));
}
if (ACTIVITY_CLASS == null || ACTIVITY_CLASS.length() == 0) {
throw new RuntimeConfigurationError(AndroidBundle.message("activity.class.not.specified.error"));
}
PsiClass c = configurationModule.findClass(ACTIVITY_CLASS);
if (c == null || !c.isInheritor(activityClass, true)) {
final ActivityAlias activityAlias = findActivityAlias(facet, ACTIVITY_CLASS);
if (activityAlias == null) {
throw new RuntimeConfigurationError(AndroidBundle.message("not.activity.subclass.error", ACTIVITY_CLASS));
}
if (!isActivityLaunchable(activityAlias.getIntentFilters())) {
throw new RuntimeConfigurationError(AndroidBundle.message("activity.not.launchable.error", AndroidUtils.LAUNCH_ACTION_NAME));
}
return;
}
if (!packageContainMavenProperty) {
List<Activity> activities = ManifestInfo.get(module, true).getActivities();
Activity activity = AndroidDomUtil.getActivityDomElementByClass(activities, c);
Module libModule = null;
if (activity == null) {
for (AndroidFacet depFacet : AndroidUtils.getAllAndroidDependencies(module, true)) {
final Module depModule = depFacet.getModule();
activities = ManifestInfo.get(depModule, true).getActivities();
activity = AndroidDomUtil.getActivityDomElementByClass(activities, c);
if (activity != null) {
libModule = depModule;
break;
}
}
if (activity == null) {
throw new RuntimeConfigurationError(AndroidBundle.message("activity.not.declared.in.manifest", c.getName()));
}
else if (!facet.getProperties().ENABLE_MANIFEST_MERGING) {
throw new RuntimeConfigurationError(AndroidBundle.message("activity.declared.but.manifest.merging.disabled", c.getName(),
libModule.getName(), module.getName()));
}
}
}
}
else if (MODE.equals(LAUNCH_DEFAULT_ACTIVITY)) {
if (packageContainMavenProperty) {
return;
}
List<Activity> activities = ManifestInfo.get(module, true).getActivities();
List<ActivityAlias> activityAliases = ManifestInfo.get(module, true).getActivityAliases();
String activity = AndroidUtils.getDefaultLauncherActivityName(activities, activityAliases);
if (activity != null) {
return;
}
throw new RuntimeConfigurationError(AndroidBundle.message("default.activity.not.found.error"));
}
}
@Nullable
private static ActivityAlias findActivityAlias(@NotNull AndroidFacet facet, @NotNull String name) {
ActivityAlias alias = doFindActivityAlias(facet, name);
if (alias != null) {
return alias;
}
for (AndroidFacet depFacet : AndroidUtils.getAllAndroidDependencies(facet.getModule(), true)) {
alias = doFindActivityAlias(depFacet, name);
if (alias != null) {
return alias;
}
}
return null;
}
@Nullable
private static ActivityAlias doFindActivityAlias(@NotNull AndroidFacet facet, @NotNull String name) {
final Manifest manifest = facet.getManifest();
if (manifest == null) {
return null;
}
final Application application = manifest.getApplication();
if (application == null) {
return null;
}
final String aPackage = manifest.getPackage().getStringValue();
for (ActivityAlias activityAlias : application.getActivityAliass()) {
final String alias = activityAlias.getName().getStringValue();
if (alias != null && alias.length() > 0 && name.endsWith(alias)) {
String prefix = name.substring(0, name.length() - alias.length());
if (prefix.endsWith(".")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
if (prefix.length() == 0 || prefix.equals(aPackage)) {
return activityAlias;
}
}
}
return null;
}
private static boolean doesPackageContainMavenProperty(@NotNull AndroidFacet facet) {
final Manifest manifest = facet.getManifest();
if (manifest == null) {
return false;
}
final String aPackage = manifest.getPackage().getStringValue();
return aPackage != null && aPackage.contains("${");
}
@Override
public AndroidRunningState getState(@NotNull Executor executor, @NotNull ExecutionEnvironment env) throws ExecutionException {
AndroidRunningState state = super.getState(executor, env);
if (state != null) {
state.setDeploy(DEPLOY);
state.setArtifactName(ARTIFACT_NAME);
state.setOpenLogcatAutomatically(SHOW_LOGCAT_AUTOMATICALLY);
state.setFilterLogcatAutomatically(FILTER_LOGCAT_AUTOMATICALLY);
}
return state;
}
@NotNull
@Override
public SettingsEditor<? extends RunConfiguration> getConfigurationEditor() {
Project project = getProject();
AndroidRunConfigurationEditor<AndroidRunConfiguration> editor =
new AndroidRunConfigurationEditor<AndroidRunConfiguration>(project, Predicates.<AndroidFacet>alwaysFalse());
editor.setConfigurationSpecificEditor(new ApplicationRunParameters(project, editor.getModuleSelector()));
return editor;
}
@Override
@Nullable
public RefactoringElementListener getRefactoringElementListener(PsiElement element) {
return RefactoringListeners.getClassOrPackageListener(element, new RefactoringListeners.Accessor<PsiClass>() {
@Override
public void setName(String qualifiedName) {
ACTIVITY_CLASS = qualifiedName;
}
@Nullable
@Override
public PsiClass getPsiElement() {
return getConfigurationModule().findClass(ACTIVITY_CLASS);
}
@Override
public void setPsiElement(PsiClass psiClass) {
ACTIVITY_CLASS = JavaExecutionUtil.getRuntimeQualifiedName(psiClass);
}
});
}
@NotNull
@Override
protected ConsoleView attachConsole(AndroidRunningState state, Executor executor) {
Project project = getConfigurationModule().getProject();
final TextConsoleBuilder builder = TextConsoleBuilderFactory.getInstance().createBuilder(project);
ConsoleView console = builder.getConsole();
console.attachToProcess(state.getProcessHandler());
return console;
}
@Override
protected boolean supportMultipleDevices() {
return true;
}
@Nullable
@Override
protected AndroidApplicationLauncher getApplicationLauncher(final AndroidFacet facet) {
return new MyApplicationLauncher() {
@Nullable
@Override
protected String getActivityName(@Nullable ProcessHandler processHandler) {
return getActivityToLaunch(facet, processHandler);
}
};
}
@Nullable
private String getActivityToLaunch(@NotNull final AndroidFacet facet, @Nullable ProcessHandler processHandler) {
String activityToLaunch = null;
if (MODE.equals(LAUNCH_DEFAULT_ACTIVITY)) {
final String defaultActivityName = computeDefaultActivity(facet, processHandler);
if (defaultActivityName != null) {
activityToLaunch = defaultActivityName;
}
else {
if (processHandler != null) {
processHandler.notifyTextAvailable(AndroidBundle.message("default.activity.not.found.error"), STDERR);
}
return null;
}
}
else if (MODE.equals(LAUNCH_SPECIFIC_ACTIVITY)) {
activityToLaunch = ACTIVITY_CLASS;
}
if (activityToLaunch != null) {
final String finalActivityToLaunch = activityToLaunch;
final String activityRuntimeQName = ApplicationManager.getApplication().runReadAction(new Computable<String>() {
@Override
public String compute() {
final GlobalSearchScope scope = facet.getModule().getModuleWithDependenciesAndLibrariesScope(false);
final PsiClass activityClass = JavaPsiFacade.getInstance(getProject()).findClass(finalActivityToLaunch, scope);
if (activityClass != null) {
return JavaExecutionUtil.getRuntimeQualifiedName(activityClass);
}
return null;
}
});
if (activityRuntimeQName != null) {
return activityRuntimeQName;
}
}
return activityToLaunch;
}
@Nullable
@VisibleForTesting
static String computeDefaultActivity(@NotNull final AndroidFacet facet, @Nullable final ProcessHandler processHandler) {
if (!facet.getProperties().USE_CUSTOM_COMPILER_MANIFEST) {
final boolean useMergedManifest = facet.isGradleProject() || facet.getProperties().ENABLE_MANIFEST_MERGING;
final ManifestInfo manifestInfo = ManifestInfo.get(facet.getModule(), useMergedManifest);
return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
@Override
public String compute() {
return AndroidUtils.getDefaultLauncherActivityName(manifestInfo.getActivities(), manifestInfo.getActivityAliases());
}
});
}
File manifestCopy = null;
try {
final Pair<File, String> pair = getCopyOfCompilerManifestFile(facet, processHandler);
manifestCopy = pair != null ? pair.getFirst() : null;
VirtualFile manifestVFile = manifestCopy != null ? LocalFileSystem.getInstance().findFileByIoFile(manifestCopy) : null;
final Manifest manifest =
manifestVFile == null ? null : AndroidUtils.loadDomElement(facet.getModule(), manifestVFile, Manifest.class);
return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
@Nullable
@Override
public String compute() {
if (manifest == null) {
if (processHandler != null) {
processHandler.notifyTextAvailable("Cannot find " + SdkConstants.FN_ANDROID_MANIFEST_XML + " file\n", STDERR);
}
return null;
}
return AndroidUtils.getDefaultLauncherActivityName(manifest);
}
});
}
finally {
if (manifestCopy != null) {
FileUtil.delete(manifestCopy.getParentFile());
}
}
}
private static boolean isActivityLaunchable(List<IntentFilter> intentFilters) {
for (IntentFilter filter : intentFilters) {
if (AndroidDomUtil.containsAction(filter, AndroidUtils.LAUNCH_ACTION_NAME)) {
return true;
}
}
return false;
}
private static abstract class MyApplicationLauncher extends AndroidApplicationLauncher {
private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.run.AndroidRunConfiguration.MyApplicationLauncher");
@Nullable
protected abstract String getActivityName(@Nullable ProcessHandler processHandler);
@SuppressWarnings({"EnumSwitchStatementWhichMissesCases"})
@Override
public boolean isReadyForDebugging(ClientData data, ProcessHandler processHandler) {
final String activityName = getActivityName(processHandler);
if (activityName == null) {
ClientData.DebuggerStatus status = data.getDebuggerConnectionStatus();
switch (status) {
case ERROR:
if (processHandler != null) {
processHandler.notifyTextAvailable("Debug port is busy\n", STDOUT);
}
LOG.info("Debug port is busy");
return false;
case ATTACHED:
if (processHandler != null) {
processHandler.notifyTextAvailable("Debugger already attached\n", STDOUT);
}
LOG.info("Debugger already attached");
return false;
default:
return true;
}
}
return super.isReadyForDebugging(data, processHandler);
}
@Override
public LaunchResult launch(@NotNull AndroidRunningState state, @NotNull IDevice device)
throws IOException, AdbCommandRejectedException, TimeoutException {
ProcessHandler processHandler = state.getProcessHandler();
String activityName = getActivityName(processHandler);
if (activityName == null) return LaunchResult.NOTHING_TO_DO;
activityName = activityName.replace("$", "\\$");
final String activityPath = state.getPackageName() + '/' + activityName;
if (state.isStopped()) return LaunchResult.STOP;
processHandler.notifyTextAvailable("Launching application: " + activityPath + ".\n", STDOUT);
AndroidRunningState.MyReceiver receiver = state.new MyReceiver();
boolean debug = state.isDebugMode();
while (true) {
if (state.isStopped()) return LaunchResult.STOP;
String command = "am start " +
(debug ? "-D " : "") +
"-n \"" + activityPath + "\" " +
"-a android.intent.action.MAIN " +
"-c android.intent.category.LAUNCHER";
boolean deviceNotResponding = false;
try {
state.executeDeviceCommandAndWriteToConsole(device, command, receiver);
}
catch (ShellCommandUnresponsiveException e) {
LOG.info(e);
deviceNotResponding = true;
}
if (!deviceNotResponding && receiver.getErrorType() != 2) {
break;
}
processHandler.notifyTextAvailable("Device is not ready. Waiting for " + AndroidRunningState.WAITING_TIME + " sec.\n", STDOUT);
synchronized (state.getRunningLock()) {
try {
state.getRunningLock().wait(AndroidRunningState.WAITING_TIME * 1000);
}
catch (InterruptedException e) {
}
}
receiver = state.new MyReceiver();
}
boolean success = receiver.getErrorType() == AndroidRunningState.NO_ERROR;
if (success) {
processHandler.notifyTextAvailable(receiver.getOutput().toString(), STDOUT);
}
else {
processHandler.notifyTextAvailable(receiver.getOutput().toString(), STDERR);
}
return success ? LaunchResult.SUCCESS : LaunchResult.STOP;
}
}
}