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