blob: de262a0293369d161df9e59c24b2fb09f058972e [file] [log] [blame]
/*
* Copyright (C) 2012 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_APP_ACTIVITY;
import static com.android.SdkConstants.ANDROID_APP_SERVICE;
import static com.android.SdkConstants.ANDROID_CONTENT_BROADCAST_RECEIVER;
import static com.android.SdkConstants.ANDROID_CONTENT_CONTENT_PROVIDER;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.TAG_ACTIVITY;
import static com.android.SdkConstants.TAG_PROVIDER;
import static com.android.SdkConstants.TAG_RECEIVER;
import static com.android.SdkConstants.TAG_SERVICE;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.ClassContext;
import com.android.tools.lint.detector.api.Detector.ClassScanner;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.Speed;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import org.w3c.dom.Element;
import java.util.Arrays;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Map.Entry;
/**
* Checks for missing manifest registrations for activities, services etc
* and also makes sure that they are registered with the correct tag
* <p>
* TODO: Rewrite as Java visitor!
*/
public class RegistrationDetector extends LayoutDetector implements ClassScanner {
/** Unregistered activities and services */
public static final Issue ISSUE = Issue.create(
"Registered", //$NON-NLS-1$
"Class is not registered in the manifest",
"Activities, services and content providers should be registered in the " +
"`AndroidManifest.xml` file using `<activity>`, `<service>` and `<provider>` tags.\n" +
"\n" +
"If your activity is simply a parent class intended to be subclassed by other " +
"\"real\" activities, make it an abstract class.",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
RegistrationDetector.class,
EnumSet.of(Scope.MANIFEST, Scope.CLASS_FILE)))
.addMoreInfo(
"http://developer.android.com/guide/topics/manifest/manifest-intro.html"); //$NON-NLS-1$
protected Multimap<String, String> mManifestRegistrations;
/** Constructs a new {@link RegistrationDetector} */
public RegistrationDetector() {
}
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
// ---- Implements XmlScanner ----
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(sTags);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String fqcn = getFqcn(context, element);
String tag = element.getTagName();
String frameworkClass = tagToClass(tag);
if (frameworkClass != null) {
String signature = ClassContext.getInternalName(fqcn);
if (mManifestRegistrations == null) {
mManifestRegistrations = ArrayListMultimap.create(4, 8);
}
mManifestRegistrations.put(frameworkClass, signature);
if (signature.indexOf('$') != -1) {
// The internal name contains a $ which means it's an inner class.
// The conversion from fqcn to internal name is a bit ambiguous:
// "a.b.C.D" usually means "inner class D in class C in package a.b".
// However, it can (see issue 31592) also mean class D in package "a.b.C".
// Place *both* of these possibilities in the registered map, since this
// is only used to check that an activity is registered, not the other way
// (so it's okay to have entries there that do not correspond to real classes).
signature = signature.replace('$', '/');
mManifestRegistrations.put(frameworkClass, signature);
}
}
}
/**
* Returns the fully qualified class name for a manifest entry element that
* specifies a name attribute
*
* @param context the query context providing the project
* @param element the element
* @return the fully qualified class name
*/
@NonNull
private static String getFqcn(@NonNull XmlContext context, @NonNull Element element) {
String className = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (className.startsWith(".")) { //$NON-NLS-1$
return context.getMainProject().getPackage() + className;
} else if (className.indexOf('.') == -1) {
// According to the <activity> manifest element documentation, this is not
// valid ( http://developer.android.com/guide/topics/manifest/activity-element.html )
// but it appears in manifest files and appears to be supported by the runtime
// so handle this in code as well:
return context.getMainProject().getPackage() + '.' + className;
} // else: the class name is already a fully qualified class name
return className;
}
// ---- Implements ClassScanner ----
@Override
public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) {
// Abstract classes do not need to be registered
if ((classNode.access & Opcodes.ACC_ABSTRACT) != 0) {
return;
}
String curr = classNode.name;
int lastIndex = curr.lastIndexOf('$');
if (lastIndex != -1 && lastIndex < curr.length() - 1) {
if (Character.isDigit(curr.charAt(lastIndex+1))) {
// Anonymous inner class, doesn't need to be registered
return;
}
}
while (curr != null) {
for (String s : sClasses) {
if (curr.equals(s)) {
Collection<String> registered = mManifestRegistrations != null ?
mManifestRegistrations.get(curr) : null;
if (registered == null || !registered.contains(classNode.name)) {
report(context, classNode, curr);
}
}
}
curr = context.getDriver().getSuperClass(curr);
}
}
private void report(ClassContext context, ClassNode classNode, String curr) {
String tag = classToTag(curr);
String className = ClassContext.createSignature(classNode.name, null, null);
String wrongClass = null; // The framework class this class actually extends
if (mManifestRegistrations != null) {
Collection<Entry<String,String>> entries =
mManifestRegistrations.entries();
for (Entry<String,String> entry : entries) {
if (entry.getValue().equals(classNode.name)) {
wrongClass = entry.getKey();
break;
}
}
}
if (wrongClass != null) {
Location location = context.getLocation(classNode);
context.report(
ISSUE,
location,
String.format(
"`%1$s` is a `<%2$s>` but is registered in the manifest as a `<%3$s>`",
className, tag, classToTag(wrongClass)));
} else if (!TAG_RECEIVER.equals(tag)) { // don't need to be registered
if (context.getMainProject().isGradleProject()) {
// Disabled for now; we need to formalize the difference between
// the *manifest* package and the variant package, since in some contexts
// (such as manifest registrations) we should be using the manifest package,
// not the gradle package
return;
}
Location location = context.getLocation(classNode);
context.report(
ISSUE,
location,
String.format(
"The `<%1$s> %2$s` is not registered in the manifest",
tag, className));
}
}
/** The manifest tags we care about */
private static final String[] sTags = new String[] {
TAG_ACTIVITY,
TAG_SERVICE,
TAG_RECEIVER,
TAG_PROVIDER,
// Keep synchronized with {@link #sClasses}
};
/** The corresponding framework classes that the tags in {@link #sTags} should extend */
private static final String[] sClasses = new String[] {
ANDROID_APP_ACTIVITY,
ANDROID_APP_SERVICE,
ANDROID_CONTENT_BROADCAST_RECEIVER,
ANDROID_CONTENT_CONTENT_PROVIDER,
// Keep synchronized with {@link #sTags}
};
/** Looks up the corresponding framework class a given manifest tag's class should extend */
private static String tagToClass(String tag) {
for (int i = 0, n = sTags.length; i < n; i++) {
if (sTags[i].equals(tag)) {
return sClasses[i];
}
}
return null;
}
/** Looks up the tag a given framework class should be registered with */
protected static String classToTag(String className) {
for (int i = 0, n = sClasses.length; i < n; i++) {
if (sClasses[i].equals(className)) {
return sTags[i];
}
}
return null;
}
}