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