aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java621
1 files changed, 621 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java
new file mode 100644
index 000000000..6df6929a7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/descriptors/CustomViewDescriptorService.java
@@ -0,0 +1,621 @@
+/*
+ * 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.descriptors;
+
+import static com.android.SdkConstants.ANDROID_NS_NAME_PREFIX;
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.AUTO_URI;
+import static com.android.SdkConstants.CLASS_VIEWGROUP;
+import static com.android.SdkConstants.URI_PREFIX;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.resources.ResourceFile;
+import com.android.ide.common.resources.ResourceItem;
+import com.android.ide.common.resources.platform.AttributeInfo;
+import com.android.ide.common.resources.platform.AttrsXmlParser;
+import com.android.ide.common.resources.platform.ViewClassInfo;
+import com.android.ide.common.resources.platform.ViewClassInfo.LayoutParamsInfo;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
+import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
+import com.android.ide.eclipse.adt.internal.editors.manifest.ManifestInfo;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.resources.ResourceType;
+import com.android.sdklib.IAndroidTarget;
+import com.google.common.collect.Maps;
+import com.google.common.collect.ObjectArrays;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.IClassFile;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IType;
+import org.eclipse.jdt.core.ITypeHierarchy;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.swt.graphics.Image;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Service responsible for creating/managing {@link ViewElementDescriptor} objects for custom
+ * View classes per project.
+ * <p/>
+ * The service provides an on-demand monitoring of custom classes to check for changes. Monitoring
+ * starts once a request for an {@link ViewElementDescriptor} object has been done for a specific
+ * class.
+ * <p/>
+ * The monitoring will notify a listener of any changes in the class triggering a change in its
+ * associated {@link ViewElementDescriptor} object.
+ * <p/>
+ * If the custom class does not exist, no monitoring is put in place to avoid having to listen
+ * to all class changes in the projects.
+ */
+public final class CustomViewDescriptorService {
+
+ private static CustomViewDescriptorService sThis = new CustomViewDescriptorService();
+
+ /**
+ * Map where keys are the project, and values are another map containing all the known
+ * custom View class for this project. The custom View class are stored in a map
+ * where the keys are the fully qualified class name, and the values are their associated
+ * {@link ViewElementDescriptor}.
+ */
+ private HashMap<IProject, HashMap<String, ViewElementDescriptor>> mCustomDescriptorMap =
+ new HashMap<IProject, HashMap<String, ViewElementDescriptor>>();
+
+ /**
+ * TODO will be used to update the ViewElementDescriptor of the custom view when it
+ * is modified (either the class itself or its attributes.xml)
+ */
+ @SuppressWarnings("unused")
+ private ICustomViewDescriptorListener mListener;
+
+ /**
+ * Classes which implements this interface provide a method that deal with modifications
+ * in custom View class triggering a change in its associated {@link ViewClassInfo} object.
+ */
+ public interface ICustomViewDescriptorListener {
+ /**
+ * Sent when a custom View class has changed and
+ * its {@link ViewElementDescriptor} was modified.
+ *
+ * @param project the project containing the class.
+ * @param className the fully qualified class name.
+ * @param descriptor the updated ElementDescriptor.
+ */
+ public void updatedClassInfo(IProject project,
+ String className,
+ ViewElementDescriptor descriptor);
+ }
+
+ /**
+ * Returns the singleton instance of {@link CustomViewDescriptorService}.
+ */
+ public static CustomViewDescriptorService getInstance() {
+ return sThis;
+ }
+
+ /**
+ * Sets the listener receiving custom View class modification notifications.
+ * @param listener the listener to receive the notifications.
+ *
+ * TODO will be used to update the ViewElementDescriptor of the custom view when it
+ * is modified (either the class itself or its attributes.xml)
+ */
+ public void setListener(ICustomViewDescriptorListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Returns the {@link ViewElementDescriptor} for a particular project/class when the
+ * fully qualified class name actually matches a class from the given project.
+ * <p/>
+ * Custom descriptors are created as needed.
+ * <p/>
+ * If it is the first time the {@link ViewElementDescriptor} is requested, the method
+ * will check that the specified class is in fact a custom View class. Once this is
+ * established, a monitoring for that particular class is initiated. Any change will
+ * trigger a notification to the {@link ICustomViewDescriptorListener}.
+ *
+ * @param project the project containing the class.
+ * @param fqcn the fully qualified name of the class.
+ * @return a {@link ViewElementDescriptor} or <code>null</code> if the class was not
+ * a custom View class.
+ */
+ public ViewElementDescriptor getDescriptor(IProject project, String fqcn) {
+ // look in the map first
+ synchronized (mCustomDescriptorMap) {
+ HashMap<String, ViewElementDescriptor> map = mCustomDescriptorMap.get(project);
+
+ if (map != null) {
+ ViewElementDescriptor descriptor = map.get(fqcn);
+ if (descriptor != null) {
+ return descriptor;
+ }
+ }
+
+ // if we step here, it looks like we haven't created it yet.
+ // First lets check this is in fact a valid type in the project
+
+ try {
+ // We expect the project to be both opened and of java type (since it's an android
+ // project), so we can create a IJavaProject object from our IProject.
+ IJavaProject javaProject = JavaCore.create(project);
+
+ // replace $ by . in the class name
+ String javaClassName = fqcn.replaceAll("\\$", "\\."); //$NON-NLS-1$ //$NON-NLS-2$
+
+ // look for the IType object for this class
+ IType type = javaProject.findType(javaClassName);
+ if (type != null && type.exists()) {
+ // the type exists. Let's get the parent class and its ViewClassInfo.
+
+ // get the type hierarchy
+ ITypeHierarchy hierarchy = type.newSupertypeHierarchy(
+ new NullProgressMonitor());
+
+ ViewElementDescriptor parentDescriptor = createViewDescriptor(
+ hierarchy.getSuperclass(type), project, hierarchy);
+
+ if (parentDescriptor != null) {
+ // we have a valid parent, lets create a new ViewElementDescriptor.
+ List<AttributeDescriptor> attrList = new ArrayList<AttributeDescriptor>();
+ List<AttributeDescriptor> paramList = new ArrayList<AttributeDescriptor>();
+ Map<ResourceFile, Long> files = findCustomDescriptors(project, type,
+ attrList, paramList);
+
+ AttributeDescriptor[] attributes =
+ getAttributeDescriptor(type, parentDescriptor);
+ if (!attrList.isEmpty()) {
+ attributes = join(attrList, attributes);
+ }
+ AttributeDescriptor[] layoutAttributes =
+ getLayoutAttributeDescriptors(type, parentDescriptor);
+ if (!paramList.isEmpty()) {
+ layoutAttributes = join(paramList, layoutAttributes);
+ }
+ String name = DescriptorsUtils.getBasename(fqcn);
+ ViewElementDescriptor descriptor = new CustomViewDescriptor(name, fqcn,
+ attributes,
+ layoutAttributes,
+ parentDescriptor.getChildren(),
+ project, files);
+ descriptor.setSuperClass(parentDescriptor);
+
+ synchronized (mCustomDescriptorMap) {
+ map = mCustomDescriptorMap.get(project);
+ if (map == null) {
+ map = new HashMap<String, ViewElementDescriptor>();
+ mCustomDescriptorMap.put(project, map);
+ }
+
+ map.put(fqcn, descriptor);
+ }
+
+ //TODO setup listener on this resource change.
+
+ return descriptor;
+ }
+ }
+ } catch (JavaModelException e) {
+ // there was an error accessing any of the IType, we'll just return null;
+ }
+ }
+
+ return null;
+ }
+
+ private static AttributeDescriptor[] join(
+ @NonNull List<AttributeDescriptor> attributeList,
+ @NonNull AttributeDescriptor[] attributes) {
+ if (!attributeList.isEmpty()) {
+ return ObjectArrays.concat(
+ attributeList.toArray(new AttributeDescriptor[attributeList.size()]),
+ attributes,
+ AttributeDescriptor.class);
+ } else {
+ return attributes;
+ }
+
+ }
+
+ /** Cache used by {@link #getParser(ResourceFile)} */
+ private Map<ResourceFile, AttrsXmlParser> mParserCache;
+
+ private AttrsXmlParser getParser(ResourceFile file) {
+ if (mParserCache == null) {
+ mParserCache = new HashMap<ResourceFile, AttrsXmlParser>();
+ }
+
+ AttrsXmlParser parser = mParserCache.get(file);
+ if (parser == null) {
+ parser = new AttrsXmlParser(
+ file.getFile().getOsLocation(),
+ AdtPlugin.getDefault(), 20);
+ parser.preload();
+ mParserCache.put(file, parser);
+ }
+
+ return parser;
+ }
+
+ /** Compute/find the styleable resources for the given type, if possible */
+ private Map<ResourceFile, Long> findCustomDescriptors(
+ IProject project,
+ IType type,
+ List<AttributeDescriptor> customAttributes,
+ List<AttributeDescriptor> customLayoutAttributes) {
+ // Look up the project where the type is declared (could be a library project;
+ // we cannot use type.getJavaProject().getProject())
+ IProject library = getProjectDeclaringType(type);
+ if (library == null) {
+ library = project;
+ }
+
+ String className = type.getElementName();
+ Set<ResourceFile> resourceFiles = findAttrsFiles(library, className);
+ if (resourceFiles != null && resourceFiles.size() > 0) {
+ String appUri = getAppResUri(project);
+ Map<ResourceFile, Long> timestamps =
+ Maps.newHashMapWithExpectedSize(resourceFiles.size());
+ for (ResourceFile file : resourceFiles) {
+ AttrsXmlParser attrsXmlParser = getParser(file);
+ String fqcn = type.getFullyQualifiedName();
+
+ // Attributes
+ ViewClassInfo classInfo = new ViewClassInfo(true, fqcn, className);
+ attrsXmlParser.loadViewAttributes(classInfo);
+ appendAttributes(customAttributes, classInfo.getAttributes(), appUri);
+
+ // Layout params
+ LayoutParamsInfo layoutInfo = new ViewClassInfo.LayoutParamsInfo(
+ classInfo, "Layout", null /*superClassInfo*/); //$NON-NLS-1$
+ attrsXmlParser.loadLayoutParamsAttributes(layoutInfo);
+ appendAttributes(customLayoutAttributes, layoutInfo.getAttributes(), appUri);
+
+ timestamps.put(file, file.getFile().getModificationStamp());
+ }
+
+ return timestamps;
+ }
+
+ return null;
+ }
+
+ /**
+ * Finds the set of XML files (if any) in the given library declaring
+ * attributes for the given class name
+ */
+ @Nullable
+ private static Set<ResourceFile> findAttrsFiles(IProject library, String className) {
+ Set<ResourceFile> resourceFiles = null;
+ ResourceManager manager = ResourceManager.getInstance();
+ ProjectResources resources = manager.getProjectResources(library);
+ if (resources != null) {
+ Collection<ResourceItem> items =
+ resources.getResourceItemsOfType(ResourceType.DECLARE_STYLEABLE);
+ for (ResourceItem item : items) {
+ String viewName = item.getName();
+ if (viewName.equals(className)
+ || (viewName.startsWith(className)
+ && viewName.equals(className + "_Layout"))) { //$NON-NLS-1$
+ if (resourceFiles == null) {
+ resourceFiles = new HashSet<ResourceFile>();
+ }
+ resourceFiles.addAll(item.getSourceFileList());
+ }
+ }
+ }
+ return resourceFiles;
+ }
+
+ /**
+ * Find the project containing this type declaration. We cannot use
+ * {@link IType#getJavaProject()} since that will return the including
+ * project and we're after the library project such that we can find the
+ * attrs.xml file in the same project.
+ */
+ @Nullable
+ private static IProject getProjectDeclaringType(IType type) {
+ IClassFile classFile = type.getClassFile();
+ if (classFile != null) {
+ IPath path = classFile.getPath();
+ IWorkspaceRoot workspace = ResourcesPlugin.getWorkspace().getRoot();
+ IResource resource;
+ if (path.isAbsolute()) {
+ resource = AdtUtils.fileToResource(path.toFile());
+ } else {
+ resource = workspace.findMember(path);
+ }
+ if (resource != null && resource.getProject() != null) {
+ return resource.getProject();
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns the name space to use for application attributes */
+ private static String getAppResUri(IProject project) {
+ String appResource;
+ ProjectState projectState = Sdk.getProjectState(project);
+ if (projectState != null && projectState.isLibrary()) {
+ appResource = AUTO_URI;
+ } else {
+ ManifestInfo manifestInfo = ManifestInfo.get(project);
+ appResource = URI_PREFIX + manifestInfo.getPackage();
+ }
+ return appResource;
+ }
+
+
+ /** Append the {@link AttributeInfo} objects converted {@link AttributeDescriptor}
+ * objects into the given attribute list.
+ * <p>
+ * This is nearly identical to
+ * {@link DescriptorsUtils#appendAttribute(List, String, String, AttributeInfo, boolean, Map)}
+ * but it handles namespace declarations in the attrs.xml file where the android:
+ * namespace is included in the names.
+ */
+ private static void appendAttributes(List<AttributeDescriptor> attributes,
+ AttributeInfo[] attributeInfos, String appResource) {
+ // Custom attributes
+ for (AttributeInfo info : attributeInfos) {
+ String nsUri;
+ if (info.getName().startsWith(ANDROID_NS_NAME_PREFIX)) {
+ info.setName(info.getName().substring(ANDROID_NS_NAME_PREFIX.length()));
+ nsUri = ANDROID_URI;
+ } else {
+ nsUri = appResource;
+ }
+
+ DescriptorsUtils.appendAttribute(attributes,
+ null /*elementXmlName*/, nsUri, info, false /*required*/,
+ null /*overrides*/);
+ }
+ }
+
+ /**
+ * Computes (if needed) and returns the {@link ViewElementDescriptor} for the specified type.
+ *
+ * @return A {@link ViewElementDescriptor} or null if type or typeHierarchy is null.
+ */
+ private ViewElementDescriptor createViewDescriptor(IType type, IProject project,
+ ITypeHierarchy typeHierarchy) {
+ // check if the type is a built-in View class.
+ List<ViewElementDescriptor> builtInList = null;
+
+ // give up if there's no type
+ if (type == null) {
+ return null;
+ }
+
+ String fqcn = type.getFullyQualifiedName();
+
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(project);
+ if (target != null) {
+ AndroidTargetData data = currentSdk.getTargetData(target);
+ if (data != null) {
+ LayoutDescriptors descriptors = data.getLayoutDescriptors();
+ ViewElementDescriptor d = descriptors.findDescriptorByClass(fqcn);
+ if (d != null) {
+ return d;
+ }
+ builtInList = descriptors.getViewDescriptors();
+ }
+ }
+ }
+
+ // it's not a built-in class? Lets look if the superclass is built-in
+ // give up if there's no type
+ if (typeHierarchy == null) {
+ return null;
+ }
+
+ IType parentType = typeHierarchy.getSuperclass(type);
+ if (parentType != null) {
+ ViewElementDescriptor parentDescriptor = createViewDescriptor(parentType, project,
+ typeHierarchy);
+
+ if (parentDescriptor != null) {
+ // parent class is a valid View class with a descriptor, so we create one
+ // for this class.
+ String name = DescriptorsUtils.getBasename(fqcn);
+ // A custom view accepts children if its parent descriptor also does.
+ // The only exception to this is ViewGroup, which accepts children even though
+ // its parent does not.
+ boolean isViewGroup = fqcn.equals(CLASS_VIEWGROUP);
+ boolean hasChildren = isViewGroup || parentDescriptor.hasChildren();
+ ViewElementDescriptor[] children = null;
+ if (hasChildren && builtInList != null) {
+ // We can't figure out what the allowable children are by just
+ // looking at the class, so assume any View is valid
+ children = builtInList.toArray(new ViewElementDescriptor[builtInList.size()]);
+ }
+ ViewElementDescriptor descriptor = new CustomViewDescriptor(name, fqcn,
+ getAttributeDescriptor(type, parentDescriptor),
+ getLayoutAttributeDescriptors(type, parentDescriptor),
+ children, project, null);
+ descriptor.setSuperClass(parentDescriptor);
+
+ // add it to the map
+ synchronized (mCustomDescriptorMap) {
+ HashMap<String, ViewElementDescriptor> map = mCustomDescriptorMap.get(project);
+
+ if (map == null) {
+ map = new HashMap<String, ViewElementDescriptor>();
+ mCustomDescriptorMap.put(project, map);
+ }
+
+ map.put(fqcn, descriptor);
+
+ }
+
+ //TODO setup listener on this resource change.
+
+ return descriptor;
+ }
+ }
+
+ // class is neither a built-in view class, nor extend one. return null.
+ return null;
+ }
+
+ /**
+ * Returns the array of {@link AttributeDescriptor} for the specified {@link IType}.
+ * <p/>
+ * The array should contain the descriptor for this type and all its supertypes.
+ *
+ * @param type the type for which the {@link AttributeDescriptor} are returned.
+ * @param parentDescriptor the {@link ViewElementDescriptor} of the direct superclass.
+ */
+ private static AttributeDescriptor[] getAttributeDescriptor(IType type,
+ ViewElementDescriptor parentDescriptor) {
+ // TODO add the class attribute descriptors to the parent descriptors.
+ return parentDescriptor.getAttributes();
+ }
+
+ private static AttributeDescriptor[] getLayoutAttributeDescriptors(IType type,
+ ViewElementDescriptor parentDescriptor) {
+ return parentDescriptor.getLayoutAttributes();
+ }
+
+ private class CustomViewDescriptor extends ViewElementDescriptor {
+ private Map<ResourceFile, Long> mTimeStamps;
+ private IProject mProject;
+
+ public CustomViewDescriptor(String name, String fqcn, AttributeDescriptor[] attributes,
+ AttributeDescriptor[] layoutAttributes,
+ ElementDescriptor[] children, IProject project,
+ Map<ResourceFile, Long> timestamps) {
+ super(
+ fqcn, // xml name
+ name, // ui name
+ fqcn, // full class name
+ fqcn, // tooltip
+ null, // sdk_url
+ attributes,
+ layoutAttributes,
+ children,
+ false // mandatory
+ );
+ mTimeStamps = timestamps;
+ mProject = project;
+ }
+
+ @Override
+ public Image getGenericIcon() {
+ IconFactory iconFactory = IconFactory.getInstance();
+
+ int index = mXmlName.lastIndexOf('.');
+ if (index != -1) {
+ return iconFactory.getIcon(mXmlName.substring(index + 1),
+ "customView"); //$NON-NLS-1$
+ }
+
+ return iconFactory.getIcon("customView"); //$NON-NLS-1$
+ }
+
+ @Override
+ public boolean syncAttributes() {
+ // Check if any of the descriptors
+ if (mTimeStamps != null) {
+ // Prevent checking actual file timestamps too frequently on rapid burst calls
+ long now = System.currentTimeMillis();
+ if (now - sLastCheck < 1000) {
+ return true;
+ }
+ sLastCheck = now;
+
+ // Check whether the resource files (typically just one) which defined
+ // custom attributes for this custom view have changed, and if so,
+ // refresh the attribute descriptors.
+ // This doesn't work the cases where you add descriptors for a custom
+ // view after using it, or add attributes in a separate file, but those
+ // scenarios aren't quite as common (and would require a bit more expensive
+ // analysis.)
+ for (Map.Entry<ResourceFile, Long> entry : mTimeStamps.entrySet()) {
+ ResourceFile file = entry.getKey();
+ Long timestamp = entry.getValue();
+ boolean recompute = false;
+ if (file.getFile().getModificationStamp() > timestamp.longValue()) {
+ // One or more attributes changed: recompute
+ recompute = true;
+ mParserCache.remove(file);
+ }
+
+ if (recompute) {
+ IJavaProject javaProject = JavaCore.create(mProject);
+ String fqcn = getFullClassName();
+ IType type = null;
+ try {
+ type = javaProject.findType(fqcn);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ if (type == null || !type.exists()) {
+ return true;
+ }
+
+ List<AttributeDescriptor> attrList = new ArrayList<AttributeDescriptor>();
+ List<AttributeDescriptor> paramList = new ArrayList<AttributeDescriptor>();
+
+ mTimeStamps = findCustomDescriptors(mProject, type, attrList, paramList);
+
+ ViewElementDescriptor parentDescriptor = getSuperClassDesc();
+ AttributeDescriptor[] attributes =
+ getAttributeDescriptor(type, parentDescriptor);
+ if (!attrList.isEmpty()) {
+ attributes = join(attrList, attributes);
+ }
+ attributes = attrList.toArray(new AttributeDescriptor[attrList.size()]);
+ setAttributes(attributes);
+
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+
+ /** Timestamp of the most recent {@link CustomViewDescriptor#syncAttributes} check */
+ private static long sLastCheck;
+}