blob: 44dbe931744e514c66151974c8c3607713f4e36d [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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.tools.idea.editors.navigation.macros;
import com.android.SdkConstants;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.resources.ResourceResolver;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.editors.navigation.NavigationView;
import com.android.tools.idea.editors.navigation.NavigationEditorUtils;
import com.android.tools.idea.editors.navigation.model.*;
import com.android.tools.idea.model.ManifestInfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.PsiClassReferenceType;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PropertyUtil;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.android.dom.AndroidAttributeValue;
import org.jetbrains.android.dom.manifest.Activity;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public class Analyser {
private static final Logger LOG = Logger.getInstance(Analyser.class.getName());
private static final String[] ID_PREFIXES = {"@+id/", "@android:id/"};
private static final boolean DEBUG = false;
private static final String[] ACTIVITY_SIGNATURES =
{"void onCreate(Bundle b)", "void onCreateView(View parent, String name, Context context, AttributeSet attrs)"};
private static final String[] FRAGMENT_SIGNATURES = {"void onCreate(Bundle b)", "void onViewCreated(View v, Bundle b)"};
public static final String FAKE_OVERFLOW_MENU_ID = "$overflow_menu$";
public static final String EMPTY_LAYOUT_TAG = SdkConstants.SPACE;
private final Module myModule;
private final Macros myMacros;
public Analyser(Module module) {
myModule = module;
myMacros = Macros.getInstance(myModule.getProject());
}
public NavigationModel getNavigationModel(Configuration configuration) {
return deriveAllStatesAndTransitions(configuration);
}
@Nullable
private static String qualifyClassNameIfNecessary(@Nullable String packageName, String className) {
if (className == null) {
return null;
}
else {
if (className.startsWith(".")) {
if (packageName == null) {
LOG.warn("Missing package name for unqualified class: " + className);
return null;
}
return packageName + className;
}
else {
return className;
}
}
}
@Nullable
public static String unQuote(String s) {
if (s.startsWith("\"") && s.endsWith("\"")) {
return s.substring(1, s.length() - 1);
}
assert false;
return null;
}
private static Set<String> getActivitiesFromManifestFile(Module module) {
Set<String> result = new HashSet<String>();
ManifestInfo manifestInfo = ManifestInfo.get(module, false);
String packageName = manifestInfo.getPackage();
List<Activity> activities = manifestInfo.getActivities();
for (Activity activity : activities) {
AndroidAttributeValue<PsiClass> activityClass = activity.getActivityClass();
String className = activityClass.getRawText();
String qualifiedName = qualifyClassNameIfNecessary(packageName, className);
result.add(qualifiedName);
}
return result;
}
private static ActivityState getActivityState(String className, Map<String, ActivityState> classNameToActivityState) {
ActivityState result = classNameToActivityState.get(className);
if (result == null) {
classNameToActivityState.put(className, result = new ActivityState(className));
}
return result;
}
private static MenuState getMenuState(String className, String menuName, Map<String, MenuState> classNameToMenuState) {
MenuState result = classNameToMenuState.get(className);
if (result == null) {
classNameToMenuState.put(menuName, result = MenuState.create(className, menuName));
}
return result;
}
public static abstract class Evaluator {
public static final Evaluator TRUE_OR_FALSE = new Evaluator() {
@Nullable
@Override
public Object evaluate(@Nullable PsiExpression expression) {
return null;
}
};
public abstract Object evaluate(@Nullable PsiExpression expression);
}
public <T extends PsiVariable> Map<T, Object> searchForVariableAssignments(@Nullable PsiElement input,
final Class<T> variableType,
final Evaluator evaluator) {
final Map<T, Object> result = new HashMap<T, Object>();
search(input, evaluator, myMacros.createMacro("void assign(Object $lhs, Object $rhs) { $lhs = $rhs; }"), new Processor() {
@Override
public void process(MultiMatch.Bindings<PsiElement> exp) {
PsiElement lExpression = exp.get("$lhs");
PsiElement rExpression = exp.get("$rhs");
if (lExpression instanceof PsiReferenceExpression && rExpression instanceof PsiExpression) {
PsiReferenceExpression ref = (PsiReferenceExpression)lExpression;
PsiElement resolvedValue = ref.resolve();
if (variableType.isInstance(resolvedValue)) {
//noinspection unchecked
result.put((T)resolvedValue, evaluator.evaluate((PsiExpression)rExpression));
}
}
}
});
return result;
}
public Map<PsiLocalVariable, Object> searchForVariableBindings(@Nullable PsiElement input, final Evaluator evaluator) {
final Map<PsiLocalVariable, Object> result = new HashMap<PsiLocalVariable, Object>();
if (input == null) {
return result;
}
input.accept(new JavaRecursiveElementVisitor() {
@Override
public void visitDeclarationStatement(PsiDeclarationStatement statement) {
super.visitDeclarationStatement(statement);
PsiElement[] declaredElements = statement.getDeclaredElements();
for (PsiElement element : declaredElements) {
if (element instanceof PsiLocalVariable) {
PsiLocalVariable local = (PsiLocalVariable)element;
PsiElement rExpression = local.getInitializer();
result.put(local, evaluator.evaluate((PsiExpression)rExpression));
}
}
}
});
return result;
}
private Map<PsiField, Object> getFieldAssignmentsInOnCreate(PsiClass activityClass, Evaluator evaluator) {
if (activityClass == null) {
return Collections.emptyMap();
}
PsiMethod method = NavigationEditorUtils.findMethodBySignature(activityClass, "void onCreate(Bundle bundle)");
if (method == null) {
return Collections.emptyMap();
}
return searchForVariableAssignments(method.getBody(), PsiField.class, evaluator);
}
private Map<PsiLocalVariable, Object> getLocalVariableBindingsInOnViewCreated(PsiClass activityClass, Evaluator evaluator) {
if (activityClass == null) {
return Collections.emptyMap();
}
PsiMethod method = NavigationEditorUtils.findMethodBySignature(activityClass, "void onViewCreated(View view, Bundle bundle)");
if (method == null) {
return Collections.emptyMap();
}
return searchForVariableBindings(method.getBody(), evaluator);
}
private Evaluator getEvaluator(final Set<String> ids,
final Set<String> tags,
final Map<PsiField, Object> fieldValues,
final Map<PsiLocalVariable, Object> localVariableValues) {
return new Evaluator() {
@Nullable
@Override
public Object evaluate(@Nullable PsiExpression exp) {
if (exp == null) { // todo does this actually make sense?
return null;
}
if (exp instanceof PsiLiteral) {
PsiLiteral literal = (PsiLiteral)exp;
return literal.getValue();
}
MultiMatch.Bindings<PsiElement> match1 = myMacros.findViewById1.match(exp);
if (match1 != null) {
String id = match1.bindings.get("$id").getText();
return ids.contains(id) ? new Object() : null;
}
MultiMatch.Bindings<PsiElement> match2 = myMacros.findFragmentByTag.match(exp);
if (match2 != null) {
String tag = unQuote(match2.bindings.get("$tag").getText());
return tags.contains(tag) ? new Object() : null;
}
if (exp instanceof PsiTypeCastExpression) {
PsiTypeCastExpression castExp = (PsiTypeCastExpression)exp;
return evaluate(castExp.getOperand());
}
if (exp instanceof PsiBinaryExpression) {
PsiBinaryExpression binExp = (PsiBinaryExpression)exp;
IElementType op = binExp.getOperationSign().getTokenType();
Object lhs = evaluate(binExp.getLOperand());
Object rhs = evaluate(binExp.getROperand());
if (op == JavaTokenType.EQEQ) {
return lhs == rhs;
}
if (op == JavaTokenType.NE) {
return lhs != rhs;
}
}
if (exp instanceof PsiReferenceExpression) {
PsiReferenceExpression psiReferenceExpression = (PsiReferenceExpression)exp;
PsiElement resolved = psiReferenceExpression.resolve();
if (resolved instanceof PsiField) {
PsiField field = (PsiField)resolved;
return fieldValues.get(field);
}
else {
if (resolved instanceof PsiLocalVariable) {
PsiLocalVariable localVariable = (PsiLocalVariable)resolved;
return localVariableValues.get(localVariable);
}
}
}
return null;
}
};
}
@Nullable
private static String getQualifiedName(@Nullable PsiClass psiClass) {
return psiClass == null ? null : psiClass.getQualifiedName();
}
private Evaluator getEvaluator(Configuration configuration,
@Nullable PsiClass activityClass,
PsiClass activityOrFragmentClass,
boolean isActivity) {
Set<String> tags = getTags(getXmlFile(configuration, getQualifiedName(activityClass), true));
Set<String> ids = getIds(getXmlFile(configuration, getQualifiedName(activityOrFragmentClass), isActivity));
Map<PsiField, Object> noFields = Collections.emptyMap();
Map<PsiLocalVariable, Object> noVars = Collections.emptyMap();
Evaluator evaluator1 = getEvaluator(ids, tags, noFields, noVars);
Map<PsiField, Object> fieldBindings = getFieldAssignmentsInOnCreate(activityOrFragmentClass, evaluator1);
Evaluator evaluator2 = getEvaluator(ids, tags, fieldBindings, noVars);
Map<PsiLocalVariable, Object> localVariableBindings = getLocalVariableBindingsInOnViewCreated(activityOrFragmentClass, evaluator2);
return getEvaluator(ids, tags, fieldBindings, localVariableBindings);
}
@Nullable
private XmlFile getXmlFile(Configuration configuration, @Nullable String className, boolean isActivity) {
if (className == null) {
return null;
}
String xmlFileName = getXMLFileName(myModule, className, isActivity);
if (xmlFileName == null) {
return null;
}
return (XmlFile)NavigationView.getLayoutXmlFile(false, xmlFileName, configuration, myModule.getProject());
}
private static PsiLocator<String> getGetTag(final Macros macros) {
return new PsiLocator<String>() {
@Nullable
@Override
public String locateIn(MultiMatch.Bindings<PsiElement> args) {
PsiElement view = args.get("$view");
MultiMatch.Bindings<PsiElement> bindings1 = macros.findViewById1.match(view);
if (bindings1 != null) {
return bindings1.get("$id").getText();
}
MultiMatch.Bindings<PsiElement> bindings2 = macros.findViewById2.match(view);
if (bindings2 != null) {
return bindings2.get("$id").getText();
}
return null;
}
};
}
private static PsiLocator<String> getFindMenuItem(final Macros macros) {
return new PsiLocator<String>() {
@Nullable
@Override
public String locateIn(MultiMatch.Bindings<PsiElement> args) {
PsiElement view = args.get("$menuItem");
MultiMatch.Bindings<PsiElement> bindings = macros.findMenuItem.match(view);
return (bindings == null) ? null : bindings.get("$id").getText();
}
};
}
private static Processor createProcessor(final NavigationModel model,
final State fromState,
@Nullable final String fromFragmentClassName,
final Evaluator evaluator,
final Map<String, ActivityState> classNameToActivityState,
final String name,
final MultiMatch matcher,
final PsiLocator<String> locator1,
final PsiLocator<PsiElement> locator2) {
return new Processor() {
@Override
public void process(MultiMatch.Bindings<PsiElement> args) {
search(args.get(name), evaluator, matcher,
createProcessor(model, classNameToActivityState, fromState, fromFragmentClassName, constant(locator1.locateIn(args)),
locator2));
}
};
}
// Look for 'simple' names of classes here to cover both platform and support library derivatives.
private static boolean isListInheritor(PsiClass c, boolean activity) {
String baseName = activity ? "ListActivity" : "ListFragment";
for(PsiClass s = c; s != null; s = s.getSuperClass()) {
if (s.getName().equals(baseName)) {
return true;
}
}
return false;
}
private void deriveTransitions(final NavigationModel model,
final MiniModel miniModel,
final ActivityState activityState,
@Nullable final String fragmentClassName,
final Configuration configuration,
final boolean isActivity) {
boolean isFragment = !isActivity;
final String activityClassName = activityState.getClassName();
if (DEBUG) LOG.info("Analyser: activityClassName = " + activityClassName);
if (DEBUG) LOG.info("Analyser: fragmentClassName = " + fragmentClassName);
final PsiClass activityClass = NavigationEditorUtils.getPsiClass(myModule, activityClassName);
if (activityClass == null) {
// Either a build is underway or a navigation file is out-of-date and refers to classes that have been deleted. Give up.
LOG.info("Class " + activityClassName + " not found");
return;
}
final PsiClass fragmentClass = fragmentClassName != null ? NavigationEditorUtils.getPsiClass(myModule, fragmentClassName) : null;
if (isFragment && fragmentClass == null) {
// Either a build is underway or a navigation file is out-of-date and refers to classes that have been deleted. Give up.
LOG.info("Class " + fragmentClassName + " not found");
return;
}
final PsiClass activityOrFragmentClass = isActivity ? activityClass : fragmentClass;
final Evaluator evaluator = getEvaluator(configuration, activityClass, activityOrFragmentClass, isActivity);
final Map<String, ActivityState> classNameToActivityState = miniModel.classNameToActivityState;
// Search for menu inflation in Activities
if (isActivity) {
boolean actionBarDisabled = false;
final ResourceResolver resolver = configuration.getResourceResolver();
if (resolver != null) {
ResourceValue value = resolver.findItemInTheme("windowActionBar", true);
if (value != null && "false".equalsIgnoreCase(value.getValue())) {
actionBarDisabled = true;
}
}
if (!actionBarDisabled) {
search(activityClass, "boolean onCreateOptionsMenu(Menu menu)",
"void macro(Object target, String id, Menu menu) { target.inflate(id, menu); }", new Processor() {
@Override
public void process(MultiMatch.Bindings<PsiElement> args) {
String menuIdName = args.get("id").getLastChild().getText();
final MenuState menu = getMenuState(activityClassName, menuIdName, miniModel.classNameToMenuToMenuState);
addTransition(model, new Transition(Transition.PRESS, Locator.of(activityState, FAKE_OVERFLOW_MENU_ID), Locator.of(menu)));
for (PsiClass superClass = activityClass; superClass != null; superClass = superClass.getSuperClass()) {
// Search for menu item bindings in the style the Navigation Editor generates them
search(superClass, "boolean onPrepareOptionsMenu(Menu m)", myMacros.installMenuItemClickAndCallMacro,
createProcessor(model, menu, fragmentClassName, evaluator, classNameToActivityState, "$f", myMacros.createIntent,
getFindMenuItem(myMacros), CLASS_NAME_1));
// Search for switch statement style menu item bindings
search(superClass, "boolean onOptionsItemSelected(MenuItem item)", myMacros.createIntent,
createProcessor(model, classNameToActivityState, menu, fragmentClassName, constant(null), CLASS_NAME_1));
}
}
});
}
}
// In both Activities and Fragments, search for:
// 'onClick' listeners in Views (Buttons etc),
// 'OnItemClick' listeners in ListViews.
String[] signatures = isActivity ? ACTIVITY_SIGNATURES : FRAGMENT_SIGNATURES;
for (String signature : signatures) {
search(activityOrFragmentClass, signature, myMacros.installClickAndCallMacro,
createProcessor(model, activityState, fragmentClassName, evaluator, classNameToActivityState, "$f", myMacros.createIntent,
getGetTag(myMacros), CLASS_NAME_1));
search(activityOrFragmentClass, signature, myMacros.installItemClickAndCallMacro,
createProcessor(model, activityState, fragmentClassName, evaluator, classNameToActivityState, "$f", myMacros.createIntent,
constant(NavigationView.LIST_VIEW_ID), CLASS_NAME_1));
}
// Search for subclass style item click listeners in ListActivity and ListFragment derivatives
if (isListInheritor(activityOrFragmentClass, isActivity)) {
search(activityOrFragmentClass, "void onListItemClick(ListView l, View v, int position, long id)", myMacros.createIntent,
createProcessor(model, classNameToActivityState, activityState, fragmentClassName, constant(NavigationView.LIST_VIEW_ID),
CLASS_NAME_1));
}
// Accommodate idioms from Master-Detail template
if (isFragment) {
if (isListInheritor(fragmentClass, false)) {
search(activityOrFragmentClass, "void onListItemClick(ListView listView, View view, int position, long id)",
"void macro(Object f) { f.$(); }", // this obscure term matches 'any method call'
new Processor() {
@Override
public void process(MultiMatch.Bindings<PsiElement> args) {
PsiElement exp = args.get("f");
if (exp instanceof PsiMethodCallExpression) {
PsiMethodCallExpression call = (PsiMethodCallExpression)exp;
if (call.getFirstChild().getFirstChild().getText().equals("super")) {
return;
}
PsiMethod resolvedMethod = call.resolveMethod();
if (resolvedMethod != null) {
PsiMethod implementation = activityClass.findMethodBySignature(resolvedMethod, false);
if (implementation != null) {
Evaluator evaluator = getEvaluator(configuration, activityClass, activityClass, true);
search(implementation.getBody(), evaluator, myMacros.createIntent,
createProcessor(model, classNameToActivityState, activityState, fragmentClassName, constant(null),
CLASS_NAME_1));
}
}
}
}
});
}
}
}
static class MiniModel {
final Map<String, ActivityState> classNameToActivityState;
final Map<String, MenuState> classNameToMenuToMenuState;
MiniModel(Map<String, ActivityState> classNameToActivityState, Map<String, MenuState> classNameToMenuToMenuState) {
this.classNameToActivityState = classNameToActivityState;
this.classNameToMenuToMenuState = classNameToMenuToMenuState;
}
MiniModel() {
this(new HashMap<String, ActivityState>(), new HashMap<String, MenuState>());
}
}
private NavigationModel deriveAllStatesAndTransitions(Configuration configuration) {
NavigationModel model = new NavigationModel();
Set<String> activityClassNames = getActivitiesFromManifestFile(myModule);
if (DEBUG) {
LOG.info("deriveAllStatesAndTransitions = " + activityClassNames);
}
MiniModel toDo = new MiniModel(getActivityClassNameToActivityState(activityClassNames), new HashMap<String, MenuState>());
MiniModel next = new MiniModel();
MiniModel done = new MiniModel();
if (DEBUG) {
LOG.info("activityStates = " + toDo.classNameToActivityState.values());
}
while (!toDo.classNameToActivityState.isEmpty()) {
for (ActivityState sourceActivity : toDo.classNameToActivityState.values()) {
if (done.classNameToActivityState.containsKey(sourceActivity.getClassName())) {
continue;
}
XmlFile layoutFile = getXmlFile(configuration, sourceActivity.getClassName(), true);
if (isEmptyLayout(layoutFile)) {
continue;
}
model.addState(sourceActivity); // covers the case of Activities that are not part of a transition (e.g. a model with one Activity)
deriveTransitions(model, next, sourceActivity, null, configuration, true);
// Examine fragments associated with this activity
List<FragmentEntry> fragments = getFragmentEntries(layoutFile);
List<FragmentEntry> fragmentList = sourceActivity.getFragments();
fragmentList.clear();
fragmentList.addAll(fragments);
for (FragmentEntry fragment : fragments) {
deriveTransitions(model, next, sourceActivity, fragment.className, configuration, false);
}
}
done.classNameToActivityState.putAll(toDo.classNameToActivityState);
toDo = next;
next = new MiniModel();
}
return model;
}
/**
* Find a specific element from the results of an expression match.
*/
private abstract static class PsiLocator<T> {
public abstract T locateIn(MultiMatch.Bindings<PsiElement> bindings);
}
private static final PsiLocator<PsiElement> CLASS_NAME_1 = new PsiLocator<PsiElement>() {
@Override
public PsiElement locateIn(MultiMatch.Bindings<PsiElement> args) {
return args.get("activityClass");
}
};
private static PsiLocator<String> constant(@Nullable final String constant) {
return new PsiLocator<String>() {
@Nullable
@Override
public String locateIn(MultiMatch.Bindings<PsiElement> args) {
return constant;
}
};
}
private static Processor createProcessor(final NavigationModel model,
final Map<String, ActivityState> activities,
final State fromState,
@Nullable final String fromFragmentClassName,
final PsiLocator<String> fromViewIdLocator,
final PsiLocator<PsiElement> toStateClassLocator) {
return new Processor() {
@Override
public void process(MultiMatch.Bindings<PsiElement> args) {
//if (BREAK);
PsiElement activityClass = toStateClassLocator.locateIn(args).getFirstChild();
String qualifiedName = getQualifiedName(activityClass);
if (qualifiedName != null) {
ActivityState toState = getActivityState(qualifiedName, activities);
String viewId = fromViewIdLocator.locateIn(args);
addTransition(model, new Transition(Transition.PRESS, Locator.of(fromState, fromFragmentClassName, viewId), Locator.of(toState)));
}
}
};
}
private static List<FragmentEntry> getFragmentEntries(@Nullable XmlFile psiFile) {
final List<FragmentEntry> result = new ArrayList<FragmentEntry>();
if (psiFile == null) {
return result;
}
psiFile.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitXmlTag(XmlTag tag) {
super.visitXmlTag(tag);
if (tag.getName().equals("fragment")) {
String fragmentTag = tag.getAttributeValue("tag", SdkConstants.ANDROID_URI);
String fragmentClassName = tag.getAttributeValue("name", SdkConstants.ANDROID_URI);
if (DEBUG) LOG.info("Analyser: fragmentClassName = " + fragmentClassName);
if (fragmentClassName != null) {
result.add(new FragmentEntry(fragmentClassName, fragmentTag));
}
}
}
});
return result;
}
private static boolean isEmptyLayout(@Nullable XmlFile psiFile) {
if (psiFile == null) {
return false;
}
final Ref<Boolean> result = new Ref<Boolean>(false);
psiFile.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitXmlTag(XmlTag tag) {
// No call to super, we're only interested in the top level element
if (tag.getName().equals(EMPTY_LAYOUT_TAG)) {
result.set(true);
}
}
});
return result.get();
}
public static String getPrefix(@Nullable String idName) {
if (idName == null) {
return "";
}
for (String prefix : ID_PREFIXES) {
if (idName.startsWith(prefix)) {
return prefix;
}
}
int firstSlash = idName.indexOf('/');
String prefix = idName.substring(0, firstSlash + 1); // prefix is "" when '/' is absent
LOG.warn("Unrecognized prefix: " + prefix + " in " + idName);
return prefix;
}
private static Set<String> getIds(@Nullable XmlFile psiFile) {
final Set<String> result = new HashSet<String>();
if (psiFile == null) {
return result;
}
psiFile.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitXmlTag(XmlTag tag) {
super.visitXmlTag(tag);
String id = tag.getAttributeValue("id", SdkConstants.ANDROID_URI);
if (id != null) {
result.add(id.substring(getPrefix(id).length()));
}
}
});
return result;
}
private static Set<String> getTags(@Nullable XmlFile psiFile) {
final Set<String> result = new HashSet<String>();
if (psiFile == null) {
return result;
}
psiFile.accept(new XmlRecursiveElementVisitor() {
@Override
public void visitXmlTag(XmlTag tag) {
super.visitXmlTag(tag);
String id = tag.getAttributeValue("android:tag");
if (id != null) {
result.add(id);
}
}
});
return result;
}
private static boolean addTransition(NavigationModel model, Transition transition) {
if (DEBUG) LOG.info("Analyser: Adding transition: " + transition);
return model.add(transition);
}
@Nullable
private static String getQualifiedName(PsiElement element) {
if (element instanceof PsiTypeElement) {
PsiTypeElement psiTypeElement = (PsiTypeElement)element;
PsiType type1 = psiTypeElement.getType();
if (type1 instanceof PsiClassReferenceType) {
PsiClassReferenceType type = (PsiClassReferenceType)type1;
return getQualifiedName(type.resolve());
}
}
return null;
}
private static Map<String, ActivityState> getActivityClassNameToActivityState(Set<String> activityClassNames) {
Map<String, ActivityState> result = new HashMap<String, ActivityState>();
for (String activityClassName : activityClassNames) {
result.put(activityClassName, new ActivityState(activityClassName));
}
return result;
}
@Nullable
public static String getXMLFileName(Module module, String className, boolean isActivity) {
PsiClass clazz = NavigationEditorUtils.getPsiClass(module, className);
if (clazz == null) {
LOG.warn("Couldn't find class: " + className);
return null;
}
// Use $R because we sometimes see e.g.: com.example.simplemail.activity.R.layout.compose_activity
String signature = isActivity ? "void onCreate(Bundle bundle)" : "View onCreateView(LayoutInflater li, ViewGroup vg, Bundle b)";
String body = isActivity
? "void macro(Object $R, Object $id) { setContentView($R.layout.$id); }"
: "void macro(Object $inflater, Object $R, Object $id, Object $p) { $inflater.inflate($R.layout.$id, $p, false); }";
PsiClass stop = NavigationEditorUtils.getPsiClass(module, isActivity ? "android.app.Activity" : "android.app.Fragment");
for (PsiClass superClass = clazz; superClass != stop && superClass != null; superClass = superClass.getSuperClass()) {
MultiMatch.Bindings<PsiElement> exp = match(superClass, signature, body);
if (exp != null) {
String id = exp.get("$id").getText();
if (id != null) {
return id;
}
}
}
return null;
}
private static abstract class Processor {
public abstract void process(MultiMatch.Bindings<PsiElement> exp);
}
public static void search(@Nullable PsiElement input, final Evaluator evaluator, final MultiMatch matcher, final Processor processor) {
if (input != null) {
input.accept(new JavaRecursiveElementVisitor() {
private void visitNullableExpression(@Nullable PsiExpression expression) {
if (expression != null) {
visitExpression(expression);
}
}
private void visitNullableStatement(@Nullable PsiStatement statement) {
if (statement != null) {
visitStatement(statement);
}
}
@Override
public void visitIfStatement(PsiIfStatement statement) {
PsiExpression condition = statement.getCondition();
visitNullableExpression(condition);
Object value = evaluator.evaluate(condition);
// Both clauses below will be executed when value == BOOLEAN_UNKNOWN (null).
if (value != Boolean.FALSE) { // True branch
visitNullableStatement(statement.getThenBranch());
}
if (value != Boolean.TRUE) { // False branch
visitNullableStatement(statement.getElseBranch());
}
}
@Override
public void visitExpression(PsiExpression expression) {
super.visitExpression(expression);
MultiMatch.Bindings<PsiElement> exp = matcher.match(expression);
if (exp != null) {
processor.process(exp);
}
}
/*
@Override
public void visitStatement(PsiStatement statement) {
super.visitStatement(statement);
Map<String, PsiElement> bindings = Unifier.matchStatement(matcher.macro, statement);
if (bindings != null) {
processor.apply(new MultiMatch.Bindings<PsiElement>(bindings, Collections.EMPTY_MAP));
}
}
*/
});
}
}
public static void search(@Nullable PsiElement input, MultiMatch matcher, Processor processor) {
search(input, Evaluator.TRUE_OR_FALSE, matcher, processor);
}
public static List<MultiMatch.Bindings<PsiElement>> search(@Nullable PsiElement element, final MultiMatch matcher) {
final List<MultiMatch.Bindings<PsiElement>> results = new ArrayList<MultiMatch.Bindings<PsiElement>>();
search(element, matcher, new Processor() {
@Override
public void process(MultiMatch.Bindings<PsiElement> exp) {
results.add(exp);
}
});
return results;
}
private static void search(PsiClass clazz, String methodSignature, MultiMatch matcher, Processor processor) {
PsiMethod method = NavigationEditorUtils.findMethodBySignature(clazz, methodSignature);
if (method == null) {
return;
}
search(method.getBody(), matcher, processor);
}
private static void search(PsiClass clazz, String methodSignature, String matchMacro, Processor processor) {
final PsiMethod psiMethod = NavigationEditorUtils.createMethodFromText(clazz, matchMacro);
search(clazz, methodSignature, new MultiMatch(CodeTemplate.fromMethod(psiMethod)), processor);
}
@Nullable
private static MultiMatch.Bindings<PsiElement> match(@NotNull PsiClass clazz, String methodSignature, String matchMacro) {
PsiMethod method = NavigationEditorUtils.findMethodBySignature(clazz, methodSignature);
if (method == null) {
return null;
}
final PsiMethod psiMethod = NavigationEditorUtils.createMethodFromText(clazz, matchMacro);
MultiMatch matcher = new MultiMatch(CodeTemplate.fromMethod(psiMethod));
List<MultiMatch.Bindings<PsiElement>> results = search(method.getBody(), matcher);
if (results.size() != 1) {
return null;
}
return results.get(0);
}
@SuppressWarnings("UnusedDeclaration")
public static List<String> findProperties(@Nullable PsiClass input) {
if (input == null) {
return Collections.emptyList();
}
final List<String> result = new ArrayList<String>();
input.accept(new JavaRecursiveElementVisitor() {
@Override
public void visitMethod(PsiMethod method) {
super.visitMethod(method);
if (method.getName().startsWith("get")) {
result.add(PropertyUtil.getPropertyName(method));
}
}
});
return result;
}
}