blob: 2ca02306b196f6fce83294211370714e9af7e0ed [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.compiler;
import com.android.ide.common.blame.Message;
import com.android.tools.idea.gradle.GradleSyncState;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.invoker.GradleInvocationResult;
import com.android.tools.idea.gradle.project.AndroidGradleNotification;
import com.android.tools.idea.gradle.project.BuildSettings;
import com.android.tools.idea.gradle.project.GradleBuildListener;
import com.android.tools.idea.gradle.project.GradleProjectImporter;
import com.android.tools.idea.gradle.service.notification.hyperlink.NotificationHyperlink;
import com.android.tools.idea.gradle.util.BuildMode;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.Iterators;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.compiler.CompileContext;
import com.intellij.openapi.compiler.CompilerMessage;
import com.intellij.openapi.compiler.CompilerMessageCategory;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.externalSystem.util.DisposeAwareProjectChange;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ContentEntry;
import com.intellij.openapi.roots.LanguageLevelProjectExtension;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.ui.AppUIUtil;
import com.intellij.util.messages.Topic;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.settings.GradleSettings;
import java.io.File;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import static com.android.tools.idea.gradle.util.BuildMode.DEFAULT_BUILD_MODE;
import static com.android.tools.idea.gradle.util.BuildMode.SOURCE_GEN;
import static com.android.tools.idea.gradle.util.FilePaths.findParentContentEntry;
import static com.android.tools.idea.gradle.util.FilePaths.pathToIdeaUrl;
import static com.android.tools.idea.gradle.util.Projects.*;
import static com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil.executeProjectChangeAction;
import static com.intellij.openapi.util.io.FileUtil.filesEqual;
import static com.intellij.openapi.util.io.FileUtil.notNullize;
import static com.intellij.openapi.vfs.VfsUtilCore.virtualToIoFile;
import static com.intellij.util.ThreeState.YES;
/**
* After a build is complete, this class will execute the following tasks:
* <ul>
* <li>Notify user that unresolved dependencies were detected in offline mode, and suggest to go <em>online</em></li>
* <li>Refresh Studio's view of the file system (to see generated files)</li>
* <li>Remove any build-related data stored in the project itself (e.g. modules to build, current "build mode", etc.)</li>
* <li>Notify projects that source generation is finished (if applicable)</li>
* </ul>
* Both JPS and the "direct Gradle invocation" build strategies ares supported.
*/
public class PostProjectBuildTasksExecutor {
public static final Topic<GradleBuildListener> GRADLE_BUILD_TOPIC =
new Topic<GradleBuildListener>("Gradle project build", GradleBuildListener.class);
private static final Key<Boolean> UPDATE_JAVA_LANG_LEVEL_AFTER_BUILD = Key.create("android.gradle.project.update.java.lang");
private static final Key<Long> PROJECT_LAST_BUILD_TIMESTAMP_KEY = Key.create("android.gradle.project.last.build.timestamp");
@NotNull private final Project myProject;
@NotNull
public static PostProjectBuildTasksExecutor getInstance(@NotNull Project project) {
return ServiceManager.getService(project, PostProjectBuildTasksExecutor.class);
}
public PostProjectBuildTasksExecutor(@NotNull Project project) {
myProject = project;
}
public void onBuildCompletion(@NotNull CompileContext context) {
Iterator<String> errors = Iterators.emptyIterator();
CompilerMessage[] errorMessages = context.getMessages(CompilerMessageCategory.ERROR);
if (errorMessages.length > 0) {
errors = new CompilerMessageIterator(errorMessages);
}
//noinspection TestOnlyProblems
onBuildCompletion(errors, errorMessages.length);
}
public long getLastBuildTimestamp() {
Long timestamp = myProject.getUserData(PROJECT_LAST_BUILD_TIMESTAMP_KEY);
return timestamp != null ? timestamp : -1L;
}
private static class CompilerMessageIterator extends AbstractIterator<String> {
@NotNull private final CompilerMessage[] myErrors;
private int counter;
CompilerMessageIterator(@NotNull CompilerMessage[] errors) {
myErrors = errors;
}
@Override
@Nullable
protected String computeNext() {
if (counter >= myErrors.length) {
return endOfData();
}
return myErrors[counter++].getMessage();
}
}
private static class MessageIterator extends AbstractIterator<String> {
private final Iterator<Message> myIterator;
MessageIterator(@NotNull Collection<Message> compilerMessages) {
myIterator = compilerMessages.iterator();
}
@Override
@Nullable
protected String computeNext() {
if (!myIterator.hasNext()) {
return endOfData();
}
Message msg = myIterator.next();
return msg != null ? msg.getText() : null;
}
}
public void onBuildCompletion(@NotNull GradleInvocationResult result) {
Iterator<String> errors = Iterators.emptyIterator();
List<Message> errorMessages = result.getCompilerMessages(Message.Kind.ERROR);
if (!errorMessages.isEmpty()) {
errors = new MessageIterator(errorMessages);
}
//noinspection TestOnlyProblems
onBuildCompletion(errors, errorMessages.size());
}
@VisibleForTesting
void onBuildCompletion(Iterator<String> errorMessages, int errorCount) {
if (requiresAndroidModel(myProject)) {
executeProjectChanges(myProject, new Runnable() {
@Override
public void run() {
excludeOutputFolders();
}
});
if (isOfflineBuildModeEnabled(myProject)) {
while (errorMessages.hasNext()) {
String error = errorMessages.next();
if (error != null && unresolvedDependenciesFound(error)) {
notifyUnresolvedDependenciesInOfflineMode();
break;
}
}
}
// Refresh Studio's view of the file system after a compile. This is necessary for Studio to see generated code.
refreshProject();
BuildSettings buildSettings = BuildSettings.getInstance(myProject);
BuildMode buildMode = buildSettings.getBuildMode();
buildSettings.removeAll();
myProject.putUserData(PROJECT_LAST_BUILD_TIMESTAMP_KEY, System.currentTimeMillis());
notifyBuildFinished(buildMode);
syncJavaLangLevel();
// We automatically sync the model if:
// 1. The project build is doing a MAKE, has zero errors and the previous Gradle sync failed. It is likely that if the
// project build is successful, Gradle sync will be successful too.
// 2. If any build.gradle files or setting.gradle file was modified *after* last Gradle sync (we check file timestamps vs the
// timestamp of the last Gradle sync.) We don't perform this check if project build is SOURCE_GEN because, in this case,
// the project build was triggered by a Gradle sync (thus unlikely to have a stale model.) This sync is performed regardless the
// build was successful or not. If isGradleSyncNeeded returns UNSURE, the previous sync may have failed, if this happened
// an automatic sync should have been triggered already. No need to trigger a new one.
if (DEFAULT_BUILD_MODE.equals(buildMode) && lastGradleSyncFailed(myProject) && errorCount == 0 ||
!SOURCE_GEN.equals(buildMode) && GradleSyncState.getInstance(myProject).isSyncNeeded().equals(YES)) {
GradleProjectImporter.getInstance().requestProjectSync(myProject, false /* do not generate sources */, null);
}
}
}
/**
* Even though {@link com.android.tools.idea.gradle.customizer.android.ContentRootModuleCustomizer} already excluded the folders
* "$buildDir/intermediates" and "$buildDir/outputs" we go through the children of "$buildDir" and exclude any non-generated folders
* that may have been created by other plug-ins. We need to be aggressive when excluding folder to prevent over-indexing files, which
* will degrade the IDE's performance.
*/
private void excludeOutputFolders() {
if (myProject.isDisposed()) {
return;
}
ModuleManager moduleManager = ModuleManager.getInstance(myProject);
if (myProject.isDisposed()) {
return;
}
for (Module module : moduleManager.getModules()) {
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet != null && facet.requiresAndroidModel()) {
excludeOutputFolders(facet);
}
}
}
private static void excludeOutputFolders(@NotNull AndroidFacet facet) {
IdeaAndroidProject androidModel = facet.getAndroidModel();
if (androidModel == null) {
return;
}
File buildFolderPath = androidModel.getAndroidProject().getBuildFolder();
if (!buildFolderPath.isDirectory()) {
return;
}
Module module = facet.getModule();
if (module.getProject().isDisposed()) {
return;
}
ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
ModifiableRootModel rootModel = moduleRootManager.getModifiableModel();
try {
ContentEntry[] contentEntries = rootModel.getContentEntries();
ContentEntry parent = findParentContentEntry(buildFolderPath, contentEntries);
if (parent == null) {
rootModel.dispose();
return;
}
File[] outputFolderPaths = notNullize(buildFolderPath.listFiles());
if (outputFolderPaths.length == 0) {
rootModel.dispose();
return;
}
for (File outputFolderPath : outputFolderPaths) {
if (!androidModel.shouldManuallyExclude(outputFolderPath)) {
continue;
}
boolean alreadyExcluded = false;
for (VirtualFile excluded : parent.getExcludeFolderFiles()) {
if (filesEqual(outputFolderPath, virtualToIoFile(excluded))) {
alreadyExcluded = true;
break;
}
}
if (!alreadyExcluded) {
parent.addExcludeFolder(pathToIdeaUrl(outputFolderPath));
}
}
}
finally {
if (!rootModel.isDisposed()) {
rootModel.commit();
}
}
}
private static boolean unresolvedDependenciesFound(@NotNull String errorMessage) {
return errorMessage.contains("Could not resolve all dependencies");
}
private void notifyUnresolvedDependenciesInOfflineMode() {
NotificationHyperlink disableOfflineModeHyperlink = new NotificationHyperlink("disable.gradle.offline.mode", "Disable offline mode") {
@Override
protected void execute(@NotNull Project project) {
GradleSettings.getInstance(myProject).setOfflineWork(false);
}
};
String title = "Unresolved Dependencies";
String text = "Unresolved dependencies detected while building project in offline mode. Please disable offline mode and try again.";
AndroidGradleNotification.getInstance(myProject).showBalloon(title, text, NotificationType.ERROR, disableOfflineModeHyperlink);
}
/**
* Refreshes, asynchronously, the cached view of the project's contents.
*/
private void refreshProject() {
String projectPath = myProject.getBasePath();
if (projectPath != null) {
VirtualFile rootDir = LocalFileSystem.getInstance().findFileByPath(projectPath);
if (rootDir != null && rootDir.isDirectory()) {
rootDir.refresh(true, true);
}
}
}
private void notifyBuildFinished(@Nullable final BuildMode buildMode) {
syncPublisher(new Runnable() {
@Override
public void run() {
myProject.getMessageBus().syncPublisher(GRADLE_BUILD_TOPIC).buildFinished(myProject, buildMode);
}
});
}
private void syncPublisher(@NotNull Runnable publishingTask) {
AppUIUtil.invokeLaterIfProjectAlive(myProject, publishingTask);
}
public void updateJavaLangLevelAfterBuild() {
myProject.putUserData(UPDATE_JAVA_LANG_LEVEL_AFTER_BUILD, true);
}
private void syncJavaLangLevel() {
Boolean updateJavaLangLevel = myProject.getUserData(UPDATE_JAVA_LANG_LEVEL_AFTER_BUILD);
if (updateJavaLangLevel == null || !updateJavaLangLevel.booleanValue()) {
return;
}
myProject.putUserData(UPDATE_JAVA_LANG_LEVEL_AFTER_BUILD, null);
executeProjectChangeAction(true, new DisposeAwareProjectChange(myProject) {
@Override
public void execute() {
if (myProject.isOpen()) {
//noinspection TestOnlyProblems
LanguageLevel langLevel = getMaxJavaLangLevel();
if (langLevel != null) {
LanguageLevelProjectExtension ext = LanguageLevelProjectExtension.getInstance(myProject);
if (langLevel != ext.getLanguageLevel()) {
ext.setLanguageLevel(langLevel);
}
}
}
}
});
}
@VisibleForTesting
@Nullable
LanguageLevel getMaxJavaLangLevel() {
LanguageLevel maxLangLevel = null;
Module[] modules = ModuleManager.getInstance(myProject).getModules();
for (Module module : modules) {
AndroidFacet facet = AndroidFacet.getInstance(module);
if (facet == null) {
continue;
}
IdeaAndroidProject androidModel = facet.getAndroidModel();
if (androidModel != null) {
LanguageLevel langLevel = androidModel.getJavaLanguageLevel();
if (langLevel != null && (maxLangLevel == null || maxLangLevel.compareTo(langLevel) < 0)) {
maxLangLevel = langLevel;
}
}
}
return maxLangLevel;
}
}