blob: 4e4429dc8f0d93f3f7ebf99a3c995963e50abb4b [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
*
* 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.ide.eclipse.adt.internal.editors.layout;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.resources.ResourceFile;
import com.android.ide.common.resources.ResourceFolder;
import com.android.ide.eclipse.adt.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener;
import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IResourceEventListener;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager.IResourceListener;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.resources.ResourceType;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResourceDelta;
import org.eclipse.core.runtime.CoreException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* Monitor for file changes that could trigger a layout redraw, or a UI update
*/
public final class LayoutReloadMonitor {
// singleton, enforced by private constructor.
private final static LayoutReloadMonitor sThis = new LayoutReloadMonitor();
/**
* Map of listeners by IProject.
*/
private final Map<IProject, List<ILayoutReloadListener>> mListenerMap =
new HashMap<IProject, List<ILayoutReloadListener>>();
public final static class ChangeFlags {
public boolean code = false;
/** any non-layout resource changes */
public boolean resources = false;
public boolean rClass = false;
public boolean localeList = false;
public boolean manifest = false;
boolean isAllTrue() {
return code && resources && rClass && localeList && manifest;
}
}
/**
* List of projects having received a resource change.
*/
private final Map<IProject, ChangeFlags> mProjectFlags = new HashMap<IProject, ChangeFlags>();
/**
* Classes which implement this interface provide a method to respond to resource changes
* triggering a layout redraw
*/
public interface ILayoutReloadListener {
/**
* Sent when the layout needs to be redrawn
*
* @param flags a {@link ChangeFlags} object indicating what type of resource changed.
* @param libraryModified <code>true</code> if the changeFlags are not for the project
* associated with the listener, but instead correspond to a library.
*/
void reloadLayout(ChangeFlags flags, boolean libraryModified);
}
/**
* Returns the single instance of {@link LayoutReloadMonitor}.
*/
public static LayoutReloadMonitor getMonitor() {
return sThis;
}
private LayoutReloadMonitor() {
// listen to resource changes. Used for non-layout resource (trigger a redraw), or
// any resource folder (trigger a locale list refresh)
ResourceManager.getInstance().addListener(mResourceListener);
// also listen for .class file changed in case the layout has custom view classes.
GlobalProjectMonitor monitor = GlobalProjectMonitor.getMonitor();
monitor.addFileListener(mFileListener,
IResourceDelta.ADDED | IResourceDelta.CHANGED | IResourceDelta.REMOVED);
monitor.addResourceEventListener(mResourceEventListener);
}
/**
* Adds a listener for a given {@link IProject}.
* @param project
* @param listener
*/
public void addListener(IProject project, ILayoutReloadListener listener) {
synchronized (mListenerMap) {
List<ILayoutReloadListener> list = mListenerMap.get(project);
if (list == null) {
list = new ArrayList<ILayoutReloadListener>();
mListenerMap.put(project, list);
}
list.add(listener);
}
}
/**
* Removes a listener for a given {@link IProject}.
*/
public void removeListener(IProject project, ILayoutReloadListener listener) {
synchronized (mListenerMap) {
List<ILayoutReloadListener> list = mListenerMap.get(project);
if (list != null) {
list.remove(listener);
}
}
}
/**
* Removes a listener, no matter which {@link IProject} it was associated with.
*/
public void removeListener(ILayoutReloadListener listener) {
synchronized (mListenerMap) {
for (List<ILayoutReloadListener> list : mListenerMap.values()) {
Iterator<ILayoutReloadListener> it = list.iterator();
while (it.hasNext()) {
ILayoutReloadListener i = it.next();
if (i == listener) {
it.remove();
}
}
}
}
}
/**
* Implementation of the {@link IFileListener} as an internal class so that the methods
* do not appear in the public API of {@link LayoutReloadMonitor}.
*
* This is only to detect code and manifest change. Resource changes (located in res/)
* is done through {@link #mResourceListener}.
*/
private IFileListener mFileListener = new IFileListener() {
/*
* Callback for IFileListener. Called when a file changed.
* This records the changes for each project, but does not notify listeners.
*/
@Override
public void fileChanged(@NonNull IFile file, @NonNull IMarkerDelta[] markerDeltas,
int kind, @Nullable String extension, int flags, boolean isAndroidProject) {
// This listener only cares about .class files and AndroidManifest.xml files
if (!(SdkConstants.EXT_CLASS.equals(extension)
|| SdkConstants.EXT_XML.equals(extension)
&& SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName()))) {
return;
}
// get the file's project
IProject project = file.getProject();
if (isAndroidProject) {
// project is an Android project, it's the one being affected
// directly by its own file change.
processFileChanged(file, project, extension);
} else {
// check the projects depending on it, if they are Android project, update them.
IProject[] referencingProjects = project.getReferencingProjects();
for (IProject p : referencingProjects) {
try {
boolean hasAndroidNature = p.hasNature(AdtConstants.NATURE_DEFAULT);
if (hasAndroidNature) {
// the changed project is a dependency on an Android project,
// update the main project.
processFileChanged(file, p, extension);
}
} catch (CoreException e) {
// do nothing if the nature cannot be queried.
}
}
}
}
/**
* Processes a file change for a given project which may or may not be the file's project.
* @param file the changed file
* @param project the project impacted by the file change.
*/
private void processFileChanged(IFile file, IProject project, String extension) {
// if this project has already been marked as modified, we do nothing.
ChangeFlags changeFlags = mProjectFlags.get(project);
if (changeFlags != null && changeFlags.isAllTrue()) {
return;
}
// here we only care about code change (so change for .class files).
// Resource changes is handled by the IResourceListener.
if (SdkConstants.EXT_CLASS.equals(extension)) {
if (file.getName().matches("R[\\$\\.](.*)")) {
// this is a R change!
if (changeFlags == null) {
changeFlags = new ChangeFlags();
mProjectFlags.put(project, changeFlags);
}
changeFlags.rClass = true;
} else {
// this is a code change!
if (changeFlags == null) {
changeFlags = new ChangeFlags();
mProjectFlags.put(project, changeFlags);
}
changeFlags.code = true;
}
} else if (SdkConstants.FN_ANDROID_MANIFEST_XML.equals(file.getName()) &&
file.getParent().equals(project)) {
// this is a manifest change!
if (changeFlags == null) {
changeFlags = new ChangeFlags();
mProjectFlags.put(project, changeFlags);
}
changeFlags.manifest = true;
}
}
};
/**
* Implementation of the {@link IResourceEventListener} as an internal class so that the methods
* do not appear in the public API of {@link LayoutReloadMonitor}.
*/
private IResourceEventListener mResourceEventListener = new IResourceEventListener() {
/*
* Callback for ResourceMonitor.IResourceEventListener. Called at the beginning of a
* resource change event. This is called once, while fileChanged can be
* called several times.
*
*/
@Override
public void resourceChangeEventStart() {
// nothing to be done here, it all happens in the resourceChangeEventEnd
}
/*
* Callback for ResourceMonitor.IResourceEventListener. Called at the end of a resource
* change event. This is where we notify the listeners.
*/
@Override
public void resourceChangeEventEnd() {
// for each IProject that was changed, we notify all the listeners.
for (Entry<IProject, ChangeFlags> entry : mProjectFlags.entrySet()) {
IProject project = entry.getKey();
// notify the project itself.
notifyForProject(project, entry.getValue(), false);
// check if the project is a library, and if it is search for what other
// project depends on this one (directly or not)
ProjectState state = Sdk.getProjectState(project);
if (state != null && state.isLibrary()) {
Set<ProjectState> mainProjects = Sdk.getMainProjectsFor(project);
for (ProjectState mainProject : mainProjects) {
// always give the changeflag of the modified project.
notifyForProject(mainProject.getProject(), entry.getValue(), true);
}
}
}
// empty the list.
mProjectFlags.clear();
}
/**
* Notifies the listeners for a given project.
* @param project the project for which the listeners must be notified
* @param flags the change flags to pass to the listener
* @param libraryChanged a flag indicating if the change flags are for the give project,
* or if they are for a library dependency.
*/
private void notifyForProject(IProject project, ChangeFlags flags,
boolean libraryChanged) {
synchronized (mListenerMap) {
List<ILayoutReloadListener> listeners = mListenerMap.get(project);
if (listeners != null) {
for (ILayoutReloadListener listener : listeners) {
try {
listener.reloadLayout(flags, libraryChanged);
} catch (Throwable t) {
AdtPlugin.log(t, "Failed to call ILayoutReloadListener.reloadLayout");
}
}
}
}
}
};
/**
* Implementation of the {@link IResourceListener} as an internal class so that the methods
* do not appear in the public API of {@link LayoutReloadMonitor}.
*/
private IResourceListener mResourceListener = new IResourceListener() {
@Override
public void folderChanged(IProject project, ResourceFolder folder, int eventType) {
// if this project has already been marked as modified, we do nothing.
ChangeFlags changeFlags = mProjectFlags.get(project);
if (changeFlags != null && changeFlags.isAllTrue()) {
return;
}
// this means a new resource folder was added or removed, which can impact the
// locale list.
if (changeFlags == null) {
changeFlags = new ChangeFlags();
mProjectFlags.put(project, changeFlags);
}
changeFlags.localeList = true;
}
@Override
public void fileChanged(IProject project, ResourceFile file, int eventType) {
// if this project has already been marked as modified, we do nothing.
ChangeFlags changeFlags = mProjectFlags.get(project);
if (changeFlags != null && changeFlags.isAllTrue()) {
return;
}
// now check that the file is *NOT* a layout file (those automatically trigger a layout
// reload and we don't want to do it twice.)
Collection<ResourceType> resTypes = file.getResourceTypes();
// it's unclear why but there has been cases of resTypes being empty!
if (resTypes.size() > 0) {
// this is a resource change, that may require a layout redraw!
if (changeFlags == null) {
changeFlags = new ChangeFlags();
mProjectFlags.put(project, changeFlags);
}
changeFlags.resources = true;
}
}
};
}