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