blob: baed458a61a944c64612f3d9848502450f16dd96 [file] [log] [blame]
/*
* Copyright (C) 2015 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.rendering;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceFolderType;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.configurations.ConfigurationListener;
import com.android.tools.idea.gradle.invoker.GradleInvocationResult;
import com.android.tools.idea.gradle.util.ProjectBuilder;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.compiler.CompileContext;
import com.intellij.openapi.components.ProjectComponent;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.ModificationTracker;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.xml.*;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.facet.ResourceFolderManager;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import static com.android.SdkConstants.*;
/**
* The {@linkplain ResourceNotificationManager} provides notifications to editors that
* are displaying Android resources, and need to update themselves when some operation
* in the IDE edits a resource.
* <p/>
* <b>NOTE</b>: Editors should <b>only</b> listen for resource notifications while they
* are actively showing. They should <b>not</b> simply add a listener when created
* and then continue to listen in the background. A core value of the IDE is to offer
* a quick editing experience, and if all the potential editors start listening globally
* for events, this will slow everything down to a crawl.
* <p/>
* Clients should start listening for resource events when the editor is made visible,
* and stop listening when the editor is made invisible. Furthermore, when editors are
* made visible a second time, they can consult a modification stamp to see whether
* anything changed while they were in the background, and if so update themselves only
* if that's the case.
* <p/>
* Also note that
* <ul>
* <li>Resource editing events are not delivered synchronously</li>
* <li>No locks are held when the listener are notified</li>
* <li>All events are delivered on the event dispatch (UI) thread</li>
* <li>Add listener or remove listener can be done from any thread</li>
* </ul>
*/
@SuppressWarnings({"SynchronizeOnThis", "UseOfSystemOutOrSystemErr"})
public class ResourceNotificationManager implements ProjectComponent {
private final Project myProject;
/**
* Module observers: one per observed module in the project, with potentially multiple listeners
*/
private final Map<Module, ModuleEventObserver> myModuleToObserverMap = Maps.newHashMap();
/**
* File observers: one per observed file, with potentially multiple listeners
*/
private final Map<PsiFile, FileEventObserver> myFileToObserverMap = Maps.newHashMap();
/**
* Configuration observers: one per observed configuration, with potentially multiple listeners
*/
private final Map<Configuration, ConfigurationEventObserver> myConfigurationToObserverMap = Maps.newHashMap();
/**
* Project wide observer: a single one will do
*/
private ProjectEventObserver myProjectEventObserver;
/**
* Whether we've already been notified about a change and we'll be firing it shortly
*/
private boolean myPendingNotify;
/**
* Counter for events other than resource repository, configuration or file events. For example,
* this counts project builds.
*/
private long myModificationCount;
/**
* Set of events we've observed since the last notification
*/
private EnumSet<Reason> myEvents = EnumSet.noneOf(Reason.class);
/**
* Do not instantiate directly; this is a {@link ProjectComponent} and its lifecycle is managed by the IDE;
* use {@link #getInstance(Project)} instead
*/
public ResourceNotificationManager(Project project) {
myProject = project;
}
/**
* Returns the {@linkplain ResourceNotificationManager} for the given project
*
* @param project the project to return the notification manager for
* @return a notification manager
*/
@NotNull
public static ResourceNotificationManager getInstance(@NotNull Project project) {
return project.getComponent(ResourceNotificationManager.class);
}
@NotNull
public ResourceVersion getCurrentVersion(@NotNull AndroidFacet facet, @Nullable PsiFile file, @Nullable Configuration configuration) {
AppResourceRepository repository = AppResourceRepository.getAppResources(facet, true);
if (file != null) {
long fileStamp = file.getModificationStamp();
if (configuration != null) {
return new ResourceVersion(repository.getModificationCount(), fileStamp, configuration.getModificationCount(),
configuration.getConfigurationManager().getStateVersion(), myModificationCount);
}
else {
return new ResourceVersion(repository.getModificationCount(), fileStamp, 0L, 0L, myModificationCount);
}
}
else {
return new ResourceVersion(repository.getModificationCount(), 0L, 0L, 0L, myModificationCount);
}
}
/**
* Registers an interest in resources accessible from the given module
*
* @param listener the listener to notify when there is a resource change
* @param facet the facet for the Android module whose resources the listener is interested in
* @param file an optional file to observe for any edits. Note that this is not currently
* limited to this listener; all listeners in the module will be notified when
* there is an edit of this file.
* @param configuration if file is non null, this is an optional configuration
* you can listen for changes in (must be a configuration corresponding to the file)
* @return the current resource modification stamp of the given module
*/
public ResourceVersion addListener(@NotNull ResourceChangeListener listener,
@NotNull AndroidFacet facet,
@Nullable PsiFile file,
@Nullable Configuration configuration) {
synchronized (this) {
Module module = facet.getModule();
ModuleEventObserver moduleEventObserver = myModuleToObserverMap.get(module);
if (moduleEventObserver == null) {
if (myModuleToObserverMap.isEmpty()) {
if (myProjectEventObserver == null) {
myProjectEventObserver = new ProjectEventObserver();
}
myProjectEventObserver.registerListeners();
}
moduleEventObserver = new ModuleEventObserver(facet);
myModuleToObserverMap.put(module, moduleEventObserver);
}
moduleEventObserver.addListener(listener);
if (file != null) {
FileEventObserver fileEventObserver = myFileToObserverMap.get(file);
if (fileEventObserver == null) {
fileEventObserver = new FileEventObserver();
myFileToObserverMap.put(file, fileEventObserver);
}
fileEventObserver.addListener(listener);
if (configuration != null) {
ConfigurationEventObserver configurationEventObserver = myConfigurationToObserverMap.get(configuration);
if (configurationEventObserver == null) {
configurationEventObserver = new ConfigurationEventObserver(configuration);
myConfigurationToObserverMap.put(configuration, configurationEventObserver);
}
configurationEventObserver.addListener(listener);
}
}
else {
assert configuration == null : configuration;
}
return getCurrentVersion(facet, file, configuration);
}
}
/**
* Registers an interest in resources accessible from the given module
*
* @param listener the listener to notify when there is a resource change
* @param facet the facet for the Android module whose resources the listener is interested in
* @param file the file passed in to the corresponding {@link #addListener} call
* @param configuration the configuration passed in to the corresponding {@link #addListener} call
*/
public void removeListener(@NotNull ResourceChangeListener listener,
@NonNull AndroidFacet facet,
@Nullable PsiFile file,
@Nullable Configuration configuration) {
synchronized (this) {
if (file != null) {
if (configuration != null) {
ConfigurationEventObserver configurationEventObserver = myConfigurationToObserverMap.get(configuration);
if (configurationEventObserver != null) {
configurationEventObserver.removeListener(listener);
if (!configurationEventObserver.hasListeners()) {
myConfigurationToObserverMap.remove(configuration);
}
}
}
FileEventObserver fileEventObserver = myFileToObserverMap.get(file);
if (fileEventObserver != null) {
fileEventObserver.removeListener(listener);
if (!fileEventObserver.hasListeners()) {
myFileToObserverMap.remove(file);
}
}
}
else {
assert configuration == null : configuration;
}
Module module = facet.getModule();
ModuleEventObserver moduleEventObserver = myModuleToObserverMap.get(module);
if (moduleEventObserver != null) {
moduleEventObserver.removeListener(listener);
if (!moduleEventObserver.hasListeners()) {
myModuleToObserverMap.remove(module);
if (myModuleToObserverMap.isEmpty() && myProjectEventObserver != null) {
myProjectEventObserver.unregisterListeners();
}
}
}
}
}
private final Object CHANGE_PENDING_LOCK = new Object();
/**
* Something happened. Either schedule a notification or if one is already pending, do nothing.
*/
private void notice(Reason reason) {
myEvents.add(reason);
synchronized (CHANGE_PENDING_LOCK) {
if (myPendingNotify) {
return;
}
myPendingNotify = true;
}
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
synchronized (CHANGE_PENDING_LOCK) {
if (!myPendingNotify) {
return;
}
myPendingNotify = false;
}
// Ensure that the notify happens after all pending Swing Runnables
// have been processed, including any created *after* the initial notice()
// call which scheduled this runnable, since they could for example be
// ResourceFolderRepository#rescan() Runnables, and we want those to finish
// before the final notify. Notice how we clear the pending notify flag
// above though, such that if another event appears between the first
// invoke later and the second, it will schedule another complete notification
// event.
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
EnumSet<Reason> reason = myEvents;
myEvents = EnumSet.noneOf(Reason.class);
notifyListeners(reason);
myEvents.clear();
}
});
}
});
}
private void notifyListeners(@NonNull EnumSet<Reason> reason) {
ApplicationManager.getApplication().assertIsDispatchThread();
for (ModuleEventObserver moduleEventObserver : myModuleToObserverMap.values()) {
// Not every module may have pending changes; each one will check
moduleEventObserver.notifyListeners(reason);
}
}
// ---- Implements Project Component ----
@Override
public void projectOpened() {
}
@Override
public void projectClosed() {
}
@Override
public void initComponent() {
}
@Override
public void disposeComponent() {
}
@NotNull
@Override
public String getComponentName() {
return "ResourceNotificationManager";
}
/**
* A {@linkplain ModuleEventObserver} registers listeners for various module-specific events (such as
* resource folder manager changes) and then notifies {@link #notice(Reason)} when it sees an event
*/
private class ModuleEventObserver implements ModificationTracker, ResourceFolderManager.ResourceFolderListener {
private final AndroidFacet myFacet;
private long myGeneration;
private final List<ResourceChangeListener> myListeners = Lists.newArrayListWithExpectedSize(4);
private ModuleEventObserver(@NotNull AndroidFacet facet) {
myFacet = facet;
myGeneration = AppResourceRepository.getAppResources(facet, true).getModificationCount();
}
@Override
public long getModificationCount() {
return myGeneration;
}
private void addListener(@NotNull ResourceChangeListener listener) {
if (myListeners.isEmpty()) {
registerListeners();
}
myListeners.add(listener);
}
private void removeListener(@NotNull ResourceChangeListener listener) {
myListeners.remove(listener);
if (myListeners.isEmpty()) {
unregisterListeners();
}
}
private void registerListeners() {
if (myFacet.requiresAndroidModel()) {
// Ensure that the app resources have been initialized first, since
// we want it to add its own variant listeners before ours (such that
// when the variant changes, the project resources get notified and updated
// before our own update listener attempts a re-render)
ModuleResourceRepository.getModuleResources(myFacet, true /*createIfNecessary*/);
myFacet.getResourceFolderManager().addListener(this);
}
}
private void unregisterListeners() {
if (myFacet.requiresAndroidModel()) {
myFacet.getResourceFolderManager().removeListener(this);
}
}
private void notifyListeners(@NonNull EnumSet<Reason> reason) {
long generation = myFacet.getAppResources(true).getModificationCount();
if (reason.size() == 1 && reason.contains(Reason.RESOURCE_EDIT) && generation == myGeneration) {
// Notified of an edit in some file that could potentially affect the resources, but
// it didn't cause the modification stamp to increase: ignore. (If there are other reasons,
// such as a variant change, then notify regardless
return;
}
myGeneration = generation;
ApplicationManager.getApplication().assertIsDispatchThread();
List<ResourceChangeListener> listeners;
synchronized (this) {
listeners = Lists.newArrayList(myListeners);
}
for (ResourceChangeListener listener : listeners) {
listener.resourcesChanged(reason);
}
}
private boolean hasListeners() {
return !myListeners.isEmpty();
}
// ---- Implements ResourceFolderManager.ResourceFolderListener ----
@Override
public void resourceFoldersChanged(@NotNull AndroidFacet facet,
@NotNull List<VirtualFile> folders,
@NotNull Collection<VirtualFile> added,
@NotNull Collection<VirtualFile> removed) {
myModificationCount++;
notice(Reason.GRADLE_SYNC);
}
}
private class ProjectEventObserver implements PsiTreeChangeListener, ProjectBuilder.AfterProjectBuildTask {
private boolean myAlreadyAddedBuildListener;
private boolean myIgnoreBuildEvents;
private ProjectEventObserver() {
}
private void registerListeners() {
if (!myAlreadyAddedBuildListener) { // See comment in unregisterListeners
myAlreadyAddedBuildListener = true;
ProjectBuilder.getInstance(myProject).addAfterProjectBuildTask(this);
}
myIgnoreBuildEvents = false;
PsiManager.getInstance(myProject).addPsiTreeChangeListener(this);
}
private void unregisterListeners() {
PsiManager.getInstance(myProject).removePsiTreeChangeListener(this);
// Unfortunately, we can't remove build tasks once they've been added.
// https://youtrack.jetbrains.com/issue/IDEA-139893
// Therefore, we leave the listeners around, and make sure we only add
// them once -- and we also ignore any build events that appear when we're
// not intending to be actively listening
// ProjectBuilder.getInstance(myProject).removeAfterProjectBuildTask(this);
myIgnoreBuildEvents = true;
}
// ---- Implements ProjectBuilder.AfterProjectBuildTask ----
@Override
public void execute(@NotNull GradleInvocationResult result) {
buildFinished();
}
@Override
public boolean execute(CompileContext context) {
buildFinished();
return true;
}
private void buildFinished() {
if (!myIgnoreBuildEvents) {
myModificationCount++;
notice(Reason.PROJECT_BUILD);
}
}
// ---- Implements PsiTreeChangeEvent. These are fired project-wide. ----
@Override
public void beforeChildAddition(@NotNull PsiTreeChangeEvent event) {
}
@Override
public void beforeChildRemoval(@NotNull PsiTreeChangeEvent event) {
}
@Override
public void beforeChildReplacement(@NotNull PsiTreeChangeEvent event) {
}
@Override
public void beforeChildMovement(@NotNull PsiTreeChangeEvent event) {
}
@Override
public void beforeChildrenChange(@NotNull PsiTreeChangeEvent event) {
myIgnoreChildrenChanged = false;
}
@Override
public void beforePropertyChange(@NotNull PsiTreeChangeEvent event) {
}
@Override
public void childAdded(@NotNull PsiTreeChangeEvent event) {
myIgnoreChildrenChanged = true;
if (isIgnorable(event)) {
return;
}
if (isRelevantFile(event)) {
PsiElement child = event.getChild();
PsiElement parent = event.getParent();
if (child instanceof XmlAttribute && parent instanceof XmlTag) {
// Typing in a new attribute. Don't need to do any rendering until there
// is an actual value
if (((XmlAttribute)child).getValueElement() == null) {
return;
}
}
else if (parent instanceof XmlAttribute && child instanceof XmlAttributeValue) {
XmlAttributeValue attributeValue = (XmlAttributeValue)child;
if (attributeValue.getValue() == null || attributeValue.getValue().isEmpty()) {
// Just added a new blank attribute; nothing to render yet
return;
}
}
else if (parent instanceof XmlAttributeValue && child instanceof XmlToken && event.getOldChild() == null) {
// Just added attribute value
String text = child.getText();
// See if this is an attribute that takes a resource!
if (text.startsWith(PREFIX_RESOURCE_REF) && !text.startsWith(PREFIX_BINDING_EXPR)) {
if (text.equals(PREFIX_RESOURCE_REF) || text.equals(ANDROID_PREFIX)) {
// Using code completion to insert resource reference; not yet done
return;
}
ResourceUrl url = ResourceUrl.parse(text);
if (url != null && url.name.isEmpty()) {
// Using code completion to insert resource reference; not yet done
return;
}
}
}
notice(Reason.EDIT);
}
else {
notice(Reason.RESOURCE_EDIT);
}
}
@Override
public void childRemoved(@NotNull PsiTreeChangeEvent event) {
myIgnoreChildrenChanged = true;
if (isIgnorable(event)) {
return;
}
if (isRelevantFile(event)) {
PsiElement child = event.getChild();
PsiElement parent = event.getParent();
if (parent instanceof XmlAttribute && child instanceof XmlToken) {
// Typing in attribute name. Don't need to do any rendering until there
// is an actual value
XmlAttributeValue valueElement = ((XmlAttribute)parent).getValueElement();
if (valueElement == null || valueElement.getValue() == null || valueElement.getValue().isEmpty()) {
return;
}
}
notice(Reason.EDIT);
} else {
notice(Reason.RESOURCE_EDIT);
}
}
@Override
public void childReplaced(@NotNull PsiTreeChangeEvent event) {
myIgnoreChildrenChanged = true;
if (isIgnorable(event)) {
return;
}
if (isRelevantFile(event)) {
PsiElement child = event.getChild();
PsiElement parent = event.getParent();
if (parent instanceof XmlAttribute && child instanceof XmlToken) {
// Typing in attribute name. Don't need to do any rendering until there
// is an actual value
XmlAttributeValue valueElement = ((XmlAttribute)parent).getValueElement();
if (valueElement == null || valueElement.getValue() == null || valueElement.getValue().isEmpty()) {
return;
}
}
else if (parent instanceof XmlAttributeValue && child instanceof XmlToken && event.getOldChild() != null) {
String newText = child.getText();
String prevText = event.getOldChild().getText();
// See if user is working on an incomplete URL, and is still not complete, e.g. typing in @string/foo manually
if (newText.startsWith(PREFIX_RESOURCE_REF) && !newText.startsWith(PREFIX_BINDING_EXPR)) {
ResourceUrl prevUrl = ResourceUrl.parse(prevText);
ResourceUrl newUrl = ResourceUrl.parse(newText);
if (prevUrl != null && prevUrl.name.isEmpty()) {
prevUrl = null;
}
if (newUrl != null && newUrl.name.isEmpty()) {
newUrl = null;
}
if (prevUrl == null && newUrl == null) {
return;
}
}
}
notice(Reason.EDIT);
}
else {
notice(Reason.RESOURCE_EDIT);
}
}
@Override
public void childMoved(@NotNull PsiTreeChangeEvent event) {
check(event);
}
boolean myIgnoreChildrenChanged;
@Override
public void childrenChanged(@NotNull PsiTreeChangeEvent event) {
if (myIgnoreChildrenChanged) {
return;
}
check(event);
}
@Override
public void propertyChanged(@NotNull PsiTreeChangeEvent event) {
}
private boolean isRelevantFile(PsiTreeChangeEvent event) {
if (!myFileToObserverMap.isEmpty()) {
final PsiFile file = event.getFile();
if (file != null && myFileToObserverMap.containsKey(file)) {
return true;
}
}
return false;
}
private boolean isIgnorable(PsiTreeChangeEvent event) {
// We can ignore edits in whitespace, and in XML error nodes, and in comments
// (Note that editing text in an attribute value, including whitespace characters,
// is not a PsiWhiteSpace element; it's an XmlToken of token type XML_ATTRIBUTE_VALUE_TOKEN
PsiElement child = event.getChild();
PsiElement parent = event.getParent();
if (child instanceof PsiErrorElement ||
child instanceof XmlComment ||
parent instanceof XmlComment) {
return true;
}
if ((child instanceof PsiWhiteSpace || child instanceof XmlText || parent instanceof XmlText)
&& ResourceHelper.getFolderType(event.getFile()) != ResourceFolderType.VALUES) {
// Editing text or whitespace has no effect outside of values files
return true;
}
return false;
}
private void check(PsiTreeChangeEvent event) {
if (isIgnorable(event)) {
return;
}
if (isRelevantFile(event)) {
final PsiFile file = event.getFile();
if (file != null) {
notice(Reason.EDIT);
return;
}
}
notice(Reason.RESOURCE_EDIT);
}
}
private static class FileEventObserver {
private List<ResourceChangeListener> myListeners = Lists.newArrayListWithExpectedSize(2);
public FileEventObserver() {
}
private void addListener(@NotNull ResourceChangeListener listener) {
if (myListeners.isEmpty()) {
registerListeners();
}
myListeners.add(listener);
}
private void removeListener(@NotNull ResourceChangeListener listener) {
myListeners.remove(listener);
if (myListeners.isEmpty()) {
unregisterListeners();
}
}
private void registerListeners() {
}
private void unregisterListeners() {
}
private boolean hasListeners() {
return !myListeners.isEmpty();
}
}
private class ConfigurationEventObserver implements ConfigurationListener {
private final Configuration myConfiguration;
private List<ResourceChangeListener> myListeners = Lists.newArrayListWithExpectedSize(2);
public ConfigurationEventObserver(Configuration configuration) {
myConfiguration = configuration;
}
private void addListener(@NotNull ResourceChangeListener listener) {
if (myListeners.isEmpty()) {
registerListeners();
}
myListeners.add(listener);
}
private void removeListener(@NotNull ResourceChangeListener listener) {
myListeners.remove(listener);
if (myListeners.isEmpty()) {
unregisterListeners();
}
}
private boolean hasListeners() {
return !myListeners.isEmpty();
}
private void registerListeners() {
myConfiguration.addListener(this);
}
private void unregisterListeners() {
myConfiguration.removeListener(this);
}
// ---- Implements ConfigurationListener ----
@Override
public boolean changed(int flags) {
if ((flags & MASK_RENDERING) != 0) {
notice(Reason.CONFIGURATION_CHANGED);
}
return true;
}
}
/*
final MessageBusConnection connection = project.getMessageBus().connect(project);
connection.subscribe(ProjectTopics.PROJECT_ROOTS, new MyAndroidPlatformListener(project));
private class MyAndroidPlatformListener extends ModuleRootAdapter {
private final Map<Module, Sdk> myModule2Sdk = new HashMap<Module, Sdk>();
private final Project myProject;
private MyAndroidPlatformListener(@NotNull Project project) {
myProject = project;
updateMap();
}
@Override
public void rootsChanged(ModuleRootEvent event) {
final PsiFile file = myToolWindowForm.getFile();
if (file != null) {
final Module module = ModuleUtilCore.findModuleForPsiElement(file);
if (module != null) {
final Sdk prevSdk = myModule2Sdk.get(module);
final Sdk newSdk = ModuleRootManager.getInstance(module).getSdk();
if (newSdk != null &&
(newSdk.getSdkType() instanceof AndroidSdkType || (prevSdk != null && prevSdk.getSdkType() instanceof AndroidSdkType)) &&
!newSdk.equals(prevSdk)) {
notice(Reason.SDK_CHANGED);
}
}
}
updateMap();
}
private void updateMap() {
myModule2Sdk.clear();
for (Module module : ModuleManager.getInstance(myProject).getModules()) {
myModule2Sdk.put(module, ModuleRootManager.getInstance(module).getSdk());
}
}
}
*/
/**
* Interface which should be implemented by clients interested in resource edits and events that affect resources
*/
public interface ResourceChangeListener {
/**
* One or more resources have changed
*
* @param reason the set of reasons that the resources have changed since the last notification
*/
void resourcesChanged(@NotNull Set<Reason> reason);
}
/**
* A version timestamp of the resources. This snapshot version is immutable, so you can hold on
* to it and compare it with your most recent version.
*/
public static class ResourceVersion {
private final long myResourceGeneration;
private final long myFileGeneration;
private final long myConfigurationGeneration;
private final long myProjectConfigurationGeneration;
private final long myOtherGeneration;
private ResourceVersion(long resourceGeneration,
long fileGeneration,
long configurationGeneration,
long projectConfigurationGeneration,
long otherGeneration) {
myResourceGeneration = resourceGeneration;
myFileGeneration = fileGeneration;
myConfigurationGeneration = configurationGeneration;
myProjectConfigurationGeneration = projectConfigurationGeneration;
myOtherGeneration = otherGeneration;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ResourceVersion version = (ResourceVersion)o;
if (myResourceGeneration != version.myResourceGeneration) return false;
if (myFileGeneration != version.myFileGeneration) return false;
if (myConfigurationGeneration != version.myConfigurationGeneration) return false;
if (myProjectConfigurationGeneration != version.myProjectConfigurationGeneration) return false;
if (myOtherGeneration != version.myOtherGeneration) return false;
return true;
}
@Override
public int hashCode() {
int result = (int)(myResourceGeneration ^ (myResourceGeneration >>> 32));
result = 31 * result + (int)(myFileGeneration ^ (myFileGeneration >>> 32));
result = 31 * result + (int)(myConfigurationGeneration ^ (myConfigurationGeneration >>> 32));
result = 31 * result + (int)(myProjectConfigurationGeneration ^ (myProjectConfigurationGeneration >>> 32));
result = 31 * result + (int)(myOtherGeneration ^ (myOtherGeneration >>> 32));
return result;
}
@Override
public String toString() {
return "ResourceVersion{" +
"resource=" + myResourceGeneration +
", file=" + myFileGeneration +
", configuration=" + myConfigurationGeneration +
", projectConfiguration=" + myProjectConfigurationGeneration +
", other=" + myOtherGeneration +
'}';
}
}
/**
* The reason the resources have changed
*/
public enum Reason {
/**
* An edit which affects the resource repository was performed (e.g. changing the value of a string
* is a resource edit, but editing the layout parameters of a widget in a layout file is not)
*/
RESOURCE_EDIT,
/**
* Edit of a file that is being observed (if you're for example watching a menu file, this will include
* edits in whitespace etc
*/
EDIT,
/**
* The configuration changed (for example, the locale may have changed)
*/
CONFIGURATION_CHANGED,
/**
* The module SDK changed
*/
SDK_CHANGED,
/**
* The active variant changed, which affects available resource sets and values
*/
VARIANT_CHANGED,
/**
* A sync happened. This can change dynamically generated resources for example.
*/
GRADLE_SYNC,
/**
* Project build. Not a direct resource edit, but for example when a custom view
* is compiled it can affect how a resource like layouts should be rendered
*/
PROJECT_BUILD
}
}