• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
17 
18 import static com.android.SdkConstants.CLASS_VIEW;
19 import static com.android.SdkConstants.CLASS_VIEWGROUP;
20 import static com.android.SdkConstants.FN_FRAMEWORK_LIBRARY;
21 
22 import com.android.ide.eclipse.adt.AdtPlugin;
23 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
24 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
25 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
26 import com.android.utils.Pair;
27 
28 import org.eclipse.core.resources.IProject;
29 import org.eclipse.core.runtime.CoreException;
30 import org.eclipse.core.runtime.IPath;
31 import org.eclipse.core.runtime.IProgressMonitor;
32 import org.eclipse.core.runtime.IStatus;
33 import org.eclipse.core.runtime.NullProgressMonitor;
34 import org.eclipse.core.runtime.QualifiedName;
35 import org.eclipse.core.runtime.Status;
36 import org.eclipse.core.runtime.jobs.Job;
37 import org.eclipse.jdt.core.Flags;
38 import org.eclipse.jdt.core.IJavaProject;
39 import org.eclipse.jdt.core.IMethod;
40 import org.eclipse.jdt.core.IPackageFragment;
41 import org.eclipse.jdt.core.IType;
42 import org.eclipse.jdt.core.JavaModelException;
43 import org.eclipse.jdt.core.search.IJavaSearchConstants;
44 import org.eclipse.jdt.core.search.IJavaSearchScope;
45 import org.eclipse.jdt.core.search.SearchEngine;
46 import org.eclipse.jdt.core.search.SearchMatch;
47 import org.eclipse.jdt.core.search.SearchParticipant;
48 import org.eclipse.jdt.core.search.SearchPattern;
49 import org.eclipse.jdt.core.search.SearchRequestor;
50 import org.eclipse.jdt.internal.core.ResolvedBinaryType;
51 import org.eclipse.jdt.internal.core.ResolvedSourceType;
52 import org.eclipse.swt.widgets.Display;
53 
54 import java.util.ArrayList;
55 import java.util.Collection;
56 import java.util.Collections;
57 import java.util.HashSet;
58 import java.util.List;
59 import java.util.Set;
60 
61 /**
62  * The {@link CustomViewFinder} can look up the custom views and third party views
63  * available for a given project.
64  */
65 @SuppressWarnings("restriction") // JDT model access for custom-view class lookup
66 public class CustomViewFinder {
67     /**
68      * Qualified name for the per-project non-persistent property storing the
69      * {@link CustomViewFinder} for this project
70      */
71     private final static QualifiedName CUSTOM_VIEW_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID,
72             "viewfinder"); //$NON-NLS-1$
73 
74     /** Project that this view finder locates views for */
75     private final IProject mProject;
76 
77     private final List<Listener> mListeners = new ArrayList<Listener>();
78 
79     private List<String> mCustomViews;
80     private List<String> mThirdPartyViews;
81     private boolean mRefreshing;
82 
83     /**
84      * Constructs an {@link CustomViewFinder} for the given project. Don't use this method;
85      * use the {@link #get} factory method instead.
86      *
87      * @param project project to create an {@link CustomViewFinder} for
88      */
CustomViewFinder(IProject project)89     private CustomViewFinder(IProject project) {
90         mProject = project;
91     }
92 
93     /**
94      * Returns the {@link CustomViewFinder} for the given project
95      *
96      * @param project the project the finder is associated with
97      * @return a {@CustomViewFinder} for the given project, never null
98      */
get(IProject project)99     public static CustomViewFinder get(IProject project) {
100         CustomViewFinder finder = null;
101         try {
102             finder = (CustomViewFinder) project.getSessionProperty(CUSTOM_VIEW_FINDER);
103         } catch (CoreException e) {
104             // Not a problem; we will just create a new one
105         }
106 
107         if (finder == null) {
108             finder = new CustomViewFinder(project);
109             try {
110                 project.setSessionProperty(CUSTOM_VIEW_FINDER, finder);
111             } catch (CoreException e) {
112                 AdtPlugin.log(e, "Can't store CustomViewFinder");
113             }
114         }
115 
116         return finder;
117     }
118 
refresh()119     public void refresh() {
120         refresh(null /*listener*/, true /* sync */);
121     }
122 
refresh(final Listener listener)123     public void refresh(final Listener listener) {
124         refresh(listener, false /* sync */);
125     }
126 
refresh(final Listener listener, boolean sync)127     private void refresh(final Listener listener, boolean sync) {
128         // Add this listener to the list of listeners which should be notified when the
129         // search is done. (There could be more than one since multiple requests could
130         // arrive for a slow search since the search is run in a different thread).
131         if (listener != null) {
132             synchronized (this) {
133                 mListeners.add(listener);
134             }
135         }
136         synchronized (this) {
137             if (listener != null) {
138                 mListeners.add(listener);
139             }
140             if (mRefreshing) {
141                 return;
142             }
143             mRefreshing = true;
144         }
145 
146         FindViewsJob job = new FindViewsJob();
147         job.schedule();
148         if (sync) {
149             try {
150                 job.join();
151             } catch (InterruptedException e) {
152                 AdtPlugin.log(e, null);
153             }
154         }
155     }
156 
getCustomViews()157     public Collection<String> getCustomViews() {
158         return mCustomViews == null ? null : Collections.unmodifiableCollection(mCustomViews);
159     }
160 
getThirdPartyViews()161     public Collection<String> getThirdPartyViews() {
162         return mThirdPartyViews == null
163             ? null : Collections.unmodifiableCollection(mThirdPartyViews);
164     }
165 
getAllViews()166     public Collection<String> getAllViews() {
167         // Not yet initialized: return null
168         if (mCustomViews == null) {
169             return null;
170         }
171         List<String> all = new ArrayList<String>(mCustomViews.size() + mThirdPartyViews.size());
172         all.addAll(mCustomViews);
173         all.addAll(mThirdPartyViews);
174         return all;
175     }
176 
177     /**
178      * Returns a pair of view lists - the custom views and the 3rd-party views.
179      * This method performs no caching; it is the same as asking the custom view finder
180      * to refresh itself and then waiting for the answer and returning it.
181      *
182      * @param project the Android project
183      * @param layoutsOnly if true, only search for layouts
184      * @return a pair of lists, the first containing custom views and the second
185      *         containing 3rd party views
186      */
findViews( final IProject project, boolean layoutsOnly)187     public static Pair<List<String>,List<String>> findViews(
188             final IProject project, boolean layoutsOnly) {
189         CustomViewFinder finder = get(project);
190 
191         return finder.findViews(layoutsOnly);
192     }
193 
findViews(final boolean layoutsOnly)194     private Pair<List<String>,List<String>> findViews(final boolean layoutsOnly) {
195         final Set<String> customViews = new HashSet<String>();
196         final Set<String> thirdPartyViews = new HashSet<String>();
197 
198         ProjectState state = Sdk.getProjectState(mProject);
199         final List<IProject> libraries = state != null
200             ? state.getFullLibraryProjects() : Collections.<IProject>emptyList();
201 
202         SearchRequestor requestor = new SearchRequestor() {
203             @Override
204             public void acceptSearchMatch(SearchMatch match) throws CoreException {
205                 // Ignore matches in comments
206                 if (match.isInsideDocComment()) {
207                     return;
208                 }
209 
210                 Object element = match.getElement();
211                 if (element instanceof ResolvedBinaryType) {
212                     // Third party view
213                     ResolvedBinaryType type = (ResolvedBinaryType) element;
214                     IPackageFragment fragment = type.getPackageFragment();
215                     IPath path = fragment.getPath();
216                     String last = path.lastSegment();
217                     // Filter out android.jar stuff
218                     if (last.equals(FN_FRAMEWORK_LIBRARY)) {
219                         return;
220                     }
221                     if (!isValidView(type, layoutsOnly)) {
222                         return;
223                     }
224 
225                     IProject matchProject = match.getResource().getProject();
226                     if (mProject == matchProject || libraries.contains(matchProject)) {
227                         String fqn = type.getFullyQualifiedName();
228                         thirdPartyViews.add(fqn);
229                     }
230                 } else if (element instanceof ResolvedSourceType) {
231                     // User custom view
232                     IProject matchProject = match.getResource().getProject();
233                     if (mProject == matchProject || libraries.contains(matchProject)) {
234                         ResolvedSourceType type = (ResolvedSourceType) element;
235                         if (!isValidView(type, layoutsOnly)) {
236                             return;
237                         }
238                         String fqn = type.getFullyQualifiedName();
239                         fqn = fqn.replace('$', '.');
240                         customViews.add(fqn);
241                     }
242                 }
243             }
244         };
245         try {
246             IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
247             if (javaProject != null) {
248                 String className = layoutsOnly ? CLASS_VIEWGROUP : CLASS_VIEW;
249                 IType viewType = javaProject.findType(className);
250                 if (viewType != null) {
251                     IJavaSearchScope scope = SearchEngine.createHierarchyScope(viewType);
252                     SearchParticipant[] participants = new SearchParticipant[] {
253                         SearchEngine.getDefaultSearchParticipant()
254                     };
255                     int matchRule = SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE;
256 
257                     SearchPattern pattern = SearchPattern.createPattern("*",
258                             IJavaSearchConstants.CLASS, IJavaSearchConstants.IMPLEMENTORS,
259                             matchRule);
260                     SearchEngine engine = new SearchEngine();
261                     engine.search(pattern, participants, scope, requestor,
262                             new NullProgressMonitor());
263                 }
264             }
265         } catch (CoreException e) {
266             AdtPlugin.log(e, null);
267         }
268 
269 
270         List<String> custom = new ArrayList<String>(customViews);
271         List<String> thirdParty = new ArrayList<String>(thirdPartyViews);
272 
273         if (!layoutsOnly) {
274             // Update our cached answers (unless we were filtered on only layouts)
275             mCustomViews = custom;
276             mThirdPartyViews = thirdParty;
277         }
278 
279         return Pair.of(custom, thirdParty);
280     }
281 
282     /**
283      * Determines whether the given member is a valid android.view.View to be added to the
284      * list of custom views or third party views. It checks that the view is public and
285      * not abstract for example.
286      */
isValidView(IType type, boolean layoutsOnly)287     private static boolean isValidView(IType type, boolean layoutsOnly)
288             throws JavaModelException {
289         // Skip anonymous classes
290         if (type.isAnonymous()) {
291             return false;
292         }
293         int flags = type.getFlags();
294         if (Flags.isAbstract(flags) || !Flags.isPublic(flags)) {
295             return false;
296         }
297 
298         // TODO: if (layoutsOnly) perhaps try to filter out AdapterViews and other ViewGroups
299         // not willing to accept children via XML
300 
301         // See if the class has one of the acceptable constructors
302         // needed for XML instantiation:
303         //    View(Context context)
304         //    View(Context context, AttributeSet attrs)
305         //    View(Context context, AttributeSet attrs, int defStyle)
306         // We don't simply do three direct checks via type.getMethod() because the types
307         // are not resolved, so we don't know for each parameter if we will get the
308         // fully qualified or the unqualified class names.
309         // Instead, iterate over the methods and look for a match.
310         String typeName = type.getElementName();
311         for (IMethod method : type.getMethods()) {
312             // Only care about constructors
313             if (!method.getElementName().equals(typeName)) {
314                 continue;
315             }
316 
317             String[] parameterTypes = method.getParameterTypes();
318             if (parameterTypes == null || parameterTypes.length < 1 || parameterTypes.length > 3) {
319                 continue;
320             }
321 
322             String first = parameterTypes[0];
323             // Look for the parameter type signatures -- produced by
324             // JDT's Signature.createTypeSignature("Context", false /*isResolved*/);.
325             // This is not a typo; they were copy/pasted from the actual parameter names
326             // observed in the debugger examining these data structures.
327             if (first.equals("QContext;")                                   //$NON-NLS-1$
328                     || first.equals("Qandroid.content.Context;")) {         //$NON-NLS-1$
329                 if (parameterTypes.length == 1) {
330                     return true;
331                 }
332                 String second = parameterTypes[1];
333                 if (second.equals("QAttributeSet;")                         //$NON-NLS-1$
334                         || second.equals("Qandroid.util.AttributeSet;")) {  //$NON-NLS-1$
335                     if (parameterTypes.length == 2) {
336                         return true;
337                     }
338                     String third = parameterTypes[2];
339                     if (third.equals("I")) {                                //$NON-NLS-1$
340                         if (parameterTypes.length == 3) {
341                             return true;
342                         }
343                     }
344                 }
345             }
346         }
347 
348         return false;
349     }
350 
351     /**
352      * Interface implemented by clients of the {@link CustomViewFinder} to be notified
353      * when a custom view search has completed. Will always be called on the SWT event
354      * dispatch thread.
355      */
356     public interface Listener {
viewsUpdated(Collection<String> customViews, Collection<String> thirdPartyViews)357         void viewsUpdated(Collection<String> customViews, Collection<String> thirdPartyViews);
358     }
359 
360     /**
361      * Job for performing class search off the UI thread. This is marked as a system job
362      * so that it won't show up in the progress monitor etc.
363      */
364     private class FindViewsJob extends Job {
FindViewsJob()365         FindViewsJob() {
366             super("Find Custom Views");
367             setSystem(true);
368         }
369         @Override
run(IProgressMonitor monitor)370         protected IStatus run(IProgressMonitor monitor) {
371             Pair<List<String>, List<String>> views = findViews(false);
372             mCustomViews = views.getFirst();
373             mThirdPartyViews = views.getSecond();
374 
375             // Notify listeners on SWT's UI thread
376             Display.getDefault().asyncExec(new Runnable() {
377                 @Override
378                 public void run() {
379                     Collection<String> customViews =
380                         Collections.unmodifiableCollection(mCustomViews);
381                     Collection<String> thirdPartyViews =
382                         Collections.unmodifiableCollection(mThirdPartyViews);
383                     synchronized (this) {
384                         for (Listener l : mListeners) {
385                             l.viewsUpdated(customViews, thirdPartyViews);
386                         }
387                         mListeners.clear();
388                         mRefreshing = false;
389                     }
390                 }
391             });
392             return Status.OK_STATUS;
393         }
394     }
395 }
396