blob: 66537f507fa7f0719ee35f3e230ee2e2b7a5fc18 [file] [log] [blame]
/*
* 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`");
}
}
}