/* * Copyright (C) 2011 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.gle2; import static com.android.SdkConstants.CLASS_VIEW; import static com.android.SdkConstants.CLASS_VIEWGROUP; import static com.android.SdkConstants.FN_FRAMEWORK_LIBRARY; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.utils.Pair; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jdt.core.Flags; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMethod; import org.eclipse.jdt.core.IPackageFragment; import org.eclipse.jdt.core.IType; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.search.IJavaSearchConstants; import org.eclipse.jdt.core.search.IJavaSearchScope; import org.eclipse.jdt.core.search.SearchEngine; import org.eclipse.jdt.core.search.SearchMatch; import org.eclipse.jdt.core.search.SearchParticipant; import org.eclipse.jdt.core.search.SearchPattern; import org.eclipse.jdt.core.search.SearchRequestor; import org.eclipse.jdt.internal.core.ResolvedBinaryType; import org.eclipse.jdt.internal.core.ResolvedSourceType; import org.eclipse.swt.widgets.Display; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * The {@link CustomViewFinder} can look up the custom views and third party views * available for a given project. */ @SuppressWarnings("restriction") // JDT model access for custom-view class lookup public class CustomViewFinder { /** * Qualified name for the per-project non-persistent property storing the * {@link CustomViewFinder} for this project */ private final static QualifiedName CUSTOM_VIEW_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID, "viewfinder"); //$NON-NLS-1$ /** Project that this view finder locates views for */ private final IProject mProject; private final List<Listener> mListeners = new ArrayList<Listener>(); private List<String> mCustomViews; private List<String> mThirdPartyViews; private boolean mRefreshing; /** * Constructs an {@link CustomViewFinder} for the given project. Don't use this method; * use the {@link #get} factory method instead. * * @param project project to create an {@link CustomViewFinder} for */ private CustomViewFinder(IProject project) { mProject = project; } /** * Returns the {@link CustomViewFinder} for the given project * * @param project the project the finder is associated with * @return a {@CustomViewFinder} for the given project, never null */ public static CustomViewFinder get(IProject project) { CustomViewFinder finder = null; try { finder = (CustomViewFinder) project.getSessionProperty(CUSTOM_VIEW_FINDER); } catch (CoreException e) { // Not a problem; we will just create a new one } if (finder == null) { finder = new CustomViewFinder(project); try { project.setSessionProperty(CUSTOM_VIEW_FINDER, finder); } catch (CoreException e) { AdtPlugin.log(e, "Can't store CustomViewFinder"); } } return finder; } public void refresh() { refresh(null /*listener*/, true /* sync */); } public void refresh(final Listener listener) { refresh(listener, false /* sync */); } private void refresh(final Listener listener, boolean sync) { // Add this listener to the list of listeners which should be notified when the // search is done. (There could be more than one since multiple requests could // arrive for a slow search since the search is run in a different thread). if (listener != null) { synchronized (this) { mListeners.add(listener); } } synchronized (this) { if (listener != null) { mListeners.add(listener); } if (mRefreshing) { return; } mRefreshing = true; } FindViewsJob job = new FindViewsJob(); job.schedule(); if (sync) { try { job.join(); } catch (InterruptedException e) { AdtPlugin.log(e, null); } } } public Collection<String> getCustomViews() { return mCustomViews == null ? null : Collections.unmodifiableCollection(mCustomViews); } public Collection<String> getThirdPartyViews() { return mThirdPartyViews == null ? null : Collections.unmodifiableCollection(mThirdPartyViews); } public Collection<String> getAllViews() { // Not yet initialized: return null if (mCustomViews == null) { return null; } List<String> all = new ArrayList<String>(mCustomViews.size() + mThirdPartyViews.size()); all.addAll(mCustomViews); all.addAll(mThirdPartyViews); return all; } /** * Returns a pair of view lists - the custom views and the 3rd-party views. * This method performs no caching; it is the same as asking the custom view finder * to refresh itself and then waiting for the answer and returning it. * * @param project the Android project * @param layoutsOnly if true, only search for layouts * @return a pair of lists, the first containing custom views and the second * containing 3rd party views */ public static Pair<List<String>,List<String>> findViews( final IProject project, boolean layoutsOnly) { CustomViewFinder finder = get(project); return finder.findViews(layoutsOnly); } private Pair<List<String>,List<String>> findViews(final boolean layoutsOnly) { final Set<String> customViews = new HashSet<String>(); final Set<String> thirdPartyViews = new HashSet<String>(); ProjectState state = Sdk.getProjectState(mProject); final List<IProject> libraries = state != null ? state.getFullLibraryProjects() : Collections.<IProject>emptyList(); SearchRequestor requestor = new SearchRequestor() { @Override public void acceptSearchMatch(SearchMatch match) throws CoreException { // Ignore matches in comments if (match.isInsideDocComment()) { return; } Object element = match.getElement(); if (element instanceof ResolvedBinaryType) { // Third party view ResolvedBinaryType type = (ResolvedBinaryType) element; IPackageFragment fragment = type.getPackageFragment(); IPath path = fragment.getPath(); String last = path.lastSegment(); // Filter out android.jar stuff if (last.equals(FN_FRAMEWORK_LIBRARY)) { return; } if (!isValidView(type, layoutsOnly)) { return; } IProject matchProject = match.getResource().getProject(); if (mProject == matchProject || libraries.contains(matchProject)) { String fqn = type.getFullyQualifiedName(); thirdPartyViews.add(fqn); } } else if (element instanceof ResolvedSourceType) { // User custom view IProject matchProject = match.getResource().getProject(); if (mProject == matchProject || libraries.contains(matchProject)) { ResolvedSourceType type = (ResolvedSourceType) element; if (!isValidView(type, layoutsOnly)) { return; } String fqn = type.getFullyQualifiedName(); fqn = fqn.replace('$', '.'); customViews.add(fqn); } } } }; try { IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject); if (javaProject != null) { String className = layoutsOnly ? CLASS_VIEWGROUP : CLASS_VIEW; IType viewType = javaProject.findType(className); if (viewType != null) { IJavaSearchScope scope = SearchEngine.createHierarchyScope(viewType); SearchParticipant[] participants = new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }; int matchRule = SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE; SearchPattern pattern = SearchPattern.createPattern("*", IJavaSearchConstants.CLASS, IJavaSearchConstants.IMPLEMENTORS, matchRule); SearchEngine engine = new SearchEngine(); engine.search(pattern, participants, scope, requestor, new NullProgressMonitor()); } } } catch (CoreException e) { AdtPlugin.log(e, null); } List<String> custom = new ArrayList<String>(customViews); List<String> thirdParty = new ArrayList<String>(thirdPartyViews); if (!layoutsOnly) { // Update our cached answers (unless we were filtered on only layouts) mCustomViews = custom; mThirdPartyViews = thirdParty; } return Pair.of(custom, thirdParty); } /** * Determines whether the given member is a valid android.view.View to be added to the * list of custom views or third party views. It checks that the view is public and * not abstract for example. */ private static boolean isValidView(IType type, boolean layoutsOnly) throws JavaModelException { // Skip anonymous classes if (type.isAnonymous()) { return false; } int flags = type.getFlags(); if (Flags.isAbstract(flags) || !Flags.isPublic(flags)) { return false; } // TODO: if (layoutsOnly) perhaps try to filter out AdapterViews and other ViewGroups // not willing to accept children via XML // See if the class has one of the acceptable constructors // needed for XML instantiation: // View(Context context) // View(Context context, AttributeSet attrs) // View(Context context, AttributeSet attrs, int defStyle) // We don't simply do three direct checks via type.getMethod() because the types // are not resolved, so we don't know for each parameter if we will get the // fully qualified or the unqualified class names. // Instead, iterate over the methods and look for a match. String typeName = type.getElementName(); for (IMethod method : type.getMethods()) { // Only care about constructors if (!method.getElementName().equals(typeName)) { continue; } String[] parameterTypes = method.getParameterTypes(); if (parameterTypes == null || parameterTypes.length < 1 || parameterTypes.length > 3) { continue; } String first = parameterTypes[0]; // Look for the parameter type signatures -- produced by // JDT's Signature.createTypeSignature("Context", false /*isResolved*/);. // This is not a typo; they were copy/pasted from the actual parameter names // observed in the debugger examining these data structures. if (first.equals("QContext;") //$NON-NLS-1$ || first.equals("Qandroid.content.Context;")) { //$NON-NLS-1$ if (parameterTypes.length == 1) { return true; } String second = parameterTypes[1]; if (second.equals("QAttributeSet;") //$NON-NLS-1$ || second.equals("Qandroid.util.AttributeSet;")) { //$NON-NLS-1$ if (parameterTypes.length == 2) { return true; } String third = parameterTypes[2]; if (third.equals("I")) { //$NON-NLS-1$ if (parameterTypes.length == 3) { return true; } } } } } return false; } /** * Interface implemented by clients of the {@link CustomViewFinder} to be notified * when a custom view search has completed. Will always be called on the SWT event * dispatch thread. */ public interface Listener { void viewsUpdated(Collection<String> customViews, Collection<String> thirdPartyViews); } /** * Job for performing class search off the UI thread. This is marked as a system job * so that it won't show up in the progress monitor etc. */ private class FindViewsJob extends Job { FindViewsJob() { super("Find Custom Views"); setSystem(true); } @Override protected IStatus run(IProgressMonitor monitor) { Pair<List<String>, List<String>> views = findViews(false); mCustomViews = views.getFirst(); mThirdPartyViews = views.getSecond(); // Notify listeners on SWT's UI thread Display.getDefault().asyncExec(new Runnable() { @Override public void run() { Collection<String> customViews = Collections.unmodifiableCollection(mCustomViews); Collection<String> thirdPartyViews = Collections.unmodifiableCollection(mThirdPartyViews); synchronized (this) { for (Listener l : mListeners) { l.viewsUpdated(customViews, thirdPartyViews); } mListeners.clear(); mRefreshing = false; } } }); return Status.OK_STATUS; } } }