/*
* Copyright (C) 2014 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;
import com.android.SdkConstants;
import com.android.tools.idea.gradle.project.GradleSyncListener;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.android.tools.idea.gradle.variant.view.BuildVariantView;
import com.android.tools.idea.startup.AndroidStudioSpecificInitializer;
import com.android.tools.idea.stats.StatsKeys;
import com.android.tools.idea.stats.StatsTimeCollector;
import com.android.tools.lint.detector.api.LintUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Lists;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.ExtensionPoint;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.options.ConfigurableEP;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.AppUIUtil;
import com.intellij.ui.EditorNotifications;
import com.intellij.util.ThreeState;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.messages.Topic;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.util.List;
public class GradleSyncState {
private static final Logger LOG = Logger.getInstance(GradleSyncState.class);
private static final List<String> PROJECT_PREFERENCES_TO_REMOVE = Lists.newArrayList(
"org.intellij.lang.xpath.xslt.associations.impl.FileAssociationsConfigurable", "com.intellij.uiDesigner.GuiDesignerConfigurable",
"org.jetbrains.plugins.groovy.gant.GantConfigurable", "org.jetbrains.plugins.groovy.compiler.GroovyCompilerConfigurable",
"org.jetbrains.android.compiler.AndroidDexCompilerSettingsConfigurable", "org.jetbrains.idea.maven.utils.MavenSettings",
"com.intellij.compiler.options.CompilerConfigurable"
);
public static final Topic<GradleSyncListener> GRADLE_SYNC_TOPIC =
new Topic<GradleSyncListener>("Project sync with Gradle", GradleSyncListener.class);
private static final Key<Long> PROJECT_LAST_SYNC_TIMESTAMP_KEY = Key.create("android.gradle.project.last.sync.timestamp");
@NotNull private final Project myProject;
@NotNull private final MessageBus myMessageBus;
private volatile boolean mySyncInProgress;
@NotNull
public static GradleSyncState getInstance(@NotNull Project project) {
return ServiceManager.getService(project, GradleSyncState.class);
}
public GradleSyncState(@NotNull Project project, @NotNull MessageBus messageBus) {
myProject = project;
myMessageBus = messageBus;
}
public void syncSkipped(long lastSyncTimestamp) {
cleanUpProjectPreferences();
setLastGradleSyncTimestamp(lastSyncTimestamp);
syncPublisher(new Runnable() {
@Override
public void run() {
myMessageBus.syncPublisher(GRADLE_SYNC_TOPIC).syncSkipped(myProject);
}
});
}
public void syncStarted(boolean notifyUser) {
cleanUpProjectPreferences();
StatsTimeCollector.start(StatsKeys.GRADLE_SYNC_PROJECT_TIME_MS);
mySyncInProgress = true;
if (notifyUser) {
notifyUser();
}
syncPublisher(new Runnable() {
@Override
public void run() {
myMessageBus.syncPublisher(GRADLE_SYNC_TOPIC).syncStarted(myProject);
}
});
}
public void syncFailed(@NotNull final String message) {
syncFinished();
syncPublisher(new Runnable() {
@Override
public void run() {
myMessageBus.syncPublisher(GRADLE_SYNC_TOPIC).syncFailed(myProject, message);
}
});
}
public void syncEnded() {
// Temporary: Clear resourcePrefix flag in case it was set to false when working with
// an older model. TODO: Remove this when we no longer support models older than 0.10.
//noinspection AssignmentToStaticFieldFromInstanceMethod
LintUtils.sTryPrefixLookup = true;
syncFinished();
syncPublisher(new Runnable() {
@Override
public void run() {
myMessageBus.syncPublisher(GRADLE_SYNC_TOPIC).syncSucceeded(myProject);
}
});
}
private void syncFinished() {
mySyncInProgress = false;
setLastGradleSyncTimestamp(System.currentTimeMillis());
StatsTimeCollector.stop(StatsKeys.GRADLE_SYNC_PROJECT_TIME_MS);
notifyUser();
}
private void syncPublisher(@NotNull Runnable publishingTask) {
AppUIUtil.invokeLaterIfProjectAlive(myProject, publishingTask);
}
public void notifyUser() {
AppUIUtil.invokeLaterIfProjectAlive(myProject, new Runnable() {
@Override
public void run() {
EditorNotifications notifications = EditorNotifications.getInstance(myProject);
VirtualFile[] files = FileEditorManager.getInstance(myProject).getOpenFiles();
for (VirtualFile file : files) {
try {
notifications.updateNotifications(file);
}
catch (Throwable e) {
String filePath = FileUtil.toSystemDependentName(file.getPath());
String msg = String.format("Failed to update editor notifications for file '%1$s'", filePath);
LOG.info(msg, e);
}
}
notifications.updateAllNotifications();
BuildVariantView.getInstance(myProject).updateContents();
}
});
}
public boolean isSyncInProgress() {
return mySyncInProgress;
}
private void setLastGradleSyncTimestamp(long timestamp) {
myProject.putUserData(PROJECT_LAST_SYNC_TIMESTAMP_KEY, timestamp);
}
public long getLastGradleSyncTimestamp() {
Long timestamp = myProject.getUserData(PROJECT_LAST_SYNC_TIMESTAMP_KEY);
return timestamp != null ? timestamp.longValue() : -1L;
}
/**
* Indicates whether a project sync with Gradle is needed. A Gradle sync is usually needed when a build.gradle or settings.gradle file has
* been updated <b>after</b> the last project sync was performed.
*
* @return {@code YES} if a sync with Gradle is needed, {@code FALSE} otherwise, or {@code UNSURE} If the timestamp of the last Gradle
* sync cannot be found.
*/
@NotNull
public ThreeState isSyncNeeded() {
long lastSync = getLastGradleSyncTimestamp();
if (lastSync < 0) {
// Previous sync may have failed. We don't know if a sync is needed or not. Let client code decide.
return ThreeState.UNSURE;
}
return isSyncNeeded(lastSync) ? ThreeState.YES : ThreeState.NO;
}
/**
* Indicates whether a project sync with Gradle is needed if changes to build.gradle or settings.gradle files were made after the given
* time.
*
* @param referenceTimeInMillis the given time, in milliseconds.
* @return {@code true} if a sync with Gradle is needed, {@code false} otherwise.
* @throws AssertionError if the given time is less than or equal to zero.
*/
public boolean isSyncNeeded(long referenceTimeInMillis) {
assert referenceTimeInMillis > 0;
if (mySyncInProgress) {
return false;
}
FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
File settingsFilePath = new File(myProject.getBasePath(), SdkConstants.FN_SETTINGS_GRADLE);
if (settingsFilePath.exists()) {
VirtualFile settingsFile = VfsUtil.findFileByIoFile(settingsFilePath, true);
if (fileDocumentManager.isFileModified(settingsFile)) {
return true;
}
if (settingsFilePath.lastModified() > referenceTimeInMillis) {
return true;
}
}
ModuleManager moduleManager = ModuleManager.getInstance(myProject);
for (Module module : moduleManager.getModules()) {
VirtualFile buildFile = GradleUtil.getGradleBuildFile(module);
if (buildFile != null) {
if (fileDocumentManager.isFileModified(buildFile)) {
return true;
}
File buildFilePath = VfsUtilCore.virtualToIoFile(buildFile);
if (buildFilePath.lastModified() > referenceTimeInMillis) {
return true;
}
}
}
return false;
}
private void cleanUpProjectPreferences() {
if (!AndroidStudioSpecificInitializer.isAndroidStudio()) {
return;
}
try {
ExtensionPoint<ConfigurableEP<Configurable>>
projectConfigurable = Extensions.getArea(myProject).getExtensionPoint(Configurable.PROJECT_CONFIGURABLE);
GradleUtil.cleanUpPreferences(projectConfigurable, PROJECT_PREFERENCES_TO_REMOVE);
}
catch (Throwable e) {
String msg = String.format("Failed to clean up preferences for project '%1$s'", myProject.getName());
LOG.info(msg, e);
}
}
@VisibleForTesting
public void resetTimestamp() {
setLastGradleSyncTimestamp(-1L);
}
}