diff options
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.java | 621 |
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; +} |