/* * Copyright (C) 2011 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_MANIFEST_XML; import static com.android.SdkConstants.ANDROID_URI; import static com.android.SdkConstants.ATTR_MIN_SDK_VERSION; import static com.android.SdkConstants.ATTR_NAME; import static com.android.SdkConstants.ATTR_PACKAGE; 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_PERMISSION; 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_LIBRARY; import static com.android.SdkConstants.TAG_USES_PERMISSION; import static com.android.SdkConstants.TAG_USES_SDK; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.tools.lint.detector.api.Category; import com.android.tools.lint.detector.api.Context; import com.android.tools.lint.detector.api.Detector; import com.android.tools.lint.detector.api.Issue; 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.Maps; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Checks for issues in AndroidManifest files such as declaring elements in the * wrong order. */ public class ManifestOrderDetector extends Detector implements Detector.XmlScanner { /** Wrong order of elements in the manifest */ public static final Issue ORDER = Issue.create( "ManifestOrder", //$NON-NLS-1$ "Checks for manifest problems like <uses-sdk> after the <application> tag", "The <application> tag should appear after the elements which declare " + "which version you need, which features you need, which libraries you " + "need, and so on. In the past there have been subtle bugs (such as " + "themes not getting applied correctly) when the `<application>` tag appears " + "before some of these other elements, so it's best to order your " + "manifest in the logical dependency order.", Category.CORRECTNESS, 5, Severity.WARNING, ManifestOrderDetector.class, Scope.MANIFEST_SCOPE); /** Missing a {@code <uses-sdk>} element */ public static final Issue USES_SDK = Issue.create( "UsesMinSdkAttributes", //$NON-NLS-1$ "Checks that the minimum SDK and target SDK attributes are defined", "The manifest should contain a `<uses-sdk>` element which defines the " + "minimum API Level required for the application to run, " + "as well as the target version (the highest API level you have tested " + "the version for.)", Category.CORRECTNESS, 9, Severity.WARNING, ManifestOrderDetector.class, Scope.MANIFEST_SCOPE).setMoreInfo( "http://developer.android.com/guide/topics/manifest/uses-sdk-element.html"); //$NON-NLS-1$ /** Using a targetSdkVersion that isn't recent */ public static final Issue TARGET_NEWER = Issue.create( "OldTargetApi", //$NON-NLS-1$ "Checks that the manifest specifies a targetSdkVersion that is recent", "When your application runs on a version of Android that is more recent than your " + "`targetSdkVersion` specifies that it has been tested with, various compatibility " + "modes kick in. This ensures that your application continues to work, but it may " + "look out of place. For example, if the `targetSdkVersion` is less than 14, your " + "app may get an option button in the UI.\n" + "\n" + "To fix this issue, set the `targetSdkVersion` to the highest available value. Then " + "test your app to make sure everything works correctly. You may want to consult " + "the compatibility notes to see what changes apply to each version you are adding " + "support for: " + "http://developer.android.com/reference/android/os/Build.VERSION_CODES.html", Category.CORRECTNESS, 6, Severity.WARNING, ManifestOrderDetector.class, Scope.MANIFEST_SCOPE).setMoreInfo( "http://developer.android.com/reference/android/os/Build.VERSION_CODES.html"); //$NON-NLS-1$ /** Using multiple {@code <uses-sdk>} elements */ public static final Issue MULTIPLE_USES_SDK = Issue.create( "MultipleUsesSdk", //$NON-NLS-1$ "Checks that the <uses-sdk> element appears at most once", "The `<uses-sdk>` element should appear just once; the tools will *not* merge the " + "contents of all the elements so if you split up the atttributes across multiple " + "elements, only one of them will take effect. To fix this, just merge all the " + "attributes from the various elements into a single <uses-sdk> element.", Category.CORRECTNESS, 6, Severity.FATAL, ManifestOrderDetector.class, Scope.MANIFEST_SCOPE).setMoreInfo( "http://developer.android.com/guide/topics/manifest/uses-sdk-element.html"); //$NON-NLS-1$ /** Missing a {@code <uses-sdk>} element */ public static final Issue WRONG_PARENT = Issue.create( "WrongManifestParent", //$NON-NLS-1$ "Checks that various manifest elements are declared in the right place", "The `<uses-library>` element should be defined as a direct child of the " + "`<application>` tag, not the `<manifest>` tag or an `<activity>` tag. Similarly, " + "a `<uses-sdk>` tag much be declared at the root level, and so on. This check " + "looks for incorrect declaration locations in the manifest, and complains " + "if an element is found in the wrong place.", Category.CORRECTNESS, 6, Severity.FATAL, ManifestOrderDetector.class, Scope.MANIFEST_SCOPE).setMoreInfo( "http://developer.android.com/guide/topics/manifest/manifest-intro.html"); //$NON-NLS-1$ /** Missing a {@code <uses-sdk>} element */ public static final Issue DUPLICATE_ACTIVITY = Issue.create( "DuplicateActivity", //$NON-NLS-1$ "Checks that an activity is registered only once in the manifest", "An activity should only be registered once in the manifest. If it is " + "accidentally registered more than once, then subtle errors can occur, " + "since attribute declarations from the two elements are not merged, so " + "you may accidentally remove previous declarations.", Category.CORRECTNESS, 5, Severity.ERROR, ManifestOrderDetector.class, Scope.MANIFEST_SCOPE); /** Not explicitly defining allowBackup */ public static final Issue ALLOW_BACKUP = Issue.create( "AllowBackup", //$NON-NLS-1$ "Ensure that allowBackup is explicitly set in the application's manifest", "The allowBackup attribute determines if an application's data can be backed up " + "and restored. It is documented at " + "http://developer.android.com/reference/android/R.attr.html#allowBackup\n" + "\n" + "By default, this flag is set to `true`. When this flag is set to `true`, " + "application data can be backed up and restored by the user using `adb backup` " + "and `adb restore`.\n" + "\n" + "This may have security consequences for an application. `adb backup` allows " + "users who have enabled USB debugging to copy application data off of the " + "device. Once backed up, all application data can be read by the user. " + "`adb restore` allows creation of application data from a source specified " + "by the user. Following a restore, applications should not assume that the " + "data, file permissions, and directory permissions were created by the " + "application itself.\n" + "\n" + "Setting `allowBackup=\"false\"` opts an application out of both backup and " + "restore.\n" + "\n" + "To fix this warning, decide whether your application should support backup, " + "and explicitly set `android:allowBackup=(true|false)\"`", Category.SECURITY, 3, Severity.WARNING, ManifestOrderDetector.class, Scope.MANIFEST_SCOPE).setMoreInfo( "http://developer.android.com/reference/android/R.attr.html#allowBackup"); /** Conflicting permission names */ public static final Issue UNIQUE_PERMISSION = Issue.create( "UniquePermission", //$NON-NLS-1$ "Checks that permission names are unique", "The unqualified names or your permissions must be unique. The reason for this " + "is that at build time, the `aapt` tool will generate a class named `Manifest` " + "which contains a field for each of your permissions. These fields are named " + "using your permission unqualified names (i.e. the name portion after the last " + "dot).\n" + "\n" + "If more than one permission maps to the same field name, that field will " + "arbitrarily name just one of them.", Category.CORRECTNESS, 6, Severity.ERROR, ManifestOrderDetector.class, Scope.MANIFEST_SCOPE); /** Constructs a new {@link ManifestOrderDetector} check */ public ManifestOrderDetector() { } private boolean mSeenApplication; /** Number of times we've seen the <uses-sdk> element */ private int mSeenUsesSdk; /** Activities we've encountered */ private Set<String> mActivities = new HashSet<String>(); /** Permission basenames */ private Map<String, String> mPermissionNames; /** Package declared in the manifest */ private String mPackage; @Override public @NonNull Speed getSpeed() { return Speed.FAST; } @Override public boolean appliesTo(@NonNull Context context, @NonNull File file) { return file.getName().equals(ANDROID_MANIFEST_XML); } @Override public void beforeCheckFile(@NonNull Context context) { mSeenApplication = false; mSeenUsesSdk = 0; } @Override public void afterCheckFile(@NonNull Context context) { if (mSeenUsesSdk == 0 && context.isEnabled(USES_SDK)) { context.report(USES_SDK, Location.create(context.file), "Manifest should specify a minimum API level with " + "<uses-sdk android:minSdkVersion=\"?\" />; if it really supports " + "all versions of Android set it to 1.", null); } } // ---- Implements Detector.XmlScanner ---- @Override public Collection<String> getApplicableElements() { return Arrays.asList( TAG_APPLICATION, TAG_USES_PERMISSION, TAG_PERMISSION, "permission-tree", //$NON-NLS-1$ "permission-group", //$NON-NLS-1$ TAG_USES_SDK, "uses-configuration", //$NON-NLS-1$ "uses-feature", //$NON-NLS-1$ "supports-screens", //$NON-NLS-1$ "compatible-screens", //$NON-NLS-1$ "supports-gl-texture", //$NON-NLS-1$ TAG_USES_LIBRARY, TAG_ACTIVITY, TAG_SERVICE, TAG_PROVIDER, TAG_RECEIVER ); } @Override public void visitElement(@NonNull XmlContext context, @NonNull Element element) { String tag = element.getTagName(); Node parentNode = element.getParentNode(); if (tag.equals(TAG_USES_LIBRARY) || tag.equals(TAG_ACTIVITY) || tag.equals(TAG_SERVICE) || tag.equals(TAG_PROVIDER) || tag.equals(TAG_RECEIVER)) { if (!TAG_APPLICATION.equals(parentNode.getNodeName()) && context.isEnabled(WRONG_PARENT)) { context.report(WRONG_PARENT, element, context.getLocation(element), String.format( "The <%1$s> element must be a direct child of the <application> element", tag), null); } if (tag.equals(TAG_ACTIVITY)) { Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); if (nameNode != null) { String name = nameNode.getValue(); if (!name.isEmpty()) { if (name.charAt(0) == '.') { name = getPackage(element) + name; } else if (name.indexOf('.') == -1) { name = getPackage(element) + '.' + name; } if (mActivities.contains(name)) { String message = String.format( "Duplicate registration for activity %1$s", name); context.report(DUPLICATE_ACTIVITY, element, context.getLocation(nameNode), message, null); } else { mActivities.add(name); } } } } return; } if (parentNode != element.getOwnerDocument().getDocumentElement() && context.isEnabled(WRONG_PARENT)) { context.report(WRONG_PARENT, element, context.getLocation(element), String.format( "The <%1$s> element must be a direct child of the " + "<manifest> root element", tag), null); } if (tag.equals(TAG_USES_SDK)) { mSeenUsesSdk++; if (mSeenUsesSdk == 2) { // Only warn when we encounter the first one Location location = context.getLocation(element); // Link up *all* encountered locations in the document NodeList elements = element.getOwnerDocument().getElementsByTagName(TAG_USES_SDK); Location secondary = null; for (int i = elements.getLength() - 1; i >= 0; i--) { Element e = (Element) elements.item(i); if (e != element) { Location l = context.getLocation(e); l.setSecondary(secondary); l.setMessage("Also appears here"); secondary = l; } } location.setSecondary(secondary); if (context.isEnabled(MULTIPLE_USES_SDK)) { context.report(MULTIPLE_USES_SDK, element, location, "There should only be a single <uses-sdk> element in the manifest:" + " merge these together", null); } return; } if (!element.hasAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION)) { if (context.isEnabled(USES_SDK)) { context.report(USES_SDK, element, context.getLocation(element), "<uses-sdk> tag should specify a minimum API level with " + "android:minSdkVersion=\"?\"", null); } } if (!element.hasAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION)) { // Warn if not setting target SDK -- but only if the min SDK is somewhat // old so there's some compatibility stuff kicking in (such as the menu // button etc) if (context.isEnabled(USES_SDK)) { context.report(USES_SDK, element, context.getLocation(element), "<uses-sdk> tag should specify a target API level (the " + "highest verified version; when running on later versions, " + "compatibility behaviors may be enabled) with " + "android:targetSdkVersion=\"?\"", null); } } else if (context.isEnabled(TARGET_NEWER)){ String target = element.getAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION); try { int api = Integer.parseInt(target); if (api < context.getClient().getHighestKnownApiLevel()) { context.report(TARGET_NEWER, element, context.getLocation(element), "Not targeting the latest versions of Android; compatibility " + "modes apply. Consider testing and updating this version. " + "Consult the android.os.Build.VERSION_CODES javadoc for details.", null); } } catch (NumberFormatException nufe) { // Ignore: AAPT will enforce this. } } } if (tag.equals(TAG_PERMISSION)) { Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); if (nameNode != null) { String name = nameNode.getValue(); String base = name.substring(name.lastIndexOf('.') + 1); if (mPermissionNames == null) { mPermissionNames = Maps.newHashMap(); } else if (mPermissionNames.containsKey(base)) { String prevName = mPermissionNames.get(base); Location location = context.getLocation(nameNode); NodeList siblings = element.getParentNode().getChildNodes(); for (int i = 0, n = siblings.getLength(); i < n; i++) { Node node = siblings.item(i); if (node == element) { break; } else if (node.getNodeType() == Node.ELEMENT_NODE) { Element sibling = (Element) node; String suffix = '.' + base; if (sibling.getTagName().equals(TAG_PERMISSION)) { String b = element.getAttributeNS(ANDROID_URI, ATTR_NAME); if (b.endsWith(suffix)) { Location prevLocation = context.getLocation(node); prevLocation.setMessage("Previous permission here"); location.setSecondary(prevLocation); break; } } } } String message = String.format("Permission name %1$s is not unique " + "(appears in both %2$s and %3$s)", base, prevName, name); context.report(UNIQUE_PERMISSION, element, location, message, null); } mPermissionNames.put(base, name); } } if (tag.equals(TAG_APPLICATION)) { mSeenApplication = true; if (!element.hasAttributeNS(ANDROID_URI, SdkConstants.ATTR_ALLOW_BACKUP) && context.isEnabled(ALLOW_BACKUP)) { context.report(ALLOW_BACKUP, element, context.getLocation(element), String.format("Should explicitly set android:allowBackup to true or " + "false (it's true by default, and that can have some security " + "implications for the application's data)", tag), null); } } else if (mSeenApplication) { if (context.isEnabled(ORDER)) { context.report(ORDER, element, context.getLocation(element), String.format("<%1$s> tag appears after <application> tag", tag), null); } // Don't complain for *every* element following the <application> tag mSeenApplication = false; } } private String getPackage(Element element) { if (mPackage == null) { return element.getOwnerDocument().getDocumentElement().getAttribute(ATTR_PACKAGE); } return mPackage; } }