Merge "Annotations for Bluetooth broadcast intents." into sc-dev am: d4b3fcac16

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14253645

Change-Id: I9a999a6483a3a2ec2ec7976a27a4505cb70b63bf
This commit is contained in:
Jeff Sharkey
2021-04-21 22:46:51 +00:00
committed by Automerger Merge Worker
26 changed files with 515 additions and 52 deletions

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2008 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 android.annotation;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Indicates a constant field value should be exported to be used in the SDK tools.
* @hide
*/
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.SOURCE)
public @interface SdkConstant {
public static enum SdkConstantType {
ACTIVITY_INTENT_ACTION, BROADCAST_INTENT_ACTION, SERVICE_ACTION, INTENT_CATEGORY, FEATURE;
}
SdkConstantType value();
}

View File

@@ -26,40 +26,49 @@ import static com.google.errorprone.matchers.Matchers.methodInvocation;
import static com.google.errorprone.matchers.Matchers.methodIsNamed;
import static com.google.errorprone.matchers.Matchers.staticMethod;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import com.google.auto.service.AutoService;
import com.google.common.base.Objects;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreeScanner;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Type.ClassType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
/**
* Inspects both the client and server side of AIDL interfaces to ensure that
@@ -71,9 +80,8 @@ import javax.lang.model.element.ExecutableElement;
name = "AndroidFrameworkRequiresPermission",
summary = "Verifies that @RequiresPermission annotations are consistent across AIDL",
severity = WARNING)
public final class RequiresPermissionChecker extends BugChecker implements MethodTreeMatcher {
private static final String ANNOTATION_REQUIRES_PERMISSION = "RequiresPermission";
public final class RequiresPermissionChecker extends BugChecker
implements MethodTreeMatcher, MethodInvocationTreeMatcher {
private static final Matcher<ExpressionTree> ENFORCE_VIA_CONTEXT = methodInvocation(
instanceMethod()
.onDescendantOf("android.content.Context")
@@ -110,6 +118,18 @@ public final class RequiresPermissionChecker extends BugChecker implements Metho
private static final Matcher<ExpressionTree> RESTORE_CALL = methodInvocation(staticMethod()
.onClass("android.os.Binder").withSignature("restoreCallingIdentity(long)"));
private static final Matcher<ExpressionTree> SEND_BROADCAST = methodInvocation(
instanceMethod()
.onDescendantOf("android.content.Context")
.withNameMatching(Pattern.compile("^send(Ordered|Sticky)?Broadcast.*$")));
private static final Matcher<ExpressionTree> SEND_PENDING_INTENT = methodInvocation(
instanceMethod()
.onDescendantOf("android.app.PendingIntent")
.named("send"));
private static final Matcher<ExpressionTree> INTENT_SET_ACTION = methodInvocation(
instanceMethod().onDescendantOf("android.content.Intent").named("setAction"));
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
// Ignore methods without an implementation
@@ -166,7 +186,7 @@ public final class RequiresPermissionChecker extends BugChecker implements Metho
// to require; yell if we're too broad
if (!actualPerm.containsAll(expectedPerm)) {
return buildDescription(tree)
.setMessage("Method " + method.name.toString() + "() annotated " + expectedPerm
.setMessage("Method " + method.name.toString() + "() annotated " + expectedPerm
+ " but too wide; only invokes methods requiring " + actualPerm)
.build();
}
@@ -174,6 +194,27 @@ public final class RequiresPermissionChecker extends BugChecker implements Metho
return Description.NO_MATCH;
}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (SEND_BROADCAST.matches(tree, state)) {
final ParsedRequiresPermission sourcePerm =
parseBroadcastSourceRequiresPermission(tree, state);
final ParsedRequiresPermission targetPerm =
parseBroadcastTargetRequiresPermission(tree, state);
if (sourcePerm == null) {
return buildDescription(tree)
.setMessage("Failed to resolve broadcast intent action for validation")
.build();
} else if (!Objects.equal(sourcePerm, targetPerm)) {
return buildDescription(tree)
.setMessage("Broadcast annotated " + sourcePerm + " but protected with "
+ targetPerm)
.build();
}
}
return Description.NO_MATCH;
}
static class ParsedRequiresPermission {
final Set<String> allOf = new HashSet<>();
final Set<String> anyOf = new HashSet<>();
@@ -202,6 +243,16 @@ public final class RequiresPermissionChecker extends BugChecker implements Metho
return allMet && anyMet;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ParsedRequiresPermission) {
final ParsedRequiresPermission other = (ParsedRequiresPermission) obj;
return allOf.equals(other.allOf) && anyOf.equals(other.anyOf);
} else {
return false;
}
}
@Override
public String toString() {
if (isEmpty()) {
@@ -215,39 +266,145 @@ public final class RequiresPermissionChecker extends BugChecker implements Metho
return res;
}
public static ParsedRequiresPermission from(RequiresPermission perm) {
final ParsedRequiresPermission res = new ParsedRequiresPermission();
res.addAll(perm);
return res;
}
public void addAll(ParsedRequiresPermission perm) {
if (perm == null) return;
this.allOf.addAll(perm.allOf);
this.anyOf.addAll(perm.anyOf);
}
public void addAll(AnnotationMirror a) {
for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : a
.getElementValues().entrySet()) {
if (entry.getKey().getSimpleName().contentEquals("value")) {
maybeAdd(allOf, entry.getValue());
} else if (entry.getKey().getSimpleName().contentEquals("allOf")) {
maybeAdd(allOf, entry.getValue());
} else if (entry.getKey().getSimpleName().contentEquals("anyOf")) {
maybeAdd(anyOf, entry.getValue());
}
}
public void addAll(RequiresPermission perm) {
if (perm == null) return;
if (!perm.value().isEmpty()) this.allOf.add(perm.value());
if (perm.allOf() != null) this.allOf.addAll(Arrays.asList(perm.allOf()));
if (perm.anyOf() != null) this.anyOf.addAll(Arrays.asList(perm.anyOf()));
}
private static void maybeAdd(Set<String> set, Object value) {
if (value instanceof AnnotationValue) {
maybeAdd(set, ((AnnotationValue) value).getValue());
} else if (value instanceof String) {
set.add((String) value);
} else if (value instanceof Collection) {
for (Object o : (Collection) value) {
maybeAdd(set, o);
}
} else {
throw new RuntimeException(String.valueOf(value.getClass()));
public void addConstValue(Tree tree) {
final Object value = ASTHelpers.constValue(tree);
if (value != null) {
allOf.add(String.valueOf(value));
}
}
}
private static ExpressionTree findArgumentByParameterName(MethodInvocationTree tree,
Predicate<String> paramName) {
final MethodSymbol sym = ASTHelpers.getSymbol(tree);
final List<VarSymbol> params = sym.getParameters();
for (int i = 0; i < params.size(); i++) {
if (paramName.test(params.get(i).name.toString())) {
return tree.getArguments().get(i);
}
}
return null;
}
private static Name resolveName(ExpressionTree tree) {
if (tree instanceof IdentifierTree) {
return ((IdentifierTree) tree).getName();
} else if (tree instanceof MemberSelectTree) {
return resolveName(((MemberSelectTree) tree).getExpression());
} else {
return null;
}
}
private static ParsedRequiresPermission parseIntentAction(NewClassTree tree) {
final Optional<? extends ExpressionTree> arg = tree.getArguments().stream().findFirst();
if (arg.isPresent()) {
return ParsedRequiresPermission.from(
ASTHelpers.getAnnotation(arg.get(), RequiresPermission.class));
} else {
return null;
}
}
private static ParsedRequiresPermission parseIntentAction(MethodInvocationTree tree) {
return ParsedRequiresPermission.from(ASTHelpers.getAnnotation(
tree.getArguments().get(0), RequiresPermission.class));
}
private static ParsedRequiresPermission parseBroadcastSourceRequiresPermission(
MethodInvocationTree methodTree, VisitorState state) {
final ExpressionTree arg = findArgumentByParameterName(methodTree,
(name) -> name.toLowerCase().contains("intent"));
if (arg instanceof IdentifierTree) {
final Name argName = ((IdentifierTree) arg).getName();
final MethodTree method = state.findEnclosing(MethodTree.class);
final AtomicReference<ParsedRequiresPermission> res = new AtomicReference<>();
method.accept(new TreeScanner<Void, Void>() {
private ParsedRequiresPermission last;
@Override
public Void visitMethodInvocation(MethodInvocationTree tree, Void param) {
if (Objects.equal(methodTree, tree)) {
res.set(last);
} else {
final Name name = resolveName(tree.getMethodSelect());
if (Objects.equal(argName, name)
&& INTENT_SET_ACTION.matches(tree, state)) {
last = parseIntentAction(tree);
}
}
return super.visitMethodInvocation(tree, param);
}
@Override
public Void visitAssignment(AssignmentTree tree, Void param) {
final Name name = resolveName(tree.getVariable());
final Tree init = tree.getExpression();
if (Objects.equal(argName, name)
&& init instanceof NewClassTree) {
last = parseIntentAction((NewClassTree) init);
}
return super.visitAssignment(tree, param);
}
@Override
public Void visitVariable(VariableTree tree, Void param) {
final Name name = tree.getName();
final ExpressionTree init = tree.getInitializer();
if (Objects.equal(argName, name)
&& init instanceof NewClassTree) {
last = parseIntentAction((NewClassTree) init);
}
return super.visitVariable(tree, param);
}
}, null);
return res.get();
}
return null;
}
private static ParsedRequiresPermission parseBroadcastTargetRequiresPermission(
MethodInvocationTree tree, VisitorState state) {
final ExpressionTree arg = findArgumentByParameterName(tree,
(name) -> name.toLowerCase().contains("permission"));
final ParsedRequiresPermission res = new ParsedRequiresPermission();
if (arg != null) {
arg.accept(new TreeScanner<Void, Void>() {
@Override
public Void visitIdentifier(IdentifierTree tree, Void param) {
res.addConstValue(tree);
return super.visitIdentifier(tree, param);
}
@Override
public Void visitMemberSelect(MemberSelectTree tree, Void param) {
res.addConstValue(tree);
return super.visitMemberSelect(tree, param);
}
}, null);
}
return res;
}
private static ParsedRequiresPermission parseRequiresPermissionRecursively(
MethodInvocationTree tree, VisitorState state) {
if (ENFORCE_VIA_CONTEXT.matches(tree, state)) {
@@ -276,12 +433,7 @@ public final class RequiresPermissionChecker extends BugChecker implements Metho
final ParsedRequiresPermission res = new ParsedRequiresPermission();
for (MethodSymbol symbol : symbols) {
for (AnnotationMirror a : symbol.getAnnotationMirrors()) {
if (a.getAnnotationType().asElement().getSimpleName()
.contentEquals(ANNOTATION_REQUIRES_PERMISSION)) {
res.addAll(a);
}
}
res.addAll(symbol.getAnnotation(RequiresPermission.class));
}
return res;
}

View File

@@ -38,6 +38,7 @@ public class ContextUserIdCheckerTest {
compilationHelper
.addSourceFile("/android/annotation/SystemService.java")
.addSourceFile("/android/content/Context.java")
.addSourceFile("/android/content/Intent.java")
.addSourceFile("/android/foo/IFooService.java")
.addSourceFile("/android/os/IInterface.java")
.addSourceFile("/android/os/RemoteException.java")
@@ -68,6 +69,7 @@ public class ContextUserIdCheckerTest {
compilationHelper
.addSourceFile("/android/annotation/SystemService.java")
.addSourceFile("/android/content/Context.java")
.addSourceFile("/android/content/Intent.java")
.addSourceFile("/android/foo/IFooService.java")
.addSourceFile("/android/os/IInterface.java")
.addSourceFile("/android/os/UserHandle.java")
@@ -98,6 +100,7 @@ public class ContextUserIdCheckerTest {
compilationHelper
.addSourceFile("/android/annotation/SystemService.java")
.addSourceFile("/android/content/Context.java")
.addSourceFile("/android/content/Intent.java")
.addSourceFile("/android/foo/IFooService.java")
.addSourceFile("/android/os/IInterface.java")
.addSourceFile("/android/os/UserHandle.java")
@@ -124,6 +127,7 @@ public class ContextUserIdCheckerTest {
compilationHelper
.addSourceFile("/android/annotation/SystemService.java")
.addSourceFile("/android/content/Context.java")
.addSourceFile("/android/content/Intent.java")
.addSourceFile("/android/foo/IFooService.java")
.addSourceFile("/android/os/IInterface.java")
.addSourceFile("/android/os/UserHandle.java")

View File

@@ -81,6 +81,7 @@ public class RequiresPermissionCheckerTest {
compilationHelper
.addSourceFile("/android/annotation/RequiresPermission.java")
.addSourceFile("/android/content/Context.java")
.addSourceFile("/android/content/Intent.java")
.addSourceLines("ColorManager.java",
"import android.annotation.RequiresPermission;",
"import android.content.Context;",
@@ -139,6 +140,7 @@ public class RequiresPermissionCheckerTest {
compilationHelper
.addSourceFile("/android/annotation/RequiresPermission.java")
.addSourceFile("/android/content/Context.java")
.addSourceFile("/android/content/Intent.java")
.addSourceFile("/android/foo/IColorService.java")
.addSourceFile("/android/os/IInterface.java")
.addSourceLines("ColorService.java",
@@ -322,4 +324,95 @@ public class RequiresPermissionCheckerTest {
"}")
.doTest();
}
@Test
public void testSendBroadcast() {
compilationHelper
.addSourceFile("/android/annotation/RequiresPermission.java")
.addSourceFile("/android/annotation/SdkConstant.java")
.addSourceFile("/android/content/Context.java")
.addSourceFile("/android/content/Intent.java")
.addSourceLines("FooManager.java",
"import android.annotation.RequiresPermission;",
"import android.annotation.SdkConstant;",
"import android.content.Context;",
"import android.content.Intent;",
"public class FooManager {",
" private static final String PERMISSION_RED = \"red\";",
" private static final String PERMISSION_BLUE = \"blue\";",
" @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)",
" private static final String ACTION_NONE = \"none\";",
" @RequiresPermission(PERMISSION_RED)",
" @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)",
" private static final String ACTION_RED = \"red\";",
" @RequiresPermission(allOf={PERMISSION_RED,PERMISSION_BLUE})",
" @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)",
" private static final String ACTION_RED_BLUE = \"red_blue\";",
" public void exampleNone(Context context) {",
" Intent intent = new Intent(ACTION_NONE);",
" context.sendBroadcast(intent);",
" // BUG: Diagnostic contains:",
" context.sendBroadcast(intent, PERMISSION_RED);",
" // BUG: Diagnostic contains:",
" context.sendBroadcastWithMultiplePermissions(intent,",
" new String[] { PERMISSION_RED });",
" // BUG: Diagnostic contains:",
" context.sendBroadcastWithMultiplePermissions(intent,",
" new String[] { PERMISSION_RED, PERMISSION_BLUE });",
" }",
" public void exampleRed(Context context) {",
" Intent intent = new Intent(FooManager.ACTION_RED);",
" // BUG: Diagnostic contains:",
" context.sendBroadcast(intent);",
" context.sendBroadcast(intent, FooManager.PERMISSION_RED);",
" context.sendBroadcastWithMultiplePermissions(intent,",
" new String[] { FooManager.PERMISSION_RED });",
" // BUG: Diagnostic contains:",
" context.sendBroadcastWithMultiplePermissions(intent,",
" new String[] { FooManager.PERMISSION_RED, PERMISSION_BLUE });",
" }",
" public void exampleRedBlue(Context context) {",
" Intent intent = new Intent(ACTION_RED_BLUE);",
" // BUG: Diagnostic contains:",
" context.sendBroadcast(intent);",
" // BUG: Diagnostic contains:",
" context.sendBroadcast(intent, PERMISSION_RED);",
" // BUG: Diagnostic contains:",
" context.sendBroadcastWithMultiplePermissions(intent,",
" new String[] { PERMISSION_RED });",
" context.sendBroadcastWithMultiplePermissions(intent,",
" new String[] { PERMISSION_RED, PERMISSION_BLUE });",
" }",
" public void exampleUnknown(Context context, Intent intent) {",
" // BUG: Diagnostic contains:",
" context.sendBroadcast(intent);",
" // BUG: Diagnostic contains:",
" context.sendBroadcast(intent, PERMISSION_RED);",
" // BUG: Diagnostic contains:",
" context.sendBroadcastWithMultiplePermissions(intent,",
" new String[] { PERMISSION_RED });",
" // BUG: Diagnostic contains:",
" context.sendBroadcastWithMultiplePermissions(intent,",
" new String[] { PERMISSION_RED, PERMISSION_BLUE });",
" }",
" public void exampleReuse(Context context) {",
" Intent intent = new Intent(ACTION_RED);",
" context.sendBroadcast(intent, PERMISSION_RED);",
" intent = new Intent(ACTION_NONE);",
" context.sendBroadcast(intent);",
" intent.setAction(ACTION_RED);",
" context.sendBroadcast(intent, PERMISSION_RED);",
" }",
" public void exampleScoped(Context context) {",
" if (true) {",
" Intent intent = new Intent(ACTION_RED);",
" context.sendBroadcast(intent, PERMISSION_RED);",
" } else {",
" Intent intent = new Intent(ACTION_NONE);",
" context.sendBroadcast(intent);",
" }",
" }",
"}")
.doTest();
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2008 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 android.annotation;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* Indicates a constant field value should be exported to be used in the SDK tools.
* @hide
*/
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.SOURCE)
public @interface SdkConstant {
public static enum SdkConstantType {
ACTIVITY_INTENT_ACTION, BROADCAST_INTENT_ACTION, SERVICE_ACTION, INTENT_CATEGORY, FEATURE;
}
SdkConstantType value();
}

View File

@@ -18,9 +18,22 @@ package android.content;
public class Context {
public int getUserId() {
return 0;
throw new UnsupportedOperationException();
}
public void enforceCallingOrSelfPermission(String permission, String message) {
throw new UnsupportedOperationException();
}
public void sendBroadcast(Intent intent) {
throw new UnsupportedOperationException();
}
public void sendBroadcast(Intent intent, String receiverPermission) {
throw new UnsupportedOperationException();
}
public void sendBroadcastWithMultiplePermissions(Intent intent, String[] receiverPermissions) {
throw new UnsupportedOperationException();
}
}

View File

@@ -17,4 +17,11 @@
package android.content;
public class Intent {
public Intent(String action) {
throw new UnsupportedOperationException();
}
public Intent setAction(String action) {
throw new UnsupportedOperationException();
}
}