| /* |
| * 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.tools.idea.gradle.project.GradleSyncListener; |
| import com.android.tools.idea.gradle.variant.view.BuildVariantView; |
| import com.android.tools.idea.startup.AndroidStudioSpecificInitializer; |
| import com.android.tools.idea.stats.UsageTracker; |
| import com.android.tools.lint.detector.api.LintUtils; |
| import com.google.common.collect.Lists; |
| import com.intellij.notification.NotificationGroup; |
| 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.ui.MessageType; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.vfs.VirtualFile; |
| import com.intellij.ui.EditorNotifications; |
| import com.intellij.util.ThreeState; |
| import com.intellij.util.messages.MessageBus; |
| import com.intellij.util.messages.Topic; |
| import net.jcip.annotations.GuardedBy; |
| import org.jetbrains.annotations.NotNull; |
| |
| import java.io.File; |
| import java.util.List; |
| |
| import static com.android.SdkConstants.FN_SETTINGS_GRADLE; |
| import static com.android.tools.idea.gradle.util.GradleUtil.cleanUpPreferences; |
| import static com.android.tools.idea.gradle.util.GradleUtil.getGradleBuildFile; |
| import static com.android.tools.idea.gradle.util.Projects.getBaseDirPath; |
| import static com.intellij.openapi.options.Configurable.PROJECT_CONFIGURABLE; |
| import static com.intellij.openapi.ui.MessageType.ERROR; |
| import static com.intellij.openapi.ui.MessageType.INFO; |
| import static com.intellij.openapi.util.io.FileUtil.toSystemDependentName; |
| import static com.intellij.openapi.util.text.StringUtil.isNotEmpty; |
| import static com.intellij.openapi.vfs.VfsUtil.findFileByIoFile; |
| import static com.intellij.openapi.vfs.VfsUtilCore.virtualToIoFile; |
| import static com.intellij.ui.AppUIUtil.invokeLaterIfProjectAlive; |
| |
| public class GradleSyncState { |
| private static final Logger LOG = Logger.getInstance(GradleSyncState.class); |
| private static final NotificationGroup LOGGING_NOTIFICATION = NotificationGroup.logOnlyGroup("Gradle sync"); |
| |
| 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 final Object myLock = new Object(); |
| |
| @GuardedBy("myLock") |
| private boolean mySyncNotificationsEnabled; |
| |
| @GuardedBy("myLock") |
| private 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 boolean areSyncNotificationsEnabled() { |
| synchronized (myLock) { |
| return mySyncNotificationsEnabled; |
| } |
| } |
| |
| public void syncSkipped(long lastSyncTimestamp) { |
| LOG.info(String.format("Skipped sync with Gradle for project '%1$s'. Data model(s) loaded from cache.", myProject.getName())); |
| |
| cleanUpProjectPreferences(); |
| setLastGradleSyncTimestamp(lastSyncTimestamp); |
| syncPublisher(new Runnable() { |
| @Override |
| public void run() { |
| myMessageBus.syncPublisher(GRADLE_SYNC_TOPIC).syncSkipped(myProject); |
| } |
| }); |
| |
| enableNotifications(); |
| UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_GRADLE, UsageTracker.ACTION_GRADLE_SYNC_SKIPPED, null, null); |
| } |
| |
| public void syncStarted(boolean notifyUser) { |
| LOG.info(String.format("Started sync with Gradle for project '%1$s'.", myProject.getName())); |
| |
| addInfoToEventLog("Gradle sync started"); |
| |
| cleanUpProjectPreferences(); |
| synchronized (myLock) { |
| mySyncInProgress = true; |
| } |
| if (notifyUser) { |
| notifyUser(); |
| } |
| syncPublisher(new Runnable() { |
| @Override |
| public void run() { |
| myMessageBus.syncPublisher(GRADLE_SYNC_TOPIC).syncStarted(myProject); |
| } |
| }); |
| |
| UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_GRADLE, UsageTracker.ACTION_GRADLE_SYNC_STARTED, null, null); |
| } |
| |
| public void syncFailed(@NotNull final String message) { |
| LOG.info(String.format("Sync with Gradle for project '%1$s' failed: %2$s", myProject.getName(), message)); |
| |
| String logMsg = "Gradle sync failed"; |
| if (isNotEmpty(message)) { |
| logMsg += String.format(": %1$s", message); |
| } |
| addToEventLog(logMsg, ERROR); |
| |
| syncFinished(); |
| syncPublisher(new Runnable() { |
| @Override |
| public void run() { |
| myMessageBus.syncPublisher(GRADLE_SYNC_TOPIC).syncFailed(myProject, message); |
| } |
| }); |
| |
| UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_GRADLE, UsageTracker.ACTION_GRADLE_SYNC_FAILED, null, null); |
| } |
| |
| public void syncEnded() { |
| LOG.info(String.format("Sync with Gradle successful for project '%1$s'.", myProject.getName())); |
| |
| addInfoToEventLog("Gradle sync completed"); |
| |
| // 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); |
| } |
| }); |
| |
| UsageTracker.getInstance().trackEvent(UsageTracker.CATEGORY_GRADLE, UsageTracker.ACTION_GRADLE_SYNC_ENDED, null, null); |
| } |
| |
| |
| private void addInfoToEventLog(@NotNull String message) { |
| addToEventLog(message, INFO); |
| } |
| |
| private void addToEventLog(@NotNull String message, @NotNull MessageType type) { |
| LOGGING_NOTIFICATION.createNotification(message, type).notify(myProject); |
| } |
| |
| private void syncFinished() { |
| synchronized (myLock) { |
| mySyncInProgress = false; |
| } |
| setLastGradleSyncTimestamp(System.currentTimeMillis()); |
| enableNotifications(); |
| notifyUser(); |
| } |
| |
| private void syncPublisher(@NotNull Runnable publishingTask) { |
| invokeLaterIfProjectAlive(myProject, publishingTask); |
| } |
| |
| private void enableNotifications() { |
| synchronized (myLock) { |
| mySyncNotificationsEnabled = true; |
| } |
| } |
| |
| public void notifyUser() { |
| 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 = toSystemDependentName(file.getPath()); |
| String msg = String.format("Failed to update editor notifications for file '%1$s'", filePath); |
| LOG.info(msg, e); |
| } |
| } |
| |
| BuildVariantView.getInstance(myProject).updateContents(); |
| } |
| }); |
| } |
| |
| public boolean isSyncInProgress() { |
| synchronized (myLock) { |
| 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. |
| */ |
| private boolean isSyncNeeded(long referenceTimeInMillis) { |
| assert referenceTimeInMillis > 0; |
| if (isSyncInProgress()) { |
| return false; |
| } |
| |
| FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance(); |
| File settingsFilePath = new File(getBaseDirPath(myProject), FN_SETTINGS_GRADLE); |
| if (settingsFilePath.exists()) { |
| VirtualFile settingsFile = findFileByIoFile(settingsFilePath, true); |
| if (settingsFile != null && fileDocumentManager.isFileModified(settingsFile)) { |
| return true; |
| } |
| if (settingsFilePath.lastModified() > referenceTimeInMillis) { |
| return true; |
| } |
| } |
| |
| ModuleManager moduleManager = ModuleManager.getInstance(myProject); |
| for (Module module : moduleManager.getModules()) { |
| VirtualFile buildFile = getGradleBuildFile(module); |
| if (buildFile != null) { |
| if (fileDocumentManager.isFileModified(buildFile)) { |
| return true; |
| } |
| |
| File buildFilePath = 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(PROJECT_CONFIGURABLE); |
| |
| 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); |
| } |
| } |
| } |