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.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      */
     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      */
     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 
    117     public void refresh() {
    118         refresh(null /*listener*/, true /* sync */);
    119     }
    120 
    121     public void refresh(final Listener listener) {
    122         refresh(listener, false /* sync */);
    123     }
    124 
    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 
    155     public Collection<String> getCustomViews() {
    156         return mCustomViews == null ? null : Collections.unmodifiableCollection(mCustomViews);
    157     }
    158 
    159     public Collection<String> getThirdPartyViews() {
    160         return mThirdPartyViews == null
    161             ? null : Collections.unmodifiableCollection(mThirdPartyViews);
    162     }
    163 
    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      */
    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 
    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      */
    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 {
    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 {
    359         FindViewsJob() {
    360             super("Find Custom Views");
    361             setSystem(true);
    362         }
    363         @Override
    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