diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java | 375 |
1 files changed, 375 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java new file mode 100644 index 000000000..4e4429dc8 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/LayoutReloadMonitor.java @@ -0,0 +1,375 @@ +/* + * 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; + } + } + }; +} |