package permissions.dispatcher;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
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.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiReferenceExpression;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class CallNeedsPermissionDetector extends Detector implements Detector.JavaPsiScanner {
public static final Issue ISSUE = Issue.create("CallNeedsPermission",
"Call the corresponding \"withCheck\" method of the generated PermissionsDispatcher class instead",
"Directly invoking a method annotated with @NeedsPermission may lead to misleading behaviour on devices running Marshmallow and up. Therefore, it is advised to use the generated PermissionsDispatcher class instead, which provides a \"withCheck\" method that safely handles runtime permissions.",
Category.CORRECTNESS,
7,
Severity.ERROR,
new Implementation(CallNeedsPermissionDetector.class,
EnumSet.of(Scope.ALL_JAVA_FILES)));
static List<String> generatedClassNames = new ArrayList<String>();
static List<String> methods = Collections.emptyList();
@Override
public List<Class<? extends PsiElement>> getApplicablePsiTypes() {
return Collections.<Class<? extends PsiElement>>singletonList(PsiClass.class);
}
public CallNeedsPermissionDetector() {
// No-op
}
@Override
public JavaElementVisitor createPsiVisitor(final JavaContext context) {
if (context.getPhase() == 1) {
// find out which class has RuntimePermissions
return new AnnotationChecker(context);
}
return null;
}
@Override
public List<String> getApplicableMethodNames() {
return methods;
}
@Override
public void visitMethod(JavaContext context, JavaElementVisitor visitor,
PsiMethodCallExpression node, PsiMethod method) {
if (methods.contains(method.getName())) {
context.report(ISSUE, node, context.getLocation(node),
"Trying to access permission-protected method directly");
}
}
private static class AnnotationChecker extends JavaElementVisitor {
private final JavaContext context;
private final Set<String> matchingAnnotationTypeNames;
private AnnotationChecker(JavaContext context) {
this.context = context;
matchingAnnotationTypeNames = new HashSet<String>();
matchingAnnotationTypeNames.add("RuntimePermissions");
matchingAnnotationTypeNames.add("permissions.dispatcher.RuntimePermissions");
}
@Override
public void visitReferenceExpression(PsiReferenceExpression expression) {
skipGeneratedFiles(context);
super.visitReferenceExpression(expression);
}
@Override
public void visitAnnotation(PsiAnnotation annotation) {
if (!context.isEnabled(ISSUE)) {
super.visitAnnotation(annotation);
return;
}
String type = annotation.getQualifiedName();
if (!matchingAnnotationTypeNames.contains(type)) {
super.visitAnnotation(annotation);
return;
}
PsiClass[] classes = context.getJavaFile().getClasses();
if (classes.length > 0 && classes[0].getName() != null) {
generatedClassNames.add(classes[0].getName() + "PermissionsDispatcher");
// let's check method call!
context.requestRepeat(new CallNeedsPermissionDetector(),
EnumSet.of(Scope.ALL_JAVA_FILES));
}
super.visitAnnotation(annotation);
}
private static void skipGeneratedFiles(JavaContext context) {
PsiClass[] classes = context.getJavaFile().getClasses();
if (classes.length > 0 && classes[0].getName() != null) {
String qualifiedName = classes[0].getName();
if (qualifiedName.contains("PermissionsDispatcher")) {
// skip generated files
return;
}
}
}
}
}