/* * 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_ALLOW_BACKUP; import static com.android.SdkConstants.ATTR_ICON; 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.ATTR_VERSION_CODE; import static com.android.SdkConstants.ATTR_VERSION_NAME; import static com.android.SdkConstants.DRAWABLE_PREFIX; import static com.android.SdkConstants.PREFIX_RESOURCE_REF; import static com.android.SdkConstants.TAG_ACTIVITY; import static com.android.SdkConstants.TAG_APPLICATION; import static com.android.SdkConstants.TAG_INTENT_FILTER; 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_FEATURE; 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 static com.android.xml.AndroidManifest.NODE_ACTION; import static com.android.xml.AndroidManifest.NODE_DATA; import static com.android.xml.AndroidManifest.NODE_METADATA; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.builder.model.AndroidProject; import com.android.builder.model.ApiVersion; import com.android.builder.model.BuildTypeContainer; import com.android.builder.model.ProductFlavor; import com.android.builder.model.ProductFlavorContainer; import com.android.builder.model.SourceProviderContainer; import com.android.builder.model.Variant; import com.android.ide.common.res2.AbstractResourceRepository; import com.android.ide.common.resources.ResourceUrl; 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.Implementation; import com.android.tools.lint.detector.api.Issue; import com.android.tools.lint.detector.api.LintUtils; import com.android.tools.lint.detector.api.Location; import com.android.tools.lint.detector.api.Project; 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.List; import java.util.Map; import java.util.Set; /** * Checks for issues in AndroidManifest files such as declaring elements in the * wrong order. */ public class ManifestDetector extends Detector implements Detector.XmlScanner { private static final Implementation IMPLEMENTATION = new Implementation( ManifestDetector.class, Scope.MANIFEST_SCOPE ); /** Wrong order of elements in the manifest */ public static final Issue ORDER = Issue.create( "ManifestOrder", //$NON-NLS-1$ "Incorrect order of elements in manifest", "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, IMPLEMENTATION); /** Missing a {@code <uses-sdk>} element */ public static final Issue USES_SDK = Issue.create( "UsesMinSdkAttributes", //$NON-NLS-1$ "Minimum SDK and target SDK attributes not 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, IMPLEMENTATION).addMoreInfo( "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$ "Target SDK attribute is not targeting latest version", "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, IMPLEMENTATION).addMoreInfo( "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$ "Multiple `<uses-sdk>` elements in the manifest", "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 attributes 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, IMPLEMENTATION).addMoreInfo( "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$ "Wrong manifest parent", "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, IMPLEMENTATION).addMoreInfo( "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$ "Activity registered more than once", "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.FATAL, IMPLEMENTATION); /** Not explicitly defining allowBackup */ public static final Issue ALLOW_BACKUP = Issue.create( "AllowBackup", //$NON-NLS-1$ "Missing `allowBackup` attribute", "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, IMPLEMENTATION).addMoreInfo( "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$ "Permission names are not 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.FATAL, IMPLEMENTATION); /** Using a resource for attributes that do not allow it */ public static final Issue SET_VERSION = Issue.create( "MissingVersion", //$NON-NLS-1$ "Missing application name/version", "You should define the version information for your application.\n" + "`android:versionCode`: An integer value that represents the version of the " + "application code, relative to other versions.\n" + "\n" + "`android:versionName`: A string value that represents the release version of " + "the application code, as it should be shown to users.", Category.CORRECTNESS, 2, Severity.WARNING, IMPLEMENTATION).addMoreInfo( "http://developer.android.com/tools/publishing/versioning.html#appversioning"); /** Using a resource for attributes that do not allow it */ public static final Issue ILLEGAL_REFERENCE = Issue.create( "IllegalResourceRef", //$NON-NLS-1$ "Name and version must be integer or string, not resource", "For the `versionCode` attribute, you have to specify an actual integer " + "literal; you cannot use an indirection with a `@dimen/name` resource. " + "Similarly, the `versionName` attribute should be an actual string, not " + "a string resource url.", Category.CORRECTNESS, 8, Severity.WARNING, IMPLEMENTATION); /** Declaring a uses-feature multiple time */ public static final Issue DUPLICATE_USES_FEATURE = Issue.create( "DuplicateUsesFeature", //$NON-NLS-1$ "Feature declared more than once", "A given feature should only be declared once in the manifest.", Category.CORRECTNESS, 5, Severity.WARNING, IMPLEMENTATION); /** Not explicitly defining application icon */ public static final Issue APPLICATION_ICON = Issue.create( "MissingApplicationIcon", //$NON-NLS-1$ "Missing application icon", "You should set an icon for the application as whole because there is no " + "default. This attribute must be set as a reference to a drawable resource " + "containing the image (for example `@drawable/icon`).", Category.ICONS, 5, Severity.WARNING, IMPLEMENTATION).addMoreInfo( "http://developer.android.com/tools/publishing/preparing.html#publishing-configure"); //$NON-NLS-1$ /** Malformed Device Admin */ public static final Issue DEVICE_ADMIN = Issue.create( "DeviceAdmin", //$NON-NLS-1$ "Malformed Device Admin", "If you register a broadcast receiver which acts as a device admin, you must also " + "register an `<intent-filter>` for the action " + "`android.app.action.DEVICE_ADMIN_ENABLED`, without any `<data>`, such that the " + "device admin can be activated/deactivated.\n" + "\n" + "To do this, add\n" + "`<intent-filter>`\n" + " `<action android:name=\"android.app.action.DEVICE_ADMIN_ENABLED\" />`\n" + "`</intent-filter>`\n" + "to your `<receiver>`.", Category.CORRECTNESS, 7, Severity.WARNING, IMPLEMENTATION); /** Using a mock location in a non-debug-specific manifest file */ public static final Issue MOCK_LOCATION = Issue.create( "MockLocation", //$NON-NLS-1$ "Using mock location provider in production", "Using a mock location provider (by requiring the permission " + "`android.permission.ACCESS_MOCK_LOCATION`) should *only* be done " + "in debug builds (or from tests). In Gradle projects, that means you should only " + "request this permission in a test or debug source set specific manifest file.\n" + "\n" + "To fix this, create a new manifest file in the debug folder and move " + "the `<uses-permission>` element there. A typical path to a debug manifest " + "override file in a Gradle project is src/debug/AndroidManifest.xml.", Category.CORRECTNESS, 8, Severity.FATAL, IMPLEMENTATION); /** Defining a value that is overridden by Gradle */ public static final Issue GRADLE_OVERRIDES = Issue.create( "GradleOverrides", //$NON-NLS-1$ "Value overridden by Gradle build script", "The value of (for example) `minSdkVersion` is only used if it is not specified in " + "the `build.gradle` build scripts. When specified in the Gradle build scripts, " + "the manifest value is ignored and can be misleading, so should be removed to " + "avoid ambiguity.", Category.CORRECTNESS, 4, Severity.WARNING, IMPLEMENTATION); /** Using drawable rather than mipmap launcher icons */ public static final Issue MIPMAP = Issue.create( "MipmapIcons", //$NON-NLS-1$ "Use Mipmap Launcher Icons", "Launcher icons should be provided in the `mipmap` resource directory. " + "This is the same as the `drawable` resource directory, except resources in " + "the `mipmap` directory will not get stripped out when creating density-specific " + "APKs.\n" + "\n" + "In certain cases, the Launcher app may use a higher resolution asset (than " + "would normally be computed for the device) to display large app shortcuts. " + "If drawables for densities other than the device's resolution have been " + "stripped out, then the app shortcut could appear blurry.\n" + "\n" + "To fix this, move your launcher icons from `drawable-`dpi to `mipmap-`dpi " + "and change references from @drawable/ and R.drawable to @mipmap/ and R.mipmap.\n" + "In Android Studio this lint warning has a quickfix to perform this automatically.", Category.ICONS, 5, Severity.WARNING, IMPLEMENTATION); /** Permission name of mock location permission */ public static final String MOCK_LOCATION_PERMISSION = "android.permission.ACCESS_MOCK_LOCATION"; //$NON-NLS-1$ /** Constructs a new {@link ManifestDetector} check */ public ManifestDetector() { } private boolean mSeenApplication; /** Number of times we've seen the <uses-sdk> element */ private int mSeenUsesSdk; /** Activities we've encountered */ private Set<String> mActivities; /** Features we've encountered */ private Set<String> mUsesFeatures; /** Permission basenames */ private Map<String, String> mPermissionNames; /** Handle to the {@code <application>} tag */ private Location.Handle mApplicationTagHandle; /** Whether we've seen an application icon definition in any of the manifest files (or * if a manifest tag warning for this has been explicitly disabled) */ private boolean mSeenAppIcon; /** Whether we've seen an allow backup definition in any of the manifest files (or * if a manifest tag warning for this has been explicitly disabled) */ private boolean mSeenAllowBackup; @NonNull @Override public 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; mActivities = new HashSet<String>(); mUsesFeatures = new HashSet<String>(); } @Override public void afterCheckFile(@NonNull Context context) { XmlContext xmlContext = (XmlContext) context; Element element = xmlContext.document.getDocumentElement(); if (element != null) { checkDocumentElement(xmlContext, element); } if (mSeenUsesSdk == 0 && context.isEnabled(USES_SDK) // Not required in Gradle projects; typically defined in build.gradle instead // and inserted at build time && !context.getMainProject().isGradleProject()) { 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."); } } @Override public void afterCheckProject(@NonNull Context context) { if (!mSeenAllowBackup && context.isEnabled(ALLOW_BACKUP) && !context.getProject().isLibrary() && context.getMainProject().getMinSdk() >= 4) { Location location = getMainApplicationTagLocation(context); context.report(ALLOW_BACKUP, location, "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)"); } if (!context.getMainProject().isLibrary() && !mSeenAppIcon && context.isEnabled(APPLICATION_ICON)) { Location location = getMainApplicationTagLocation(context); context.report(APPLICATION_ICON, location, "Should explicitly set `android:icon`, there is no default"); } } @Nullable private Location getMainApplicationTagLocation(@NonNull Context context) { if (mApplicationTagHandle != null) { return mApplicationTagHandle.resolve(); } List<File> manifestFiles = context.getMainProject().getManifestFiles(); if (!manifestFiles.isEmpty()) { return Location.create(manifestFiles.get(0)); } return null; } private static void checkDocumentElement(XmlContext context, Element element) { Attr codeNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_VERSION_CODE); if (codeNode != null && codeNode.getValue().startsWith(PREFIX_RESOURCE_REF) && context.isEnabled(ILLEGAL_REFERENCE)) { context.report(ILLEGAL_REFERENCE, element, context.getLocation(codeNode), "The `android:versionCode` cannot be a resource url, it must be " + "a literal integer"); } else if (codeNode == null && context.isEnabled(SET_VERSION) // Not required in Gradle projects; typically defined in build.gradle instead // and inserted at build time && !context.getMainProject().isGradleProject()) { context.report(SET_VERSION, element, context.getLocation(element), "Should set `android:versionCode` to specify the application version"); } Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_VERSION_NAME); if (nameNode == null && context.isEnabled(SET_VERSION) // Not required in Gradle projects; typically defined in build.gradle instead // and inserted at build time && !context.getMainProject().isGradleProject()) { context.report(SET_VERSION, element, context.getLocation(element), "Should set `android:versionName` to specify the application version"); } checkOverride(context, element, ATTR_VERSION_CODE); checkOverride(context, element, ATTR_VERSION_NAME); Attr pkgNode = element.getAttributeNode(ATTR_PACKAGE); if (pkgNode != null) { String pkg = pkgNode.getValue(); if (pkg.contains("${") && context.getMainProject().isGradleProject()) { context.report(GRADLE_OVERRIDES, pkgNode, context.getLocation(pkgNode), "Cannot use placeholder for the package in the manifest; " + "set `applicationId` in `build.gradle` instead"); } } } private static void checkOverride(XmlContext context, Element element, String attributeName) { Project project = context.getProject(); Attr attribute = element.getAttributeNodeNS(ANDROID_URI, attributeName); if (project.isGradleProject() && attribute != null && context.isEnabled(GRADLE_OVERRIDES)) { Variant variant = project.getCurrentVariant(); if (variant != null) { ProductFlavor flavor = variant.getMergedFlavor(); String gradleValue = null; if (ATTR_MIN_SDK_VERSION.equals(attributeName)) { try { ApiVersion minSdkVersion = flavor.getMinSdkVersion(); gradleValue = minSdkVersion != null ? minSdkVersion.getApiString() : null; } catch (Throwable e) { // TODO: REMOVE ME // This method was added in the 0.11 model. We'll need to drop support // for 0.10 shortly but until 0.11 is available this is a stopgap measure } } else if (ATTR_TARGET_SDK_VERSION.equals(attributeName)) { try { ApiVersion targetSdkVersion = flavor.getTargetSdkVersion(); gradleValue = targetSdkVersion != null ? targetSdkVersion.getApiString() : null; } catch (Throwable e) { // TODO: REMOVE ME // This method was added in the 0.11 model. We'll need to drop support // for 0.10 shortly but until 0.11 is available this is a stopgap measure } } else if (ATTR_VERSION_CODE.equals(attributeName)) { Integer versionCode = flavor.getVersionCode(); if (versionCode != null) { gradleValue = versionCode.toString(); } } else if (ATTR_VERSION_NAME.equals(attributeName)) { gradleValue = flavor.getVersionName(); } else { assert false : attributeName; return; } if (gradleValue != null) { String manifestValue = attribute.getValue(); String message = String.format("This `%1$s` value (`%2$s`) is not used; it is " + "always overridden by the value specified in the Gradle build " + "script (`%3$s`)", attributeName, manifestValue, gradleValue); context.report(GRADLE_OVERRIDES, attribute, context.getLocation(attribute), message); } } } } // ---- 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$ TAG_USES_FEATURE, "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(); boolean isReceiver = tag.equals(TAG_RECEIVER); if (isReceiver) { checkDeviceAdmin(context, element); } if (tag.equals(TAG_USES_LIBRARY) || tag.equals(TAG_ACTIVITY) || tag.equals(TAG_SERVICE) || tag.equals(TAG_PROVIDER) || isReceiver) { 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)); } if (tag.equals(TAG_ACTIVITY)) { Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); if (nameNode != null) { String name = nameNode.getValue(); if (!name.isEmpty()) { String pkg = context.getMainProject().getPackage(); if (name.charAt(0) == '.') { name = pkg + name; } else if (name.indexOf('.') == -1) { name = pkg + '.' + 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); } else { mActivities.add(name); } } } checkMipmapIcon(context, element); } 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)); } 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"); } return; } if (!element.hasAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION)) { if (context.isEnabled(USES_SDK) && !context.getMainProject().isGradleProject()) { context.report(USES_SDK, element, context.getLocation(element), "`<uses-sdk>` tag should specify a minimum API level with " + "`android:minSdkVersion=\"?\"`"); } } else { Attr codeNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION); if (codeNode != null && codeNode.getValue().startsWith(PREFIX_RESOURCE_REF) && context.isEnabled(ILLEGAL_REFERENCE)) { context.report(ILLEGAL_REFERENCE, element, context.getLocation(codeNode), "The `android:minSdkVersion` cannot be a resource url, it must be " + "a literal integer (or string if a preview codename)"); } checkOverride(context, element, ATTR_MIN_SDK_VERSION); } 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.getMainProject().isGradleProject()) { 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=\"?\"`"); } } else { checkOverride(context, element, ATTR_TARGET_SDK_VERSION); if (context.isEnabled(TARGET_NEWER)) { Attr targetSdkVersionNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION); if (targetSdkVersionNode != null) { String target = targetSdkVersionNode.getValue(); try { int api = Integer.parseInt(target); if (api < context.getClient().getHighestKnownApiLevel()) { context.report(TARGET_NEWER, element, context.getLocation(targetSdkVersionNode), "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."); } } catch (NumberFormatException nufe) { // Ignore: AAPT will enforce this. } } } } Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION); if (nameNode != null && nameNode.getValue().startsWith(PREFIX_RESOURCE_REF) && context.isEnabled(ILLEGAL_REFERENCE)) { context.report(ILLEGAL_REFERENCE, element, context.getLocation(nameNode), "The `android:targetSdkVersion` cannot be a resource url, it must be " + "a literal integer (or string if a preview codename)"); } } 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); } mPermissionNames.put(base, name); } } if (tag.equals(TAG_USES_PERMISSION)) { Attr name = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); if (name != null && name.getValue().equals(MOCK_LOCATION_PERMISSION) && context.getMainProject().isGradleProject() && !isDebugOrTestManifest(context, context.file) && context.isEnabled(MOCK_LOCATION)) { String message = "Mock locations should only be requested in a test or " + "debug-specific manifest file (typically `src/debug/AndroidManifest.xml`)"; Location location = context.getLocation(name); context.report(MOCK_LOCATION, element, location, message); } } if (tag.equals(TAG_APPLICATION)) { mSeenApplication = true; boolean recordLocation = false; if (element.hasAttributeNS(ANDROID_URI, ATTR_ALLOW_BACKUP) || context.getDriver().isSuppressed(context, ALLOW_BACKUP, element)) { mSeenAllowBackup = true; } else { recordLocation = true; } if (element.hasAttributeNS(ANDROID_URI, ATTR_ICON) || context.getDriver().isSuppressed(context, APPLICATION_ICON, element)) { checkMipmapIcon(context, element); mSeenAppIcon = true; } else { recordLocation = true; } if (recordLocation && !context.getProject().isLibrary() && (mApplicationTagHandle == null || isMainManifest(context, context.file))) { mApplicationTagHandle = context.createLocationHandle(element); } Attr fullBackupNode = element.getAttributeNodeNS(ANDROID_URI, "fullBackupContent"); if (fullBackupNode != null && fullBackupNode.getValue().startsWith(PREFIX_RESOURCE_REF) && context.getClient().supportsProjectResources()) { AbstractResourceRepository resources = context.getClient() .getProjectResources(context.getProject(), true); ResourceUrl url = ResourceUrl.parse(fullBackupNode.getValue()); if (url != null && !url.framework && resources != null && !resources.hasResourceItem(url.type, url.name)) { Location location = context.getValueLocation(fullBackupNode); context.report(ALLOW_BACKUP, fullBackupNode, location, "Missing `<full-backup-content>` resource"); } } else if (fullBackupNode == null && context.getMainProject().getTargetSdk() >= 23) { Location location = context.getLocation(element); context.report(ALLOW_BACKUP, element, location, "Should explicitly set `android:fullBackupContent` to `true` or `false` " + "to opt-in to or out of full app data back-up and restore, or " + "alternatively to an `@xml` resource which specifies which " + "files to backup"); } else if (fullBackupNode == null && hasGcmReceiver(element)) { Location location = context.getLocation(element); context.report(ALLOW_BACKUP, element, location, "Should explicitly set `android:fullBackupContent` to avoid backing up " + "the GCM device specific regId."); } } else if (mSeenApplication) { if (context.isEnabled(ORDER)) { context.report(ORDER, element, context.getLocation(element), String.format("`<%1$s>` tag appears after `<application>` tag", tag)); } // Don't complain for *every* element following the <application> tag mSeenApplication = false; } if (tag.equals(TAG_USES_FEATURE)) { Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); if (nameNode != null) { String name = nameNode.getValue(); if (!name.isEmpty()) { if (mUsesFeatures.contains(name)) { String message = String.format( "Duplicate declaration of uses-feature `%1$s`", name); context.report(DUPLICATE_USES_FEATURE, element, context.getLocation(nameNode), message); } else { mUsesFeatures.add(name); } } } } } /** * Returns true if the given application element has a receiver with an intent filter * action for GCM receive */ private static boolean hasGcmReceiver(@NonNull Element application) { NodeList applicationChildren = application.getChildNodes(); for (int i1 = 0, n1 = applicationChildren.getLength(); i1 < n1; i1++) { Node applicationChild = applicationChildren.item(i1); if (applicationChild.getNodeType() == Node.ELEMENT_NODE && TAG_RECEIVER.equals(applicationChild.getNodeName())) { NodeList receiverChildren = applicationChild.getChildNodes(); for (int i2 = 0, n2 = receiverChildren.getLength(); i2 < n2; i2++) { Node receiverChild = receiverChildren.item(i2); if (receiverChild.getNodeType() == Node.ELEMENT_NODE && TAG_INTENT_FILTER.equals(receiverChild.getNodeName())) { NodeList filterChildren = receiverChild.getChildNodes(); for (int i3 = 0, n3 = filterChildren.getLength(); i3 < n3; i3++) { Node filterChild = filterChildren.item(i3); if (filterChild.getNodeType() == Node.ELEMENT_NODE && NODE_ACTION.equals(filterChild.getNodeName())) { Element action = (Element) filterChild; String name = action.getAttributeNS(ANDROID_URI, ATTR_NAME); if ("com.google.android.c2dm.intent.RECEIVE".equals(name)) { return true; } } } } } } } return false; } private static void checkMipmapIcon(@NonNull XmlContext context, @NonNull Element element) { Attr attribute = element.getAttributeNodeNS(ANDROID_URI, ATTR_ICON); if (attribute == null) { return; } String icon = attribute.getValue(); if (icon.startsWith(DRAWABLE_PREFIX)) { if (TAG_ACTIVITY.equals(element.getTagName()) && !isLaunchableActivity(element)) { return; } if (context.isEnabled(MIPMAP) // Only complain if this app is skipping some densities && context.getProject().getApplicableDensities() != null) { context.report(MIPMAP, element, context.getLocation(attribute), "Should use `@mipmap` instead of `@drawable` for launcher icons"); } } } @SuppressWarnings("SpellCheckingInspection") private static boolean isLaunchableActivity(@NonNull Element element) { if (!TAG_ACTIVITY.equals(element.getTagName())) { return false; } for (Element child : LintUtils.getChildren(element)) { if (child.getTagName().equals(TAG_INTENT_FILTER)) { for (Element innerChild : LintUtils.getChildren(child)) { if (innerChild.getTagName().equals("category")) { //$NON-NLS-1$ String categoryString = innerChild.getAttributeNS(ANDROID_URI, ATTR_NAME); return "android.intent.category.LAUNCHER".equals(categoryString); } } } } return false; } /** Returns true iff the given manifest file is the main manifest file */ private static boolean isMainManifest(XmlContext context, File manifestFile) { if (!context.getProject().isGradleProject()) { // In non-gradle projects, just one manifest per project return true; } AndroidProject model = context.getProject().getGradleProjectModel(); return model == null || manifestFile .equals(model.getDefaultConfig().getSourceProvider().getManifestFile()); } /** * Returns true iff the given manifest file is in a debug-specific source set, * or a test source set */ private static boolean isDebugOrTestManifest( @NonNull XmlContext context, @NonNull File manifestFile) { AndroidProject model = context.getProject().getGradleProjectModel(); if (model != null) { // Quickly check if it's the main manifest first; that's the most likely scenario if (manifestFile.equals(model.getDefaultConfig().getSourceProvider().getManifestFile())) { return false; } // Debug build type? for (BuildTypeContainer container : model.getBuildTypes()) { if (container.getBuildType().isDebuggable()) { if (manifestFile.equals(container.getSourceProvider().getManifestFile())) { return true; } } } // Test source set? for (ProductFlavorContainer container : model.getProductFlavors()) { for (SourceProviderContainer extra : container.getExtraSourceProviders()) { String artifactName = extra.getArtifactName(); if (AndroidProject.ARTIFACT_ANDROID_TEST.equals(artifactName) && manifestFile.equals(extra.getSourceProvider().getManifestFile())) { return true; } } } } return false; } private static void checkDeviceAdmin(XmlContext context, Element element) { List<Element> children = LintUtils.getChildren(element); boolean requiredIntentFilterFound = false; boolean deviceAdmin = false; Attr locationNode = null; for (Element child : children) { String tagName = child.getTagName(); if (tagName.equals(TAG_INTENT_FILTER) && !requiredIntentFilterFound) { boolean dataFound = false; boolean actionFound = false; for (Element filterChild : LintUtils.getChildren(child)) { String filterTag = filterChild.getTagName(); if (filterTag.equals(NODE_ACTION)) { String name = filterChild.getAttributeNS(ANDROID_URI, ATTR_NAME); if ("android.app.action.DEVICE_ADMIN_ENABLED".equals(name)) { //$NON-NLS-1$ actionFound = true; } } else if (filterTag.equals(NODE_DATA)) { dataFound = true; } } if (actionFound && !dataFound) { requiredIntentFilterFound = true; } } else if (tagName.equals(NODE_METADATA)) { Attr valueNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); if (valueNode != null) { String name = valueNode.getValue(); if ("android.app.device_admin".equals(name)) { //$NON-NLS-1$ deviceAdmin = true; locationNode = valueNode; } } } } if (deviceAdmin && !requiredIntentFilterFound && context.isEnabled(DEVICE_ADMIN)) { context.report(DEVICE_ADMIN, locationNode, context.getLocation(locationNode), "You must have an intent filter for action " + "`android.app.action.DEVICE_ADMIN_ENABLED`"); } } }