blob: 54aa711c710f21bff2f9bcfcca444bbc2089398a [file] [log] [blame]
/*
* Copyright (C) 2017 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_NAME;
import static com.android.SdkConstants.ATTR_TARGET_SDK_VERSION;
import static com.android.SdkConstants.TAG_ACTIVITY;
import static com.android.SdkConstants.TAG_APPLICATION;
import static com.android.SdkConstants.TAG_PROVIDER;
import static com.android.SdkConstants.TAG_RECEIVER;
import static com.android.SdkConstants.TAG_SERVICE;
import static com.android.SdkConstants.TAG_USES_PERMISSION;
import static com.android.SdkConstants.TAG_USES_SDK;
import static com.android.utils.BuildScriptUtil.findGradleBuildFile;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.repository.GradleVersion;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.SdkVersionInfo;
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.ConstantEvaluator;
import com.android.tools.lint.detector.api.Context;
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.Location;
import com.android.tools.lint.detector.api.Project;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.SourceCodeScanner;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.tools.lint.model.LintModelModule;
import com.android.tools.lint.model.LintModelModuleType;
import com.android.utils.Pair;
import com.android.utils.XmlUtils;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMethod;
import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
import org.jetbrains.uast.UCallExpression;
import org.jetbrains.uast.UElement;
import org.jetbrains.uast.UExpression;
import org.jetbrains.uast.UReferenceExpression;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* Checks related to instant apps. FIXME: This needs to be refactored to support the feature plugin.
*/
public class InstantAppDetector extends ResourceXmlDetector implements SourceCodeScanner {
@SuppressWarnings("unchecked")
public static final Implementation IMPLEMENTATION =
new Implementation(
InstantAppDetector.class,
EnumSet.of(Scope.MANIFEST, Scope.JAVA_FILE),
Scope.MANIFEST_SCOPE,
Scope.JAVA_FILE_SCOPE);
/** Instant App related issues */
public static final Issue ISSUE =
Issue.create(
"InstantApps",
"Instant App Issues",
// TODO: Need full explanation here
"This issue flags code that will not work correctly in Instant Apps",
Category.CORRECTNESS,
5,
Severity.WARNING,
IMPLEMENTATION);
/** Constructs a new {@link InstantAppDetector} */
public InstantAppDetector() {}
/**
* Checks whether the source is part of an Instant App module -- or a module included from an
* instant app module
*/
private static boolean isInstantApp(@NonNull Context context) {
Project mainProject = context.getMainProject();
LintModelModule model = mainProject.getBuildModule();
if (model == null) {
return false;
}
GradleVersion modelVersion = mainProject.getGradleModelVersion();
if (modelVersion == null) {
return false;
}
if (!modelVersion.isAtLeast(2, 4, 0, "alpha", 1, false)) {
return false;
}
if (isInstantApp(mainProject)) {
return true;
}
Project project = context.getProject();
return project != mainProject && isInstantApp(project);
}
/** Checks whether the given project is an instant app module */
private static boolean isInstantApp(@NonNull Project project) {
return project.getType() == LintModelModuleType.INSTANT_APP;
}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TAG_PROVIDER, TAG_RECEIVER, TAG_SERVICE, TAG_USES_SDK, TAG_USES_PERMISSION);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String tag = element.getTagName();
switch (tag) {
case TAG_PROVIDER:
case TAG_RECEIVER:
case TAG_SERVICE:
{
report(
ISSUE,
context,
element,
"Instant Apps are not allowed to export services, receivers, "
+ "and providers");
break;
}
case TAG_USES_SDK:
{
Attr targetSdkVersionNode =
element.getAttributeNodeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION);
if (targetSdkVersionNode != null) {
String target = targetSdkVersionNode.getValue();
AndroidVersion version = SdkVersionInfo.getVersion(target, null);
if (version != null && version.getFeatureLevel() < 23) {
report(
ISSUE,
context,
targetSdkVersionNode,
"Instant Apps must target API 23+");
}
}
break;
}
case TAG_USES_PERMISSION:
{
String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
switch (name) {
case "android.permission.ACCESS_NETWORK_STATE":
case "android.permission.ACCESS_WIFI_STATE":
case "android.permission.INTERNET":
case "android.permission.WAKE_LOCK":
case "android.permission.VIBRATE":
case "android.permission.ACCESS_COARSE_LOCATION":
case "android.permission.ACCESS_FINE_LOCATION":
case "android.permission.RECORD_AUDIO":
// Still being considered: READ_GSERVICES, GET_ACCOUNTS
return;
default:
if (!name.startsWith("android.permission.")) {
if (name.endsWith(".permission.C2D_MESSAGE")) {
report(
ISSUE,
context,
element,
"Instant Apps are not allowed to use Google Cloud "
+ "Messaging (GCM)");
}
return;
}
report(
ISSUE,
context,
element,
"This permission is not allowed for Instant Apps");
}
break;
}
}
}
@Override
public void afterCheckRootProject(@NonNull Context context) {
if (context.getProject() != context.getMainProject()) {
return;
}
// Check whether there is a merged targetSdkVersion that is lower than 23. If so
// report it.
if (!isInstantApp(context)) {
return;
}
Project project = context.getMainProject();
Document mergedManifest = project.getMergedManifest();
if (mergedManifest == null) {
return;
}
Element root = mergedManifest.getDocumentElement();
if (root == null) {
return;
}
checkMultipleLauncherActivities(context, root);
if (context.getScope().contains(Scope.GRADLE_FILE)) {
checkMergedTargetSdkVersion(context, project, root);
}
}
private static void checkMultipleLauncherActivities(@NonNull Context context, Element root) {
Element application = XmlUtils.getFirstSubTagByName(root, TAG_APPLICATION);
if (application == null) {
return;
}
Element activity = XmlUtils.getFirstSubTagByName(application, TAG_ACTIVITY);
Element launchableActivity = null;
while (activity != null) {
if (ManifestDetector.isLaunchableActivity(activity)) {
if (launchableActivity == null) {
// First one
launchableActivity = activity;
} else {
// More than one found: complain
LintClient client = context.getClient();
Pair<File, ? extends Node> source = client.findManifestSourceNode(activity);
if (source != null) {
XmlParser parser = client.getXmlParser();
// Don't search for the category tag directly; the manifest merger
// does not create a unique key for it across activities, so we end
// up always returning the first one. If we instead search for the
// activity, we'll get the correct unique node, and then we can simply
// search for the category node under the source node.
Node locationNode =
ManifestDetector.findLaunchableCategoryNode(
(Element) source.getSecond());
if (locationNode == null) {
locationNode = activity;
}
Location location = parser.getLocation(source.getFirst(), locationNode);
Pair<File, ? extends Node> original =
client.findManifestSourceNode(launchableActivity);
if (original != null && original.getSecond() != source.getSecond()) {
locationNode =
ManifestDetector.findLaunchableCategoryNode(
(Element) original.getSecond());
if (locationNode == null) {
locationNode = original.getSecond();
}
Location secondary =
parser.getLocation(original.getFirst(), locationNode);
secondary.setMessage("Other launchable activity here");
location.setSecondary(secondary);
}
context.report(
ISSUE,
location,
"Instant Apps are not allowed to have multiple "
+ "launchable activities");
}
}
}
activity = XmlUtils.getNextTagByName(activity, TAG_ACTIVITY);
}
}
private static void checkMergedTargetSdkVersion(
@NonNull Context context, Project project, Element root) {
// Look up targetSdkVersion from the merged manifest to make sure we also pick up
// on any Gradle-overrides
Element usesSdk = XmlUtils.getFirstSubTagByName(root, TAG_USES_SDK);
if (usesSdk == null) {
return;
}
Attr targetSdkVersionNode =
usesSdk.getAttributeNodeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION);
if (targetSdkVersionNode != null) {
String target = targetSdkVersionNode.getValue();
AndroidVersion version = SdkVersionInfo.getVersion(target, null);
if (version != null && version.getFeatureLevel() < 23) {
File dir = project.getDir();
File gradle = project.isGradleProject() ? findGradleBuildFile(dir) : null;
Location location =
Location.create(gradle != null && gradle.isFile() ? gradle : dir);
context.report(
ISSUE, location, "Instant Apps must target API 23+ (was " + version + ")");
}
}
}
@Nullable
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList(
"notify", "registerReceiver", "getMacAddress", "getAddress", "getLong");
}
@Override
public void visitMethodCall(
@NonNull JavaContext context,
@NonNull UCallExpression call,
@NonNull PsiMethod method) {
JavaEvaluator evaluator = context.getEvaluator();
switch (method.getName()) {
case "notify":
if (evaluator.isMemberInClass(method, "android.app.NotificationManager")) {
report(
ISSUE,
context,
call,
"Instant Apps are not allowed to create notifications");
}
break;
case "registerReceiver":
if (evaluator.isMemberInClass(method, "android.content.Context")) {
report(
ISSUE,
context,
call,
"Instant Apps are not allowed to listen to broadcasts from "
+ "system or other apps");
}
break;
case "getMacAddress":
if (evaluator.isMemberInClass(method, "android.net.wifi.WifiInfo")) {
report(ISSUE, context, call, getPlaceHolderError("Mac Addresses"));
}
break;
case "getAddress":
if (evaluator.isMemberInClass(method, "android.bluetooth.BluetoothAdapter")) {
report(ISSUE, context, call, getPlaceHolderError("Mac Addresses"));
}
break;
case "getLong":
if (evaluator.isMemberInClass(method, "com.google.android.gsf.Gservices")) {
List<UExpression> arguments = call.getValueArguments();
if (arguments.size() == 3) {
Object key = ConstantEvaluator.evaluate(context, arguments.get(1));
if ("android_id".equals(key)) {
report(ISSUE, context, call, getPlaceHolderError("Android Id"));
}
}
}
break;
default:
assert false;
}
}
private static String getPlaceHolderError(String type) {
return "Instant Apps accessing \"" + type + "\" will get a XXX value";
}
@Nullable
@Override
public List<String> getApplicableReferenceNames() {
return Arrays.asList("SERIAL", "ANDROID_ID");
}
@Override
public void visitReference(
@NonNull JavaContext context,
@NonNull UReferenceExpression reference,
@NonNull PsiElement referenced) {
if (!(referenced instanceof PsiField)) {
return;
}
PsiClass containingClass = ((PsiField) referenced).getContainingClass();
if (containingClass == null) {
return;
}
String qualifiedName = containingClass.getQualifiedName();
if (qualifiedName == null) {
return;
}
switch (qualifiedName) {
case "android.os.Build":
if ("SERIAL".equals(reference.getResolvedName())) {
report(ISSUE, context, reference, getPlaceHolderError("Build Serial"));
}
break;
case "android.provider.Settings.Secure":
if ("ANDROID_ID".equals(reference.getResolvedName())) {
report(
ISSUE,
context,
reference,
getPlaceHolderError("Settings.Secure Android Id"));
}
break;
}
}
private static void report(
@NonNull Issue issue,
@NonNull XmlContext context,
@NonNull Node node,
@NonNull String message) {
if (isInstantApp(context)) {
context.report(issue, node, context.getLocation(node), message);
}
}
private static void report(
@NonNull Issue issue,
@NonNull JavaContext context,
@NonNull UElement element,
@NonNull String message) {
if (isInstantApp(context)) {
context.report(issue, element, context.getLocation(element), message);
}
}
}