blob: 9029ed20c4fedb9a99dcb486dce4e2aa2fe8d391 [file] [log] [blame]
/*
* Copyright (C) 2015 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.lint.checks;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_EXPORTED;
import static com.android.SdkConstants.ATTR_HOST;
import static com.android.SdkConstants.ATTR_PATH;
import static com.android.SdkConstants.ATTR_PATH_PREFIX;
import static com.android.SdkConstants.ATTR_SCHEME;
import static com.android.SdkConstants.CLASS_ACTIVITY;
import static com.android.xml.AndroidManifest.ATTRIBUTE_MIME_TYPE;
import static com.android.xml.AndroidManifest.ATTRIBUTE_NAME;
import static com.android.xml.AndroidManifest.ATTRIBUTE_PORT;
import static com.android.xml.AndroidManifest.NODE_ACTION;
import static com.android.xml.AndroidManifest.NODE_ACTIVITY;
import static com.android.xml.AndroidManifest.NODE_APPLICATION;
import static com.android.xml.AndroidManifest.NODE_CATEGORY;
import static com.android.xml.AndroidManifest.NODE_DATA;
import static com.android.xml.AndroidManifest.NODE_INTENT;
import static com.android.xml.AndroidManifest.NODE_MANIFEST;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceType;
import com.android.tools.lint.client.api.JavaEvaluator;
import com.android.tools.lint.client.api.LintClient;
import com.android.tools.lint.client.api.XmlParser;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Detector.JavaPsiScanner;
import com.android.tools.lint.detector.api.Detector.XmlScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.LintUtils;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.intellij.psi.JavaRecursiveElementVisitor;
import com.intellij.psi.PsiAnonymousClass;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMethodCallExpression;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
/**
* Check if the usage of App Indexing is correct.
*/
public class AppIndexingApiDetector extends Detector implements XmlScanner, JavaPsiScanner {
private static final Implementation URL_IMPLEMENTATION = new Implementation(
AppIndexingApiDetector.class, Scope.MANIFEST_SCOPE);
@SuppressWarnings("unchecked")
private static final Implementation APP_INDEXING_API_IMPLEMENTATION =
new Implementation(
AppIndexingApiDetector.class,
EnumSet.of(Scope.JAVA_FILE, Scope.MANIFEST),
Scope.JAVA_FILE_SCOPE, Scope.MANIFEST_SCOPE);
public static final Issue ISSUE_URL_ERROR = Issue.create(
"GoogleAppIndexingUrlError", //$NON-NLS-1$
"URL not supported by app for Google App Indexing",
"Ensure the URL is supported by your app, to get installs and traffic to your"
+ " app from Google Search.",
Category.USABILITY, 5, Severity.ERROR, URL_IMPLEMENTATION)
.addMoreInfo("https://g.co/AppIndexing/AndroidStudio");
public static final Issue ISSUE_APP_INDEXING =
Issue.create(
"GoogleAppIndexingWarning", //$NON-NLS-1$
"Missing support for Google App Indexing",
"Adds URLs to get your app into the Google index, to get installs"
+ " and traffic to your app from Google Search.",
Category.USABILITY, 5, Severity.WARNING, URL_IMPLEMENTATION)
.addMoreInfo("https://g.co/AppIndexing/AndroidStudio");
public static final Issue ISSUE_APP_INDEXING_API =
Issue.create(
"GoogleAppIndexingApiWarning", //$NON-NLS-1$
"Missing support for Google App Indexing Api",
"Adds URLs to get your app into the Google index, to get installs"
+ " and traffic to your app from Google Search.",
Category.USABILITY, 5, Severity.WARNING, APP_INDEXING_API_IMPLEMENTATION)
.addMoreInfo("https://g.co/AppIndexing/AndroidStudio")
.setEnabledByDefault(false);
private static final String[] PATH_ATTR_LIST = new String[]{ATTR_PATH_PREFIX, ATTR_PATH};
private static final String SCHEME_MISSING = "android:scheme is missing";
private static final String HOST_MISSING = "android:host is missing";
private static final String DATA_MISSING = "Missing data element";
private static final String URL_MISSING = "Missing URL for the intent filter";
private static final String NOT_BROWSABLE
= "Activity supporting ACTION_VIEW is not set as BROWSABLE";
private static final String ILLEGAL_NUMBER = "android:port is not a legal number";
private static final String APP_INDEX_START = "start"; //$NON-NLS-1$
private static final String APP_INDEX_END = "end"; //$NON-NLS-1$
private static final String APP_INDEX_VIEW = "view"; //$NON-NLS-1$
private static final String APP_INDEX_VIEW_END = "viewEnd"; //$NON-NLS-1$
private static final String CLIENT_CONNECT = "connect"; //$NON-NLS-1$
private static final String CLIENT_DISCONNECT = "disconnect"; //$NON-NLS-1$
private static final String ADD_API = "addApi"; //$NON-NLS-1$
private static final String APP_INDEXING_API_CLASS
= "com.google.android.gms.appindexing.AppIndexApi";
private static final String GOOGLE_API_CLIENT_CLASS
= "com.google.android.gms.common.api.GoogleApiClient";
private static final String GOOGLE_API_CLIENT_BUILDER_CLASS
= "com.google.android.gms.common.api.GoogleApiClient.Builder";
private static final String API_CLASS = "com.google.android.gms.appindexing.AppIndex";
public enum IssueType {
SCHEME_MISSING(AppIndexingApiDetector.SCHEME_MISSING),
HOST_MISSING(AppIndexingApiDetector.HOST_MISSING),
DATA_MISSING(AppIndexingApiDetector.DATA_MISSING),
URL_MISSING(AppIndexingApiDetector.URL_MISSING),
NOT_BROWSABLE(AppIndexingApiDetector.NOT_BROWSABLE),
ILLEGAL_NUMBER(AppIndexingApiDetector.ILLEGAL_NUMBER),
EMPTY_FIELD("cannot be empty"),
MISSING_SLASH("attribute should start with '/'"),
UNKNOWN("unknown error type");
private final String message;
IssueType(String str) {
this.message = str;
}
public static IssueType parse(String str) {
for (IssueType type : IssueType.values()) {
if (str.contains(type.message)) {
return type;
}
}
return UNKNOWN;
}
}
// ---- Implements XmlScanner ----
@Override
@Nullable
public Collection<String> getApplicableElements() {
return Collections.singletonList(NODE_APPLICATION);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element application) {
List<Element> activities = extractChildrenByName(application, NODE_ACTIVITY);
boolean applicationHasActionView = false;
for (Element activity : activities) {
List<Element> intents = extractChildrenByName(activity, NODE_INTENT);
boolean activityHasActionView = false;
for (Element intent : intents) {
boolean actionView = hasActionView(intent);
if (actionView) {
activityHasActionView = true;
}
visitIntent(context, intent);
}
if (activityHasActionView) {
applicationHasActionView = true;
if (activity.hasAttributeNS(ANDROID_URI, ATTR_EXPORTED)) {
Attr exported = activity.getAttributeNodeNS(ANDROID_URI, ATTR_EXPORTED);
if (!exported.getValue().equals("true")) {
// Report error if the activity supporting action view is not exported.
context.report(ISSUE_URL_ERROR, activity,
context.getLocation(activity),
"Activity supporting ACTION_VIEW is not exported");
}
}
}
}
if (!applicationHasActionView && !context.getProject().isLibrary()) {
// Report warning if there is no activity that supports action view.
context.report(ISSUE_APP_INDEXING, application, context.getLocation(application),
// This error message is more verbose than the other app indexing lint warnings, because it
// shows up on a blank project, and we want to make it obvious by just looking at the error
// message what this is
"App is not indexable by Google Search; consider adding at least one Activity with an ACTION-VIEW " +
"intent filter. See issue explanation for more details.");
}
}
@Nullable
@Override
public List<String> applicableSuperClasses() {
return Collections.singletonList(CLASS_ACTIVITY);
}
@Override
public void checkClass(@NonNull JavaContext context, @NonNull PsiClass declaration) {
if (declaration.getName() == null) {
return;
}
// In case linting the base class itself.
if (!context.getEvaluator().extendsClass(declaration, CLASS_ACTIVITY, true)) {
return;
}
declaration.accept(new MethodVisitor(context, declaration));
}
static class MethodVisitor extends JavaRecursiveElementVisitor {
private final JavaContext mContext;
private final PsiClass mCls;
private final List<PsiMethodCallExpression> mStartMethods;
private final List<PsiMethodCallExpression> mEndMethods;
private final List<PsiMethodCallExpression> mConnectMethods;
private final List<PsiMethodCallExpression> mDisconnectMethods;
private boolean mHasAddAppIndexApi;
MethodVisitor(JavaContext context, PsiClass cls) {
mCls = cls;
mContext = context;
mStartMethods = Lists.newArrayListWithExpectedSize(2);
mEndMethods = Lists.newArrayListWithExpectedSize(2);
mConnectMethods = Lists.newArrayListWithExpectedSize(2);
mDisconnectMethods = Lists.newArrayListWithExpectedSize(2);
}
@Override
public void visitClass(PsiClass aClass) {
if (aClass == mCls) {
super.visitClass(aClass);
report();
} // else: don't go into inner classes
}
@Override
public void visitMethodCallExpression(PsiMethodCallExpression node) {
super.visitMethodCallExpression(node);
String methodName = node.getMethodExpression().getReferenceName();
if (methodName == null) {
return;
}
JavaEvaluator evaluator = mContext.getEvaluator();
if (methodName.equals(APP_INDEX_START)) {
if (evaluator.isMemberInClass(node.resolveMethod(), APP_INDEXING_API_CLASS)) {
mStartMethods.add(node);
}
} else if (methodName.equals(APP_INDEX_END)) {
if (evaluator.isMemberInClass(node.resolveMethod(), APP_INDEXING_API_CLASS)) {
mEndMethods.add(node);
}
} else if (methodName.equals(APP_INDEX_VIEW)) {
if (evaluator.isMemberInClass(node.resolveMethod(), APP_INDEXING_API_CLASS)) {
mStartMethods.add(node);
}
} else if (methodName.equals(APP_INDEX_VIEW_END)) {
if (evaluator.isMemberInClass(node.resolveMethod(), APP_INDEXING_API_CLASS)) {
mEndMethods.add(node);
}
} else if (methodName.equals(CLIENT_CONNECT)) {
if (evaluator.isMemberInClass(node.resolveMethod(), GOOGLE_API_CLIENT_CLASS)) {
mConnectMethods.add(node);
}
} else if (methodName.equals(CLIENT_DISCONNECT)) {
if (evaluator.isMemberInClass(node.resolveMethod(), GOOGLE_API_CLIENT_CLASS)) {
mDisconnectMethods.add(node);
}
} else if (methodName.equals(ADD_API)) {
if (evaluator.isMemberInClass(node.resolveMethod(), GOOGLE_API_CLIENT_BUILDER_CLASS)) {
PsiExpression[] args = node.getArgumentList().getExpressions();
if (args.length > 0) {
PsiElement resolved = evaluator.resolve(args[0]);
if (resolved instanceof PsiField &&
evaluator.isMemberInClass((PsiField) resolved, API_CLASS)) {
mHasAddAppIndexApi = true;
}
}
}
}
}
@Override
public void visitAnonymousClass(PsiAnonymousClass aClass) {
// Don't jump into inner classes
}
private void report() {
// finds the activity classes that need app activity annotation
Set<String> activitiesToCheck = getActivitiesToCheck(mContext);
// app indexing API used but no support in manifest
boolean hasIntent = activitiesToCheck.contains(mCls.getQualifiedName());
if (!hasIntent) {
for (PsiMethodCallExpression call : mStartMethods) {
mContext.report(ISSUE_APP_INDEXING_API, call,
mContext.getNameLocation(call),
"Missing support for Google App Indexing in the manifest");
}
for (PsiMethodCallExpression call : mEndMethods) {
mContext.report(ISSUE_APP_INDEXING_API, call,
mContext.getNameLocation(call),
"Missing support for Google App Indexing in the manifest");
}
return;
}
// `AppIndex.AppIndexApi.start / end / view / viewEnd` should exist
if (mStartMethods.isEmpty() && mEndMethods.isEmpty()) {
mContext.report(ISSUE_APP_INDEXING_API, mCls,
mContext.getNameLocation(mCls),
"Missing support for Google App Indexing API");
return;
}
for (PsiMethodCallExpression startNode : mStartMethods) {
PsiExpression[] expressions = startNode.getArgumentList().getExpressions();
if (expressions.length == 0) {
continue;
}
PsiExpression startClient = expressions[0];
// GoogleApiClient should `addApi(AppIndex.APP_INDEX_API)`
if (!mHasAddAppIndexApi) {
String message = String.format(
"GoogleApiClient `%1$s` has not added support for App Indexing API",
startClient.getText());
mContext.report(ISSUE_APP_INDEXING_API, startClient,
mContext.getLocation(startClient), message);
}
// GoogleApiClient `connect` should exist
if (!hasOperand(startClient, mConnectMethods)) {
String message = String.format("GoogleApiClient `%1$s` is not connected",
startClient.getText());
mContext.report(ISSUE_APP_INDEXING_API, startClient,
mContext.getLocation(startClient), message);
}
// `AppIndex.AppIndexApi.end` should pair with `AppIndex.AppIndexApi.start`
if (!hasFirstArgument(startClient, mEndMethods)) {
mContext.report(ISSUE_APP_INDEXING_API, startNode,
mContext.getNameLocation(startNode),
"Missing corresponding `AppIndex.AppIndexApi.end` method");
}
}
for (PsiMethodCallExpression endNode : mEndMethods) {
PsiExpression[] expressions = endNode.getArgumentList().getExpressions();
if (expressions.length == 0) {
continue;
}
PsiExpression endClient = expressions[0];
// GoogleApiClient should `addApi(AppIndex.APP_INDEX_API)`
if (!mHasAddAppIndexApi) {
String message = String.format(
"GoogleApiClient `%1$s` has not added support for App Indexing API",
endClient.getText());
mContext.report(ISSUE_APP_INDEXING_API, endClient,
mContext.getLocation(endClient), message);
}
// GoogleApiClient `disconnect` should exist
if (!hasOperand(endClient, mDisconnectMethods)) {
String message = String.format("GoogleApiClient `%1$s`"
+ " is not disconnected", endClient.getText());
mContext.report(ISSUE_APP_INDEXING_API, endClient,
mContext.getLocation(endClient), message);
}
// `AppIndex.AppIndexApi.start` should pair with `AppIndex.AppIndexApi.end`
if (!hasFirstArgument(endClient, mStartMethods)) {
mContext.report(ISSUE_APP_INDEXING_API, endNode,
mContext.getNameLocation(endNode),
"Missing corresponding `AppIndex.AppIndexApi.start` method");
}
}
}
}
/**
* Gets names of activities which needs app indexing. i.e. the activities have data tag in their
* intent filters.
* TODO: Cache the activities to speed up batch lint.
*
* @param context The context to check in.
*/
private static Set<String> getActivitiesToCheck(Context context) {
Set<String> activitiesToCheck = Sets.newHashSet();
List<File> manifestFiles = context.getProject().getManifestFiles();
XmlParser xmlParser = context.getDriver().getClient().getXmlParser();
if (xmlParser != null) {
// TODO: Avoid visit all manifest files before enable this check by default.
for (File manifest : manifestFiles) {
XmlContext xmlContext =
new XmlContext(context.getDriver(), context.getProject(),
null, manifest, null, xmlParser);
Document doc = xmlParser.parseXml(xmlContext);
if (doc != null) {
List<Element> children = LintUtils.getChildren(doc);
for (Element child : children) {
if (child.getNodeName().equals(NODE_MANIFEST)) {
List<Element> apps = extractChildrenByName(child, NODE_APPLICATION);
for (Element app : apps) {
List<Element> acts = extractChildrenByName(app, NODE_ACTIVITY);
for (Element act : acts) {
List<Element> intents = extractChildrenByName(act, NODE_INTENT);
for (Element intent : intents) {
List<Element> data = extractChildrenByName(intent,
NODE_DATA);
if (!data.isEmpty() && act.hasAttributeNS(
ANDROID_URI, ATTRIBUTE_NAME)) {
Attr attr = act.getAttributeNodeNS(
ANDROID_URI, ATTRIBUTE_NAME);
String activityName = attr.getValue();
int dotIndex = activityName.indexOf('.');
if (dotIndex <= 0) {
String pkg = context.getMainProject().getPackage();
if (pkg != null) {
if (dotIndex == 0) {
activityName = pkg + activityName;
}
else {
activityName = pkg + '.' + activityName;
}
}
}
activitiesToCheck.add(activityName);
}
}
}
}
}
}
}
}
}
return activitiesToCheck;
}
private static void visitIntent(@NonNull XmlContext context, @NonNull Element intent) {
boolean actionView = hasActionView(intent);
boolean browsable = isBrowsable(intent);
boolean isHttp = false;
boolean hasScheme = false;
boolean hasHost = false;
boolean hasPort = false;
boolean hasPath = false;
boolean hasMimeType = false;
Element firstData = null;
List<Element> children = extractChildrenByName(intent, NODE_DATA);
for (Element data : children) {
if (firstData == null) {
firstData = data;
}
if (isHttpSchema(data)) {
isHttp = true;
}
checkSingleData(context, data);
for (String name : PATH_ATTR_LIST) {
if (data.hasAttributeNS(ANDROID_URI, name)) {
hasPath = true;
}
}
if (data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) {
hasScheme = true;
}
if (data.hasAttributeNS(ANDROID_URI, ATTR_HOST)) {
hasHost = true;
}
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_PORT)) {
hasPort = true;
}
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_MIME_TYPE)) {
hasMimeType = true;
}
}
// In data field, a URL is consisted by
// <scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>]
// Each part of the URL should not have illegal character.
if ((hasPath || hasHost || hasPort) && !hasScheme) {
context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData),
SCHEME_MISSING);
}
if ((hasPath || hasPort) && !hasHost) {
context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData),
HOST_MISSING);
}
if (actionView && browsable) {
if (firstData == null) {
// If this activity is an ACTION_VIEW action with category BROWSABLE, but doesn't
// have data node, it may be a mistake and we will report error.
context.report(ISSUE_URL_ERROR, intent, context.getLocation(intent),
DATA_MISSING);
} else if (!hasScheme && !hasMimeType) {
// If this activity is an action view, is browsable, but has neither a
// URL nor mimeType, it may be a mistake and we will report error.
context.report(ISSUE_URL_ERROR, firstData, context.getLocation(firstData),
URL_MISSING);
}
}
// If this activity is an ACTION_VIEW action, has a http URL but doesn't have
// BROWSABLE, it may be a mistake and and we will report warning.
if (actionView && isHttp && !browsable) {
context.report(ISSUE_APP_INDEXING, intent, context.getLocation(intent),
NOT_BROWSABLE);
}
if (actionView && !hasScheme) {
context.report(ISSUE_APP_INDEXING, intent, context.getLocation(intent),
"Missing URL");
}
}
/**
* Check if the intent filter supports action view.
*
* @param intent the intent filter
* @return true if it does
*/
private static boolean hasActionView(@NonNull Element intent) {
List<Element> children = extractChildrenByName(intent, NODE_ACTION);
for (Element action : children) {
if (action.hasAttributeNS(ANDROID_URI, ATTRIBUTE_NAME)) {
Attr attr = action.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_NAME);
if (attr.getValue().equals("android.intent.action.VIEW")) {
return true;
}
}
}
return false;
}
/**
* Check if the intent filter is browsable.
*
* @param intent the intent filter
* @return true if it does
*/
private static boolean isBrowsable(@NonNull Element intent) {
List<Element> children = extractChildrenByName(intent, NODE_CATEGORY);
for (Element e : children) {
if (e.hasAttributeNS(ANDROID_URI, ATTRIBUTE_NAME)) {
Attr attr = e.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_NAME);
if (attr.getNodeValue().equals("android.intent.category.BROWSABLE")) {
return true;
}
}
}
return false;
}
/**
* Check if the data node contains http schema
*
* @param data the data node
* @return true if it does
*/
private static boolean isHttpSchema(@NonNull Element data) {
if (data.hasAttributeNS(ANDROID_URI, ATTR_SCHEME)) {
String value = data.getAttributeNodeNS(ANDROID_URI, ATTR_SCHEME).getValue();
if (value.equalsIgnoreCase("http") || value.equalsIgnoreCase("https")) {
return true;
}
}
return false;
}
private static void checkSingleData(@NonNull XmlContext context, @NonNull Element data) {
// path, pathPrefix and pathPattern should starts with /.
for (String name : PATH_ATTR_LIST) {
if (data.hasAttributeNS(ANDROID_URI, name)) {
Attr attr = data.getAttributeNodeNS(ANDROID_URI, name);
String path = replaceUrlWithValue(context, attr.getValue());
if (!path.startsWith("/") && !path.startsWith(SdkConstants.PREFIX_RESOURCE_REF)) {
context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr),
"android:" + name + " attribute should start with '/', but it is : "
+ path);
}
}
}
// port should be a legal number.
if (data.hasAttributeNS(ANDROID_URI, ATTRIBUTE_PORT)) {
Attr attr = data.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_PORT);
try {
String port = replaceUrlWithValue(context, attr.getValue());
Integer.parseInt(port);
} catch (NumberFormatException e) {
context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr),
ILLEGAL_NUMBER);
}
}
// Each field should be non empty.
NamedNodeMap attrs = data.getAttributes();
for (int i = 0; i < attrs.getLength(); i++) {
Node item = attrs.item(i);
if (item.getNodeType() == Node.ATTRIBUTE_NODE) {
Attr attr = (Attr) attrs.item(i);
if (attr.getValue().isEmpty()) {
context.report(ISSUE_URL_ERROR, attr, context.getLocation(attr),
attr.getName() + " cannot be empty");
}
}
}
}
private static String replaceUrlWithValue(@NonNull XmlContext context,
@NonNull String str) {
Project project = context.getProject();
LintClient client = context.getClient();
if (!client.supportsProjectResources()) {
return str;
}
ResourceUrl style = ResourceUrl.parse(str);
if (style == null || style.type != ResourceType.STRING || style.framework) {
return str;
}
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources == null) {
return str;
}
List<ResourceItem> items = resources.getResourceItem(ResourceType.STRING, style.name);
if (items == null || items.isEmpty()) {
return str;
}
ResourceValue resourceValue = items.get(0).getResourceValue(false);
if (resourceValue == null) {
return str;
}
return resourceValue.getValue() == null ? str : resourceValue.getValue();
}
/**
* If a method with a certain argument exists in the list of methods.
*
* @param argument The first argument of the method.
* @param list The methods list.
* @return If such a method exists in the list.
*/
private static boolean hasFirstArgument(PsiExpression argument, List<PsiMethodCallExpression> list) {
for (PsiMethodCallExpression call : list) {
PsiExpression[] expressions = call.getArgumentList().getExpressions();
if (expressions.length > 0) {
PsiExpression argument2 = expressions[0];
if (argument.getText().equals(argument2.getText())) {
return true;
}
}
}
return false;
}
/**
* If a method with a certain operand exists in the list of methods.
*
* @param operand The operand of the method.
* @param list The methods list.
* @return If such a method exists in the list.
*/
private static boolean hasOperand(PsiExpression operand, List<PsiMethodCallExpression> list) {
for (PsiMethodCallExpression method : list) {
PsiElement operand2 = method.getMethodExpression().getQualifier();
if (operand2 != null && operand.getText().equals(operand2.getText())) {
return true;
}
}
return false;
}
private static List<Element> extractChildrenByName(@NonNull Element node,
@NonNull String name) {
List<Element> result = Lists.newArrayList();
List<Element> children = LintUtils.getChildren(node);
for (Element child : children) {
if (child.getNodeName().equals(name)) {
result.add(child);
}
}
return result;
}
}