| /* |
| * 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; |
| } |
| } |
| }; |
| } |