/* * 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; } }