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