/*
* Copyright (C) 2013 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.startup;
import com.android.SdkConstants;
import com.android.sdklib.IAndroidTarget;
import com.android.tools.idea.actions.*;
import com.android.tools.idea.avdmanager.AvdListDialog;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.android.tools.idea.gradle.util.Projects;
import com.android.tools.idea.gradle.util.PropertiesUtil;
import com.android.tools.idea.run.ArrayMapRenderer;
import com.android.tools.idea.sdk.DefaultSdks;
import com.android.tools.idea.sdk.VersionCheck;
import com.android.utils.Pair;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.intellij.debugger.settings.NodeRendererSettings;
import com.intellij.ide.AppLifecycleListener;
import com.intellij.ide.actions.TemplateProjectSettingsGroup;
import com.intellij.ide.projectView.actions.MarkRootGroup;
import com.intellij.ide.projectView.impl.MoveModuleToGroupTopLevel;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.HighlighterColors;
import com.intellij.openapi.editor.XmlHighlighterColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.extensions.ExtensionPoint;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.options.ConfigurableEP;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkAdditionalData;
import com.intellij.openapi.projectRoots.SdkModificator;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.codeStyle.CodeStyleScheme;
import com.intellij.psi.codeStyle.CodeStyleSchemes;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.util.PlatformUtils;
import com.intellij.util.SystemProperties;
import com.intellij.util.messages.MessageBusConnection;
import org.jetbrains.android.AndroidPlugin;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
import org.jetbrains.android.sdk.AndroidSdkType;
import org.jetbrains.android.sdk.AndroidSdkUtils;
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.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.Properties;
/** Initialization performed only in the context of the Android IDE. */
public class AndroidStudioSpecificInitializer implements Runnable {
/**
* We set the timeout for Gradle daemons to -1, this way IDEA will not set it to 1 minute and it will use the default instead (3 hours.)
* We need to keep Gradle daemons around as much as possible because creating new daemons is resource-consuming and slows down the IDE.
*/
public static final int GRADLE_DAEMON_TIMEOUT_MS = -1;
static {
System.setProperty("external.system.remote.process.idle.ttl.ms", String.valueOf(GRADLE_DAEMON_TIMEOUT_MS));
}
private static final Logger LOG = Logger.getInstance("#com.android.tools.idea.startup.AndroidStudioSpecificInitializer");
private static final List<String> IDE_SETTINGS_TO_REMOVE = Lists.newArrayList(
"org.jetbrains.plugins.javaFX.JavaFxSettingsConfigurable", "org.intellij.plugins.xpathView.XPathConfigurable",
"org.intellij.lang.xpath.xslt.impl.XsltConfigImpl$UIImpl"
);
@NonNls private static final String USE_IDEA_NEW_PROJECT_WIZARDS = "use.idea.newProjectWizard";
@NonNls private static final String USE_JPS_MAKE_ACTIONS = "use.idea.jpsMakeActions";
@NonNls private static final String USE_IDEA_NEW_FILE_POPUPS = "use.idea.newFilePopupActions";
@NonNls private static final String USE_IDEA_PROJECT_STRUCTURE = "use.idea.projectStructure";
@NonNls public static final String ENABLE_EXPERIMENTAL_ACTIONS = "enable.experimental.actions";
@NonNls private static final String ANDROID_SDK_FOLDER_NAME = "sdk";
/** Paths relative to the IDE installation folder where the Android SDK maybe present. */
private static final String[] ANDROID_SDK_RELATIVE_PATHS =
{ ANDROID_SDK_FOLDER_NAME, File.separator + ".." + File.separator + ANDROID_SDK_FOLDER_NAME,};
public static boolean isAndroidStudio() {
return "AndroidStudio".equals(PlatformUtils.getPlatformPrefix());
}
private static void checkInstallation() {
String studioHome = PathManager.getHomePath();
if (StringUtil.isEmpty(studioHome)) {
LOG.info("Unable to find Studio home directory");
return;
}
File studioHomePath = new File(FileUtil.toSystemDependentName(studioHome));
if (!studioHomePath.isDirectory()) {
LOG.info(String.format("The path '%1$s' does not belong to an existing directory", studioHomePath.getPath()));
return;
}
File androidPluginLibFolderPath = new File(studioHomePath, FileUtil.join("plugins", "android", "lib"));
if (!androidPluginLibFolderPath.isDirectory()) {
LOG.info(String.format("The path '%1$s' does not belong to an existing directory", androidPluginLibFolderPath.getPath()));
return;
}
// Look for signs that the installation is corrupt due to improper updates (typically unzipping on top of previous install)
// which doesn't delete files that have been removed or renamed
String cause = null;
File[] children = FileUtil.notNullize(androidPluginLibFolderPath.listFiles());
if (hasMoreThanOneBuilderModelFile(children)) {
cause = "(Found multiple versions of builder-model-*.jar in plugins/android/lib.)";
} else if (new File(studioHomePath, FileUtil.join("plugins", "android-designer")).exists()) {
cause = "(Found plugins/android-designer which should not be present.)";
}
if (cause != null) {
String msg = "Your Android Studio installation is corrupt and will not work properly.\n" +
cause + "\n" +
"This usually happens if Android Studio is extracted into an existing older version.\n\n" +
"Please reinstall (and make sure the new installation directory is empty first.)";
String title = "Corrupt Installation";
int option = Messages.showDialog(msg, title, new String[]{"Quit", "Proceed Anyway"}, 0, Messages.getErrorIcon());
if (option == 0) {
ApplicationManagerEx.getApplicationEx().exit();
}
}
}
@VisibleForTesting
static boolean hasMoreThanOneBuilderModelFile(@NotNull File[] libraryFiles) {
int builderModelFileCount = 0;
for (File file : libraryFiles) {
String fileName = file.getName();
if (fileName.startsWith("builder-model-") && SdkConstants.EXT_JAR.equals(FileUtilRt.getExtension(fileName))) {
if (++builderModelFileCount > 1) {
return true;
}
}
}
return false;
}
private static void cleanUpIdePreferences() {
try {
ExtensionPoint<ConfigurableEP<Configurable>> ideConfigurable =
Extensions.getRootArea().getExtensionPoint(Configurable.APPLICATION_CONFIGURABLE);
GradleUtil.cleanUpPreferences(ideConfigurable, IDE_SETTINGS_TO_REMOVE);
}
catch (Throwable e) {
LOG.info("Failed to clean up IDE preferences", e);
}
}
private static void replaceIdeaNewProjectActions() {
// Unregister IntelliJ's version of the project actions and manually register our own.
replaceAction("NewProject", new AndroidNewProjectAction());
replaceAction("WelcomeScreen.CreateNewProject", new AndroidNewProjectAction());
replaceAction("NewModule", new AndroidNewModuleAction());
replaceAction("NewModuleInGroup", new AndroidNewModuleInGroupAction());
replaceAction("ImportProject", new AndroidImportProjectAction());
replaceAction("WelcomeScreen.ImportProject", new AndroidImportProjectAction());
replaceAction("CreateLibraryFromFile", new CreateLibraryFromFilesAction());
replaceAction("ImportModule", new AndroidImportModuleAction());
hideAction(IdeActions.ACTION_GENERATE_ANT_BUILD, "Generate Ant Build...");
hideAction("AddFrameworkSupport", "Add Framework Support...");
hideAction("BuildArtifact", "Build Artifacts...");
hideAction("RunTargetAction", "Run Ant Target");
replaceProjectPopupActions();
}
private static void replaceProjectStructureActions() {
AndroidTemplateProjectStructureAction showDefaultProjectStructureAction = new AndroidTemplateProjectStructureAction();
showDefaultProjectStructureAction.getTemplatePresentation().setText("Default Project Structure...");
replaceAction("TemplateProjectStructure", showDefaultProjectStructureAction);
ActionManager am = ActionManager.getInstance();
AnAction action = am.getAction("WelcomeScreen.Configure.IDEA");
if (action instanceof DefaultActionGroup) {
DefaultActionGroup projectSettingsGroup = (DefaultActionGroup)action;
AnAction[] children = projectSettingsGroup.getChildren(null);
if (children.length == 1 && children[0] instanceof TemplateProjectSettingsGroup) {
projectSettingsGroup.replaceAction(children[0], new AndroidTemplateProjectSettingsGroup());
}
}
}
private static void replaceIdeaMakeActions() {
// 'Build' > 'Make Project' action
replaceAction("CompileDirty", new AndroidMakeProjectAction());
// 'Build' > 'Make Modules' action
replaceAction(IdeActions.ACTION_MAKE_MODULE, new AndroidMakeModuleAction());
// 'Build' > 'Rebuild' action
replaceAction(IdeActions.ACTION_COMPILE_PROJECT, new AndroidRebuildProjectAction());
// 'Build' > 'Compile Modules' action
hideAction(IdeActions.ACTION_COMPILE, "Compile Module(s)");
}
private static void replaceAction(String actionId, AnAction newAction) {
ActionManager am = ActionManager.getInstance();
AnAction oldAction = am.getAction(actionId);
if (oldAction != null) {
newAction.getTemplatePresentation().setIcon(oldAction.getTemplatePresentation().getIcon());
am.unregisterAction(actionId);
}
am.registerAction(actionId, newAction);
}
private static void hideAction(@NotNull String actionId, @NotNull String backupText) {
AnAction oldAction = ActionManager.getInstance().getAction(actionId);
if (oldAction != null) {
AnAction newAction = new AndroidActionRemover(oldAction, backupText);
replaceAction(actionId, newAction);
}
}
private static void replaceProjectPopupActions() {
Deque<Pair<DefaultActionGroup, AnAction>> stack = new ArrayDeque<Pair<DefaultActionGroup, AnAction>>();
stack.add(Pair.of((DefaultActionGroup)null, ActionManager.getInstance().getAction("ProjectViewPopupMenu")));
while (!stack.isEmpty()) {
Pair<DefaultActionGroup, AnAction> entry = stack.pop();
DefaultActionGroup parent = entry.getFirst();
AnAction action = entry.getSecond();
if (action instanceof DefaultActionGroup) {
DefaultActionGroup actionGroup = (DefaultActionGroup)action;
for (AnAction child : actionGroup.getChildActionsOrStubs()) {
stack.push(Pair.of(actionGroup, child));
}
}
if (action instanceof MoveModuleToGroupTopLevel) {
parent.remove(action);
parent.add(new AndroidActionGroupRemover((ActionGroup)action, "Move Module to Group"),
new Constraints(Anchor.AFTER, "OpenModuleSettings"));
} else if (action instanceof MarkRootGroup) {
parent.remove(action);
parent.add(new AndroidActionGroupRemover((ActionGroup)action, "Mark Directory As"),
new Constraints(Anchor.AFTER, "OpenModuleSettings"));
}
}
}
private static void setupSdks() {
File androidHome = DefaultSdks.getDefaultAndroidHome();
if (androidHome != null) {
// Do not prompt user to select SDK path (we have one already.) Instead, check SDK compatibility when a project is opened.
return;
}
// If running in a GUI test we don't want the "Select SDK" dialog to show up when running GUI tests.
if (AndroidPlugin.isGuiTestingMode()) {
// This is good enough. Later on in the GUI test we'll validate the given SDK path.
return;
}
final Sdk sdk = findFirstCompatibleAndroidSdk();
if (sdk != null) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
String androidHome = sdk.getHomePath();
assert androidHome != null;
DefaultSdks.createAndroidSdksForAllTargets(new File(FileUtil.toSystemDependentName(androidHome)));
}
});
return;
}
// Called in a 'invokeLater' block, otherwise file chooser will hang forever.
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
File androidSdkPath = getAndroidSdkPath();
if (androidSdkPath == null) {
return;
}
Sdk sdk = AndroidSdkUtils.createNewAndroidPlatform(androidSdkPath.getPath(), true);
if (sdk != null) {
// Rename the SDK to fit our default naming convention.
if (sdk.getName().startsWith(AndroidSdkUtils.SDK_NAME_PREFIX)) {
SdkModificator sdkModificator = sdk.getSdkModificator();
sdkModificator.setName(AndroidSdkUtils.SDK_NAME_PREFIX +
sdk.getName().substring(AndroidSdkUtils.SDK_NAME_PREFIX.length()));
sdkModificator.commitChanges();
// Rename the JDK that goes along with this SDK.
AndroidSdkAdditionalData additionalData = (AndroidSdkAdditionalData)sdk.getSdkAdditionalData();
if (additionalData != null) {
Sdk jdk = additionalData.getJavaSdk();
if (jdk != null) {
sdkModificator = jdk.getSdkModificator();
sdkModificator.setName(AndroidSdkUtils.DEFAULT_JDK_NAME);
sdkModificator.commitChanges();
}
}
// Fill out any missing build APIs for this new SDK.
DefaultSdks.createAndroidSdksForAllTargets(androidSdkPath);
}
}
}
});
}
@Nullable
private static Sdk findFirstCompatibleAndroidSdk() {
List<Sdk> sdks = AndroidSdkUtils.getAllAndroidSdks();
for (Sdk sdk : sdks) {
String sdkPath = sdk.getHomePath();
if (VersionCheck.isCompatibleVersion(sdkPath)) {
return sdk;
}
}
if (!sdks.isEmpty()) {
return sdks.get(0);
}
return null;
}
@Nullable
private static File getAndroidSdkPath() {
String studioHome = PathManager.getHomePath();
if (StringUtil.isEmpty(studioHome)) {
LOG.info("Unable to find Studio home directory");
}
else {
LOG.info(String.format("Found Studio home directory at: '%1$s'", studioHome));
for (String path : ANDROID_SDK_RELATIVE_PATHS) {
File dir = new File(studioHome, path);
String absolutePath = FileUtil.toCanonicalPath(dir.getAbsolutePath());
LOG.info(String.format("Looking for Android SDK at '%1$s'", absolutePath));
if (AndroidSdkType.getInstance().isValidSdkHome(absolutePath)) {
LOG.info(String.format("Found Android SDK at '%1$s'", absolutePath));
return new File(absolutePath);
}
}
}
LOG.info("Unable to locate SDK within the Android studio installation.");
String androidHomeValue = System.getenv(SdkConstants.ANDROID_HOME_ENV);
String msg = String.format("Checking if ANDROID_HOME is set: '%1$s' is '%2$s'", SdkConstants.ANDROID_HOME_ENV, androidHomeValue);
LOG.info(msg);
if (!StringUtil.isEmpty(androidHomeValue) && AndroidSdkType.getInstance().isValidSdkHome(androidHomeValue)) {
LOG.info("Using Android SDK specified by the environment variable.");
return new File(FileUtil.toSystemDependentName(androidHomeValue));
}
String sdkPath = getLastSdkPathUsedByAndroidTools();
if (!StringUtil.isEmpty(sdkPath) && AndroidSdkType.getInstance().isValidSdkHome(androidHomeValue)) {
msg = String.format("Last SDK used by Android tools: '%1$s'", sdkPath);
} else {
msg = "Unable to locate last SDK used by Android tools";
}
LOG.info(msg);
return sdkPath == null ? null : new File(FileUtil.toSystemDependentName(sdkPath));
}
/**
* Returns the value for property 'lastSdkPath' as stored in the properties file at $HOME/.android/ddms.cfg, or {@code null} if the file
* or property doesn't exist.
*
* This is only useful in a scenario where existing users of ADT/Eclipse get Studio, but without the bundle. This method duplicates some
* functionality of {@link com.android.prefs.AndroidLocation} since we don't want any file system writes to happen during this process.
*/
@Nullable
private static String getLastSdkPathUsedByAndroidTools() {
String userHome = SystemProperties.getUserHome();
if (userHome == null) {
return null;
}
File f = new File(new File(userHome, ".android"), "ddms.cfg");
if (!f.exists()) {
return null;
}
try {
Properties properties = PropertiesUtil.getProperties(f);
return properties.getProperty("lastSdkPath");
} catch (IOException e) {
return null;
}
}
/**
* Remove popup actions that we don't use
*/
private static void hideIdeaNewFilePopupActions() {
hideAction("NewHtmlFile", "HTML File");
hideAction("NewPackageInfo", "package-info.java");
// Hide designer actions
hideAction("NewForm", "GUI Form");
hideAction("NewDialog", "Dialog");
hideAction("NewFormSnapshot", "Form Snapshot");
// Hide individual actions that aren't part of a group
replaceAction("Groovy.NewClass", new EmptyAction());
replaceAction("Groovy.NewScript", new EmptyAction());
}
/**
* Registers an appClosing callback on the app lifecycle.
* Uses it to stop gradle daemons of currently opened projects.
*/
private static void registerAppClosing() {
MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect();
connection.subscribe(AppLifecycleListener.TOPIC, new AppLifecycleListener.Adapter() {
@Override
public void appClosing() {
try {
for (Project p : ProjectManager.getInstance().getOpenProjects()) {
if (Projects.isBuildWithGradle(p)) {
GradleUtil.stopAllGradleDaemons(false);
return;
}
}
}
catch (IOException e) {
LOG.info("Failed to stop Gradle daemons", e);
}
}
});
}
private static void checkAndSetAndroidSdkSources() {
for (Sdk sdk : AndroidSdkUtils.getAllAndroidSdks()) {
checkAndSetSources(sdk);
}
}
private static void checkAndSetSources(@NotNull Sdk sdk) {
VirtualFile[] storedSources = sdk.getRootProvider().getFiles(OrderRootType.SOURCES);
if (storedSources.length > 0) {
return;
}
SdkAdditionalData sdkData = sdk.getSdkAdditionalData();
if (sdkData instanceof AndroidSdkAdditionalData) {
AndroidSdkAdditionalData androidSdkData = (AndroidSdkAdditionalData)sdkData;
AndroidPlatform platform = androidSdkData.getAndroidPlatform();
if (platform != null) {
SdkModificator sdkModificator = sdk.getSdkModificator();
IAndroidTarget target = platform.getTarget();
AndroidSdkUtils.findAndSetPlatformSources(target, sdkModificator);
sdkModificator.commitChanges();
}
}
}
@Override
public void run() {
checkInstallation();
cleanUpIdePreferences();
if (!Boolean.getBoolean(USE_IDEA_NEW_PROJECT_WIZARDS)) {
replaceIdeaNewProjectActions();
}
if (!Boolean.getBoolean(USE_IDEA_PROJECT_STRUCTURE)) {
replaceProjectStructureActions();
}
if (!Boolean.getBoolean(USE_JPS_MAKE_ACTIONS)) {
replaceIdeaMakeActions();
}
if (!Boolean.getBoolean(USE_IDEA_NEW_FILE_POPUPS)) {
hideIdeaNewFilePopupActions();
}
try {
// Setup JDK and Android SDK if necessary
setupSdks();
} catch (Exception e) {
LOG.error("Unexpected error while setting up SDKs: ", e);
}
registerAppClosing();
// Always reset the Default scheme to match Android standards
// User modifications won't be lost since they are made in a separate scheme (copied off of this default scheme)
CodeStyleScheme scheme = CodeStyleSchemes.getInstance().getDefaultScheme();
if (scheme != null) {
CodeStyleSettings settings = scheme.getCodeStyleSettings();
if (settings != null) {
AndroidCodeStyleSettingsModifier.modify(settings);
}
}
// Modify built-in "Default" color scheme to remove background from XML tags.
// "Darcula" and user schemes will not be touched.
EditorColorsScheme colorsScheme = EditorColorsManager.getInstance().getScheme(EditorColorsScheme.DEFAULT_SCHEME_NAME);
TextAttributes textAttributes = colorsScheme.getAttributes(HighlighterColors.TEXT);
TextAttributes xmlTagAttributes = colorsScheme.getAttributes(XmlHighlighterColors.XML_TAG);
xmlTagAttributes.setBackgroundColor(textAttributes.getBackgroundColor());
NodeRendererSettings.getInstance().addPluginRenderer(new ArrayMapRenderer("android.util.ArrayMap"));
NodeRendererSettings.getInstance().addPluginRenderer(new ArrayMapRenderer("android.support.v4.util.ArrayMap"));
checkAndSetAndroidSdkSources();
}
}