blob: 1f97c8c545d0911f908dec9e4460cb758b96dc20 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Eclipse Public License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.eclipse.org/org/documents/epl-v10.php
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
import static com.android.SdkConstants.CLASS_VIEW;
import static com.android.SdkConstants.CLASS_VIEWGROUP;
import static com.android.SdkConstants.FN_FRAMEWORK_LIBRARY;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.utils.Pair;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchMatch;
import org.eclipse.jdt.core.search.SearchParticipant;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.SearchRequestor;
import org.eclipse.jdt.internal.core.ResolvedBinaryType;
import org.eclipse.jdt.internal.core.ResolvedSourceType;
import org.eclipse.swt.widgets.Display;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* The {@link CustomViewFinder} can look up the custom views and third party views
* available for a given project.
*/
@SuppressWarnings("restriction") // JDT model access for custom-view class lookup
public class CustomViewFinder {
/**
* Qualified name for the per-project non-persistent property storing the
* {@link CustomViewFinder} for this project
*/
private final static QualifiedName CUSTOM_VIEW_FINDER = new QualifiedName(AdtPlugin.PLUGIN_ID,
"viewfinder"); //$NON-NLS-1$
/** Project that this view finder locates views for */
private final IProject mProject;
private final List<Listener> mListeners = new ArrayList<Listener>();
private List<String> mCustomViews;
private List<String> mThirdPartyViews;
private boolean mRefreshing;
/**
* Constructs an {@link CustomViewFinder} for the given project. Don't use this method;
* use the {@link #get} factory method instead.
*
* @param project project to create an {@link CustomViewFinder} for
*/
private CustomViewFinder(IProject project) {
mProject = project;
}
/**
* Returns the {@link CustomViewFinder} for the given project
*
* @param project the project the finder is associated with
* @return a {@CustomViewFinder} for the given project, never null
*/
public static CustomViewFinder get(IProject project) {
CustomViewFinder finder = null;
try {
finder = (CustomViewFinder) project.getSessionProperty(CUSTOM_VIEW_FINDER);
} catch (CoreException e) {
// Not a problem; we will just create a new one
}
if (finder == null) {
finder = new CustomViewFinder(project);
try {
project.setSessionProperty(CUSTOM_VIEW_FINDER, finder);
} catch (CoreException e) {
AdtPlugin.log(e, "Can't store CustomViewFinder");
}
}
return finder;
}
public void refresh() {
refresh(null /*listener*/, true /* sync */);
}
public void refresh(final Listener listener) {
refresh(listener, false /* sync */);
}
private void refresh(final Listener listener, boolean sync) {
// Add this listener to the list of listeners which should be notified when the
// search is done. (There could be more than one since multiple requests could
// arrive for a slow search since the search is run in a different thread).
if (listener != null) {
synchronized (this) {
mListeners.add(listener);
}
}
synchronized (this) {
if (listener != null) {
mListeners.add(listener);
}
if (mRefreshing) {
return;
}
mRefreshing = true;
}
FindViewsJob job = new FindViewsJob();
job.schedule();
if (sync) {
try {
job.join();
} catch (InterruptedException e) {
AdtPlugin.log(e, null);
}
}
}
public Collection<String> getCustomViews() {
return mCustomViews == null ? null : Collections.unmodifiableCollection(mCustomViews);
}
public Collection<String> getThirdPartyViews() {
return mThirdPartyViews == null
? null : Collections.unmodifiableCollection(mThirdPartyViews);
}
public Collection<String> getAllViews() {
// Not yet initialized: return null
if (mCustomViews == null) {
return null;
}
List<String> all = new ArrayList<String>(mCustomViews.size() + mThirdPartyViews.size());
all.addAll(mCustomViews);
all.addAll(mThirdPartyViews);
return all;
}
/**
* Returns a pair of view lists - the custom views and the 3rd-party views.
* This method performs no caching; it is the same as asking the custom view finder
* to refresh itself and then waiting for the answer and returning it.
*
* @param project the Android project
* @param layoutsOnly if true, only search for layouts
* @return a pair of lists, the first containing custom views and the second
* containing 3rd party views
*/
public static Pair<List<String>,List<String>> findViews(
final IProject project, boolean layoutsOnly) {
CustomViewFinder finder = get(project);
return finder.findViews(layoutsOnly);
}
private Pair<List<String>,List<String>> findViews(final boolean layoutsOnly) {
final Set<String> customViews = new HashSet<String>();
final Set<String> thirdPartyViews = new HashSet<String>();
ProjectState state = Sdk.getProjectState(mProject);
final List<IProject> libraries = state != null
? state.getFullLibraryProjects() : Collections.<IProject>emptyList();
SearchRequestor requestor = new SearchRequestor() {
@Override
public void acceptSearchMatch(SearchMatch match) throws CoreException {
// Ignore matches in comments
if (match.isInsideDocComment()) {
return;
}
Object element = match.getElement();
if (element instanceof ResolvedBinaryType) {
// Third party view
ResolvedBinaryType type = (ResolvedBinaryType) element;
IPackageFragment fragment = type.getPackageFragment();
IPath path = fragment.getPath();
String last = path.lastSegment();
// Filter out android.jar stuff
if (last.equals(FN_FRAMEWORK_LIBRARY)) {
return;
}
if (!isValidView(type, layoutsOnly)) {
return;
}
IProject matchProject = match.getResource().getProject();
if (mProject == matchProject || libraries.contains(matchProject)) {
String fqn = type.getFullyQualifiedName();
thirdPartyViews.add(fqn);
}
} else if (element instanceof ResolvedSourceType) {
// User custom view
IProject matchProject = match.getResource().getProject();
if (mProject == matchProject || libraries.contains(matchProject)) {
ResolvedSourceType type = (ResolvedSourceType) element;
if (!isValidView(type, layoutsOnly)) {
return;
}
String fqn = type.getFullyQualifiedName();
fqn = fqn.replace('$', '.');
customViews.add(fqn);
}
}
}
};
try {
IJavaProject javaProject = BaseProjectHelper.getJavaProject(mProject);
if (javaProject != null) {
String className = layoutsOnly ? CLASS_VIEWGROUP : CLASS_VIEW;
IType viewType = javaProject.findType(className);
if (viewType != null) {
IJavaSearchScope scope = SearchEngine.createHierarchyScope(viewType);
SearchParticipant[] participants = new SearchParticipant[] {
SearchEngine.getDefaultSearchParticipant()
};
int matchRule = SearchPattern.R_PATTERN_MATCH | SearchPattern.R_CASE_SENSITIVE;
SearchPattern pattern = SearchPattern.createPattern("*",
IJavaSearchConstants.CLASS, IJavaSearchConstants.IMPLEMENTORS,
matchRule);
SearchEngine engine = new SearchEngine();
engine.search(pattern, participants, scope, requestor,
new NullProgressMonitor());
}
}
} catch (CoreException e) {
AdtPlugin.log(e, null);
}
List<String> custom = new ArrayList<String>(customViews);
List<String> thirdParty = new ArrayList<String>(thirdPartyViews);
if (!layoutsOnly) {
// Update our cached answers (unless we were filtered on only layouts)
mCustomViews = custom;
mThirdPartyViews = thirdParty;
}
return Pair.of(custom, thirdParty);
}
/**
* Determines whether the given member is a valid android.view.View to be added to the
* list of custom views or third party views. It checks that the view is public and
* not abstract for example.
*/
private static boolean isValidView(IType type, boolean layoutsOnly)
throws JavaModelException {
// Skip anonymous classes
if (type.isAnonymous()) {
return false;
}
int flags = type.getFlags();
if (Flags.isAbstract(flags) || !Flags.isPublic(flags)) {
return false;
}
// TODO: if (layoutsOnly) perhaps try to filter out AdapterViews and other ViewGroups
// not willing to accept children via XML
// See if the class has one of the acceptable constructors
// needed for XML instantiation:
// View(Context context)
// View(Context context, AttributeSet attrs)
// View(Context context, AttributeSet attrs, int defStyle)
// We don't simply do three direct checks via type.getMethod() because the types
// are not resolved, so we don't know for each parameter if we will get the
// fully qualified or the unqualified class names.
// Instead, iterate over the methods and look for a match.
String typeName = type.getElementName();
for (IMethod method : type.getMethods()) {
// Only care about constructors
if (!method.getElementName().equals(typeName)) {
continue;
}
String[] parameterTypes = method.getParameterTypes();
if (parameterTypes == null || parameterTypes.length < 1 || parameterTypes.length > 3) {
continue;
}
String first = parameterTypes[0];
// Look for the parameter type signatures -- produced by
// JDT's Signature.createTypeSignature("Context", false /*isResolved*/);.
// This is not a typo; they were copy/pasted from the actual parameter names
// observed in the debugger examining these data structures.
if (first.equals("QContext;") //$NON-NLS-1$
|| first.equals("Qandroid.content.Context;")) { //$NON-NLS-1$
if (parameterTypes.length == 1) {
return true;
}
String second = parameterTypes[1];
if (second.equals("QAttributeSet;") //$NON-NLS-1$
|| second.equals("Qandroid.util.AttributeSet;")) { //$NON-NLS-1$
if (parameterTypes.length == 2) {
return true;
}
String third = parameterTypes[2];
if (third.equals("I")) { //$NON-NLS-1$
if (parameterTypes.length == 3) {
return true;
}
}
}
}
}
return false;
}
/**
* Interface implemented by clients of the {@link CustomViewFinder} to be notified
* when a custom view search has completed. Will always be called on the SWT event
* dispatch thread.
*/
public interface Listener {
void viewsUpdated(Collection<String> customViews, Collection<String> thirdPartyViews);
}
/**
* Job for performing class search off the UI thread. This is marked as a system job
* so that it won't show up in the progress monitor etc.
*/
private class FindViewsJob extends Job {
FindViewsJob() {
super("Find Custom Views");
setSystem(true);
}
@Override
protected IStatus run(IProgressMonitor monitor) {
Pair<List<String>, List<String>> views = findViews(false);
mCustomViews = views.getFirst();
mThirdPartyViews = views.getSecond();
// Notify listeners on SWT's UI thread
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
Collection<String> customViews =
Collections.unmodifiableCollection(mCustomViews);
Collection<String> thirdPartyViews =
Collections.unmodifiableCollection(mThirdPartyViews);
synchronized (this) {
for (Listener l : mListeners) {
l.viewsUpdated(customViews, thirdPartyViews);
}
mListeners.clear();
mRefreshing = false;
}
}
});
return Status.OK_STATUS;
}
}
}