Home | History | Annotate | Download | only in gle2
      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      */
     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      */
     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 
    119     public void refresh() {
    120         refresh(null /*listener*/, true /* sync */);
    121     }
    122 
    123     public void refresh(final Listener listener) {
    124         refresh(listener, false /* sync */);
    125     }
    126 
    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 
    157     public Collection<String> getCustomViews() {
    158         return mCustomViews == null ? null : Collections.unmodifiableCollection(mCustomViews);
    159     }
    160 
    161     public Collection<String> getThirdPartyViews() {
    162         return mThirdPartyViews == null
    163             ? null : Collections.unmodifiableCollection(mThirdPartyViews);
    164     }
    165 
    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      */
    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 
    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      */
    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 {
    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 {
    365         FindViewsJob() {
    366             super("Find Custom Views");
    367             setSystem(true);
    368         }
    369         @Override
    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