blob: 071b71132b786484cdba7c2f7ef7b92328c5db9d [file] [log] [blame]
/*
* 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.gradle.service;
import com.android.builder.model.AndroidProject;
import com.android.sdklib.repository.FullRevision;
import com.android.sdklib.repository.FullRevision.PreviewComparison;
import com.android.sdklib.repository.PreciseRevision;
import com.android.tools.idea.gradle.AndroidProjectKeys;
import com.android.tools.idea.gradle.GradleSyncState;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.compiler.PostProjectBuildTasksExecutor;
import com.android.tools.idea.gradle.customizer.ModuleCustomizer;
import com.android.tools.idea.gradle.customizer.android.*;
import com.android.tools.idea.gradle.messages.Message;
import com.android.tools.idea.gradle.messages.ProjectSyncMessages;
import com.android.tools.idea.gradle.parser.BuildFileKey;
import com.android.tools.idea.gradle.parser.GradleBuildFile;
import com.android.tools.idea.gradle.project.AndroidGradleNotification;
import com.android.tools.idea.gradle.service.notification.hyperlink.FixGradleModelVersionHyperlink;
import com.android.tools.idea.gradle.service.notification.hyperlink.NotificationHyperlink;
import com.android.tools.idea.gradle.service.notification.hyperlink.OpenUrlHyperlink;
import com.android.tools.idea.sdk.IdeSdks;
import com.android.tools.idea.sdk.Jdks;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.application.RunResult;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.Key;
import com.intellij.openapi.externalSystem.service.notification.ExternalSystemNotificationManager;
import com.intellij.openapi.externalSystem.service.notification.NotificationCategory;
import com.intellij.openapi.externalSystem.service.notification.NotificationData;
import com.intellij.openapi.externalSystem.service.project.manage.ProjectDataService;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.vfs.encoding.EncodingProjectManager;
import com.intellij.pom.java.LanguageLevel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static com.android.sdklib.repository.PreciseRevision.parseRevision;
import static com.android.tools.idea.gradle.messages.CommonMessageGroupNames.EXTRA_GENERATED_SOURCES;
import static com.android.tools.idea.gradle.messages.CommonMessageGroupNames.UNHANDLED_SYNC_ISSUE_TYPE;
import static com.android.tools.idea.gradle.messages.Message.Type.INFO;
import static com.android.tools.idea.gradle.messages.Message.Type.WARNING;
import static com.android.tools.idea.gradle.util.GradleUtil.GRADLE_SYSTEM_ID;
import static com.android.tools.idea.gradle.util.GradleUtil.hasLayoutRenderingIssue;
import static com.android.tools.idea.sdk.Jdks.isApplicableJdk;
import static com.android.tools.idea.startup.AndroidStudioSpecificInitializer.isAndroidStudio;
import static com.intellij.ide.impl.NewProjectUtil.applyJdkToProject;
import static com.intellij.openapi.externalSystem.service.notification.NotificationSource.PROJECT_SYNC;
import static com.intellij.openapi.util.io.FileUtil.toSystemDependentName;
import static java.util.Collections.sort;
/**
* Service that sets an Android SDK and facets to the modules of a project that has been imported from an Android-Gradle project.
*/
public class AndroidProjectDataService implements ProjectDataService<IdeaAndroidProject, Void> {
private static final Logger LOG = Logger.getInstance(AndroidProjectDataService.class);
private final List<ModuleCustomizer<IdeaAndroidProject>> myCustomizers;
// This constructor is called by the IDE. See this module's plugin.xml file, implementation of extension 'externalProjectDataService'.
@SuppressWarnings("unused")
public AndroidProjectDataService() {
this(ImmutableList.of(new AndroidSdkModuleCustomizer(), new AndroidFacetModuleCustomizer(), new ContentRootModuleCustomizer(),
new RunConfigModuleCustomizer(), new DependenciesModuleCustomizer(), new CompilerOutputModuleCustomizer()));
}
@VisibleForTesting
AndroidProjectDataService(@NotNull List<ModuleCustomizer<IdeaAndroidProject>> customizers) {
myCustomizers = customizers;
}
@NotNull
@Override
public Key<IdeaAndroidProject> getTargetDataKey() {
return AndroidProjectKeys.IDE_ANDROID_PROJECT;
}
/**
* Sets an Android SDK and facets to the modules of a project that has been imported from an Android-Gradle project.
*
* @param toImport contains the Android-Gradle project.
* @param project IDEA project to configure.
* @param synchronous indicates whether this operation is synchronous.
*/
@Override
public void importData(@NotNull Collection<DataNode<IdeaAndroidProject>> toImport, @NotNull Project project, boolean synchronous) {
if (!toImport.isEmpty()) {
try {
doImport(toImport, project);
}
catch (Throwable e) {
LOG.error(String.format("Failed to set up Android modules in project '%1$s'", project.getName()), e);
String msg = e.getMessage();
if (msg == null) {
msg = e.getClass().getCanonicalName();
}
GradleSyncState.getInstance(project).syncFailed(msg);
}
}
}
private void doImport(final Collection<DataNode<IdeaAndroidProject>> toImport, final Project project) throws Throwable {
RunResult result = new WriteCommandAction.Simple(project) {
@Override
protected void run() throws Throwable {
LanguageLevel javaLangVersion = null;
ProjectSyncMessages messages = ProjectSyncMessages.getInstance(project);
boolean hasExtraGeneratedFolders = false;
Map<String, IdeaAndroidProject> androidModelsByModuleName = indexByModuleName(toImport);
Charset ideEncoding = EncodingProjectManager.getInstance(project).getDefaultCharset();
FullRevision oneDotTwoModelVersion = new PreciseRevision(1, 2, 0);
String nonMatchingModelEncodingFound = null;
String modelVersionWithLayoutRenderingIssue = null;
// Module name, build
List<String> modulesUsingBuildTools23rc1 = Lists.newArrayList();
ModuleManager moduleManager = ModuleManager.getInstance(project);
for (Module module : moduleManager.getModules()) {
IdeaAndroidProject androidModel = androidModelsByModuleName.get(module.getName());
customizeModule(module, project, androidModel);
if (androidModel != null) {
AndroidProject delegate = androidModel.getAndroidProject();
checkBuildToolsCompatibility(module, delegate, modulesUsingBuildTools23rc1);
// Verify that if Gradle is 2.4 (or newer,) the model is at least version 1.2.0.
if (modelVersionWithLayoutRenderingIssue == null && hasLayoutRenderingIssue(delegate)) {
modelVersionWithLayoutRenderingIssue = delegate.getModelVersion();
}
FullRevision modelVersion = parseRevision(delegate.getModelVersion());
boolean isModelVersionOneDotTwoOrNewer = modelVersion.compareTo(oneDotTwoModelVersion, PreviewComparison.IGNORE) >= 0;
// Verify that the encoding in the model is the same as the encoding in the IDE's project settings.
Charset modelEncoding = null;
if (isModelVersionOneDotTwoOrNewer) {
try {
modelEncoding = Charset.forName(delegate.getJavaCompileOptions().getEncoding());
}
catch (UnsupportedCharsetException ignore) {
// It's not going to happen.
}
}
if (nonMatchingModelEncodingFound == null && modelEncoding != null && ideEncoding.compareTo(modelEncoding) != 0) {
nonMatchingModelEncodingFound = modelEncoding.displayName();
}
// Get the Java language version from the model.
if (javaLangVersion == null) {
javaLangVersion = androidModel.getJavaLanguageLevel();
}
// Warn users that there are generated source folders at the wrong location.
File[] sourceFolders = androidModel.getExtraGeneratedSourceFolders();
if (sourceFolders.length > 0) {
hasExtraGeneratedFolders = true;
}
for (File folder : sourceFolders) {
// Have to add a word before the path, otherwise IDEA won't show it.
String[] text = {"Folder " + folder.getPath()};
messages.add(new Message(EXTRA_GENERATED_SOURCES, WARNING, text));
}
}
}
if (!modulesUsingBuildTools23rc1.isEmpty()) {
reportBuildTools23rc1Usage(modulesUsingBuildTools23rc1, project);
}
if (nonMatchingModelEncodingFound != null) {
setIdeEncodingAndAddEncodingMismatchMessage(nonMatchingModelEncodingFound, project);
}
if (modelVersionWithLayoutRenderingIssue != null) {
addLayoutRenderingIssueMessage(modelVersionWithLayoutRenderingIssue, project);
}
if (hasExtraGeneratedFolders) {
messages.add(new Message(EXTRA_GENERATED_SOURCES, INFO, "3rd-party Gradle plug-ins may be the cause"));
}
Sdk jdk = ProjectRootManager.getInstance(project).getProjectSdk();
if (jdk == null || (!isAndroidStudio() && !isApplicableJdk(jdk, javaLangVersion))) {
jdk = Jdks.chooseOrCreateJavaSdk(javaLangVersion);
}
if (jdk == null) {
String title = String.format("Problems importing/refreshing Gradle project '%1$s':\n", project.getName());
LanguageLevel level = javaLangVersion != null ? javaLangVersion : LanguageLevel.JDK_1_6;
String msg = String.format("Unable to find a JDK %1$s installed.\n", level.getPresentableText());
msg += "After configuring a suitable JDK in the \"Project Structure\" dialog, sync the Gradle project again.";
NotificationData notification = new NotificationData(title, msg, NotificationCategory.ERROR, PROJECT_SYNC);
ExternalSystemNotificationManager.getInstance(project).showNotification(GRADLE_SYSTEM_ID, notification);
}
else {
String homePath = jdk.getHomePath();
if (homePath != null) {
applyJdkToProject(project, jdk);
homePath = toSystemDependentName(homePath);
IdeSdks.setJdkPath(new File(homePath));
PostProjectBuildTasksExecutor.getInstance(project).updateJavaLangLevelAfterBuild();
}
}
}
}.execute();
Throwable error = result.getThrowable();
if (error != null) {
throw error;
}
}
// Build Tools 23 only works with Android plugin 1.3 or newer. Verify that the project is using compatible Build Tools/Android plugin
// versions.
private static void checkBuildToolsCompatibility(@NotNull Module module,
@NotNull AndroidProject project,
@NotNull List<String> moduleNames) {
if (isOneDotThreeOrLater(project)) {
return;
}
GradleBuildFile buildFile = GradleBuildFile.get(module);
if (buildFile != null) {
Object value = buildFile.getValue(BuildFileKey.BUILD_TOOLS_VERSION);
if ("23.0.0 rc1".equals(value)) {
moduleNames.add(module.getName());
}
}
}
private static boolean isOneDotThreeOrLater(@NotNull AndroidProject project) {
String modelVersion = project.getModelVersion();
// getApiVersion doesn't work prior to 1.2, and API level must be at least 3
return !(modelVersion.startsWith("1.0") || modelVersion.startsWith("1.1")) && project.getApiVersion() >= 3;
}
private static void reportBuildTools23rc1Usage(@NotNull List<String> moduleNames, @NotNull Project project) {
if (!moduleNames.isEmpty()) {
sort(moduleNames);
StringBuilder msg = new StringBuilder();
msg.append("Build Tools 23.0.0 rc1 is <b>deprecated</b>.<br>\n")
.append("Please update these modules to use Build Tools 23.0.0 rc2 instead:");
for (String moduleName : moduleNames) {
msg.append("<br>\n * ").append(moduleName);
}
msg.append("<br>\n<br>\nOtherwise the project won't build. ");
AndroidGradleNotification notification = AndroidGradleNotification.getInstance(project);
notification.showBalloon("Android Build Tools", msg.toString(), NotificationType.ERROR);
}
}
private static void setIdeEncodingAndAddEncodingMismatchMessage(@NotNull String newEncoding, @NotNull Project project) {
EncodingProjectManager encodings = EncodingProjectManager.getInstance(project);
String[] text = {
String.format("The project encoding (%1$s) has been reset to the encoding specified in the Gradle build files (%2$s).",
encodings.getDefaultCharset().displayName(), newEncoding),
"Mismatching encodings can lead to serious bugs."
};
encodings.setDefaultCharsetName(newEncoding);
NotificationHyperlink openDocHyperlink = new OpenUrlHyperlink("http://tools.android.com/knownissues/encoding", "More Info...");
ProjectSyncMessages.getInstance(project).add(new Message(UNHANDLED_SYNC_ISSUE_TYPE, INFO, text), openDocHyperlink);
}
private static void addLayoutRenderingIssueMessage(String modelVersion, @NotNull Project project) {
// See https://code.google.com/p/android/issues/detail?id=170841
NotificationHyperlink quickFix = new FixGradleModelVersionHyperlink(false);
NotificationHyperlink openDocHyperlink = new OpenUrlHyperlink("https://code.google.com/p/android/issues/detail?id=170841",
"More Info...");
String[] text = {
String.format("Using an obsolete version of the Gradle plugin (%1$s); this can lead to layouts not rendering correctly.",
modelVersion)
};
ProjectSyncMessages.getInstance(project).add(new Message(UNHANDLED_SYNC_ISSUE_TYPE, WARNING, text), openDocHyperlink, quickFix);
}
@NotNull
private static Map<String, IdeaAndroidProject> indexByModuleName(@NotNull Collection<DataNode<IdeaAndroidProject>> dataNodes) {
Map<String, IdeaAndroidProject> index = Maps.newHashMap();
for (DataNode<IdeaAndroidProject> d : dataNodes) {
IdeaAndroidProject androidModel = d.getData();
index.put(androidModel.getModuleName(), androidModel);
}
return index;
}
private void customizeModule(@NotNull Module module, @NotNull Project project, @Nullable IdeaAndroidProject androidModel) {
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
ModifiableRootModel rootModel = moduleRootManager.getModifiableModel();
try {
for (ModuleCustomizer<IdeaAndroidProject> customizer : myCustomizers) {
customizer.customizeModule(project, rootModel, androidModel);
}
}
finally {
rootModel.commit();
}
}
@Override
public void removeData(@NotNull Collection<? extends Void> toRemove, @NotNull Project project, boolean synchronous) {
}
}