Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright (C) 2015 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_URI;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.TAG_USES_FEATURE;
import static com.android.SdkConstants.TAG_USES_PERMISSION;
import static com.android.tools.lint.detector.api.TextFormat.RAW;
import static com.android.xml.AndroidManifest.ATTRIBUTE_REQUIRED;
import static com.android.xml.AndroidManifest.NODE_ACTIVITY;
import static com.android.xml.AndroidManifest.NODE_APPLICATION;
import static com.android.xml.AndroidManifest.NODE_CATEGORY;
import static com.android.xml.AndroidManifest.NODE_INTENT;
import static com.android.xml.AndroidManifest.NODE_USES_FEATURE;
import static com.android.xml.AndroidManifest.NODE_USES_PERMISSION;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
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.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.TextFormat;
import com.android.tools.lint.detector.api.XmlContext;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Detects various issues for Android TV.
*/
public class AndroidTvDetector extends Detector implements Detector.XmlScanner {
private static final Implementation IMPLEMENTATION = new Implementation(
AndroidTvDetector.class,
Scope.MANIFEST_SCOPE
);
/** Using hardware unsupported by TV */
public static final Issue UNSUPPORTED_TV_HARDWARE = Issue.create(
"UnsupportedTvHardware", //$NON-NLS-1$
"Unsupported TV Hardware Feature",
"The element should not require this unsupported TV hardware feature. " +
"Any uses-feature not explicitly marked with required=\"false\" is necessary on the " +
"device to be installed on. " +
"Ensure that any features that might prevent it from being installed on a TV device " +
"are reviewed and marked as not required in the manifest.",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPLEMENTATION).addMoreInfo(
"https://developer.android.com/training/tv/start/hardware.html#unsupported-features");
/** Missing leanback launcher intent filter */
public static final Issue MISSING_LEANBACK_LAUNCHER = Issue.create(
"MissingLeanbackLauncher", //$NON-NLS-1$
"Missing Leanback Launcher Intent Filter.",
"An application intended to run on TV devices must declare a launcher activity " +
"for TV in its manifest using a `android.intent.category.LEANBACK_LAUNCHER` " +
"intent filter.",
Category.CORRECTNESS,
8,
Severity.ERROR,
IMPLEMENTATION)
.addMoreInfo("https://developer.android.com/training/tv/start/start.html#tv-activity");
/** Missing leanback support */
public static final Issue MISSING_LEANBACK_SUPPORT = Issue.create(
"MissingLeanbackSupport", //$NON-NLS-1$
"Missing Leanback Support.",
"The manifest should declare the use of the Leanback user interface required " +
"by Android TV.\n" +
"To fix this, add\n" +
"``\n" +
"to your manifest.",
Category.CORRECTNESS,
6,
Severity.ERROR,
IMPLEMENTATION)
.addMoreInfo("https://developer.android.com/training/tv/start/start.html#leanback-req");
/** Permission implies required hardware unsupported by TV */
public static final Issue PERMISSION_IMPLIES_UNSUPPORTED_HARDWARE = Issue.create(
"PermissionImpliesUnsupportedHardware", //$NON-NLS-1$
"Permission Implies Unsupported Hardware",
"The element should not require a permission that implies an " +
"unsupported TV hardware feature. Google Play assumes that certain hardware related " +
"permissions indicate that the underlying hardware features are required by default. " +
"To fix the issue, consider declaring the corresponding uses-feature element with " +
"required=\"false\" attribute.",
Category.CORRECTNESS,
3,
Severity.WARNING,
IMPLEMENTATION).addMoreInfo(
"http://developer.android.com/guide/topics/manifest/uses-feature-element.html#permissions");
/** Missing banner attibute */
public static final Issue MISSING_BANNER = Issue.create(
"MissingTvBanner", //$NON-NLS-1$
"TV Missing Banner",
"A TV application must provide a home screen banner for each localization if it " +
"includes a Leanback launcher intent filter. The banner is the app launch point that " +
"appears on the home screen in the apps and games rows.",
Category.CORRECTNESS,
5,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo("http://developer.android.com/training/tv/start/start.html#banner");
public static final String SOFTWARE_FEATURE_LEANBACK =
"android.software.leanback"; //$NON-NLS-1$
private static final String LEANBACK_LIB_ARTIFACT =
"com.android.support:leanback-v17"; //$NON-NLS-1$
private static final String CATEGORY_LEANBACK_LAUNCHER =
"android.intent.category.LEANBACK_LAUNCHER"; //$NON-NLS-1$
private static final String HARDWARE_FEATURE_CAMERA = "android.hardware.camera"; //$NON-NLS-1$
private static final String HARDWARE_FEATURE_LOCATION_GPS =
"android.hardware.location.gps"; //$NON-NLS-1$
private static final String ANDROID_HARDWARE_TELEPHONY =
"android.hardware.telephony"; //$NON-NLS-1$
private static final String ANDROID_HARDWARE_BLUETOOTH =
"android.hardware.bluetooth"; //$NON-NLS-1$
private static final String ATTR_BANNER = "banner"; //$NON-NLS-1$
private static final String ANDROID_HARDWARE_MICROPHONE =
"android.hardware.microphone"; //$NON-NLS-1$
// https://developer.android.com/training/tv/start/hardware.html
private static final Set UNSUPPORTED_HARDWARE_FEATURES =
ImmutableSet.builder()
.add("android.hardware.touchscreen") //$NON-NLS-1$
.add("android.hardware.faketouch") //$NON-NLS-1$
.add(ANDROID_HARDWARE_TELEPHONY)
.add(HARDWARE_FEATURE_CAMERA)
.add(ANDROID_HARDWARE_BLUETOOTH)
.add("android.hardware.nfc") //$NON-NLS-1$
.add(HARDWARE_FEATURE_LOCATION_GPS) //$NON-NLS-1$
.add(ANDROID_HARDWARE_MICROPHONE) //$NON-NLS-1$
.add("android.hardware.sensors") //$NON-NLS-1$
.build();
private static final Map PERMISSIONS_TO_IMPLIED_UNSUPPORTED_HARDWARE =
ImmutableMap.builder()
.put("android.permission.BLUETOOTH", //$NON-NLS-1$
ANDROID_HARDWARE_BLUETOOTH)
.put("android.permission.BLUETOOTH_ADMIN", //$NON-NLS-1$
ANDROID_HARDWARE_BLUETOOTH)
.put("android.permission.CAMERA", //$NON-NLS-1$
HARDWARE_FEATURE_CAMERA)
.put("android.permission.RECORD_AUDIO", //$NON-NLS-1$
ANDROID_HARDWARE_MICROPHONE)
.put("android.permission.ACCESS_FINE_LOCATION", //$NON-NLS-1$
HARDWARE_FEATURE_LOCATION_GPS)
.put("android.permission.CALL_PHONE", //$NON-NLS-1$
ANDROID_HARDWARE_TELEPHONY)
.put("android.permission.CALL_PRIVILEGED", //$NON-NLS-1$
ANDROID_HARDWARE_TELEPHONY)
.put("android.permission.PROCESS_OUTGOING_CALLS", //$NON-NLS-1$
ANDROID_HARDWARE_TELEPHONY)
.put("android.permission.READ_SMS", //$NON-NLS-1$
ANDROID_HARDWARE_TELEPHONY)
.put("android.permission.RECEIVE_SMS", //$NON-NLS-1$
ANDROID_HARDWARE_TELEPHONY)
.put("android.permission.RECEIVE_MMS", //$NON-NLS-1$
ANDROID_HARDWARE_TELEPHONY)
.put("android.permission.RECEIVE_WAP_PUSH", //$NON-NLS-1$
ANDROID_HARDWARE_TELEPHONY)
.put("android.permission.SEND_SMS", //$NON-NLS-1$
ANDROID_HARDWARE_TELEPHONY)
.put("android.permission.WRITE_APN_SETTINGS", //$NON-NLS-1$
ANDROID_HARDWARE_TELEPHONY)
.put("android.permission.WRITE_SMS", //$NON-NLS-1$
ANDROID_HARDWARE_TELEPHONY)
.build();
/**
* If you change number of parameters or order, update
* {@link #getHardwareFeature(String, TextFormat)}
*/
private static final String USES_HARDWARE_ERROR_MESSAGE_FORMAT =
"Permission exists without corresponding hardware `` tag.";
/** Constructs a new {@link AndroidTvDetector} check */
public AndroidTvDetector() {
}
/** Used for {@link #MISSING_LEANBACK_LAUNCHER} */
private boolean mHasLeanbackLauncherActivity;
/** Used for {@link #MISSING_LEANBACK_SUPPORT} */
private boolean mHasLeanbackSupport;
/** Whether the app has a leanback-v7 dependency */
private boolean mHasLeanbackDependency;
/** Used for {@link #MISSING_BANNER} */
private boolean mHasApplicationBanner;
/** No. of activities that have the leanback intent but
* dont declare banners */
private int mLeanbackActivitiesWithoutBanners;
/** All permissions that imply unsupported tv hardware. */
private List mUnsupportedHardwareImpliedPermissions;
/** All Unsupported TV uses features in use by the current manifest.*/
private Set mAllUnsupportedTvUsesFeatures;
/** Set containing unsupported TV uses-features elements without required="false" */
private Set mUnsupportedTvUsesFeatures;
@NonNull
@Override
public Speed getSpeed() {
return Speed.FAST;
}
@Override
public Collection getApplicableElements() {
return Arrays.asList(
NODE_APPLICATION,
NODE_ACTIVITY,
NODE_USES_FEATURE,
NODE_USES_PERMISSION
);
}
@Override
public void beforeCheckFile(@NonNull Context context) {
mHasLeanbackLauncherActivity = false;
mHasLeanbackSupport = false;
mHasApplicationBanner = false;
mLeanbackActivitiesWithoutBanners = 0;
mUnsupportedHardwareImpliedPermissions = Lists.newArrayListWithExpectedSize(2);
mUnsupportedTvUsesFeatures = Sets.newHashSetWithExpectedSize(2);
mAllUnsupportedTvUsesFeatures = Sets.newHashSetWithExpectedSize(2);
// Check gradle dependency
Project mainProject = context.getMainProject();
mHasLeanbackDependency = (mainProject.isGradleProject()
&& Boolean.TRUE.equals(mainProject.dependsOn(LEANBACK_LIB_ARTIFACT)));
}
@Override
public void afterCheckFile(@NonNull Context context) {
boolean isTvApp = mHasLeanbackSupport
|| mHasLeanbackDependency
|| mHasLeanbackLauncherActivity;
if (!context.getMainProject().isLibrary() && isTvApp) {
XmlContext xmlContext = (XmlContext) context;
// Report an error if there's not at least one leanback launcher intent filter activity
if (!mHasLeanbackLauncherActivity
&& xmlContext.isEnabled(MISSING_LEANBACK_LAUNCHER)) {
// No launch activity
Node manifestNode = xmlContext.document.getDocumentElement();
if (manifestNode != null) {
xmlContext.report(MISSING_LEANBACK_LAUNCHER, manifestNode,
xmlContext.getLocation(manifestNode),
"Expecting an activity to have `" + CATEGORY_LEANBACK_LAUNCHER +
"` intent filter.");
}
}
// Report an issue if there is no leanback tag.
if (!mHasLeanbackSupport
&& xmlContext.isEnabled(MISSING_LEANBACK_SUPPORT)) {
Node manifestNode = xmlContext.document.getDocumentElement();
if (manifestNode != null) {
xmlContext.report(MISSING_LEANBACK_SUPPORT, manifestNode,
xmlContext.getLocation(manifestNode),
"Expecting tag.");
}
}
// Report missing banners
if (!mHasApplicationBanner // no application banner
&& mLeanbackActivitiesWithoutBanners > 0 // leanback activity without banner
&& xmlContext.isEnabled(MISSING_BANNER)) {
Node applicationElement = getApplicationElement(xmlContext.document);
if (applicationElement != null) {
xmlContext.report(MISSING_BANNER, applicationElement,
xmlContext.getLocation(applicationElement),
"Expecting `android:banner` with the `` tag or each "
+ "Leanback launcher activity.");
}
}
// Report all unsupported TV hardware uses-feature.
// These point to all unsupported tv uses features that have not be marked
// required = false;
if (!mUnsupportedTvUsesFeatures.isEmpty()
&& xmlContext.isEnabled(UNSUPPORTED_TV_HARDWARE)) {
List usesFeatureElements =
findUsesFeatureElements(mUnsupportedTvUsesFeatures, xmlContext.document);
for (Element element : usesFeatureElements) {
Attr attrRequired = element.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_REQUIRED);
Node location = attrRequired == null ? element : attrRequired;
xmlContext.report(UNSUPPORTED_TV_HARDWARE, location,
xmlContext.getLocation(location),
"Expecting `android:required=\"false\"` for this hardware "
+ "feature that may not be supported by all Android TVs.");
}
}
// Report permissions implying unsupported hardware
if (!mUnsupportedHardwareImpliedPermissions.isEmpty()
&& xmlContext.isEnabled(PERMISSION_IMPLIES_UNSUPPORTED_HARDWARE)) {
Collection filteredPermissions = Collections2.filter(
mUnsupportedHardwareImpliedPermissions,
new Predicate() {
@Override
public boolean apply(String input) {
// Filter out all permissions that already have their
// corresponding implied hardware declared in
// the AndroidManifest.xml
String usesFeature =
PERMISSIONS_TO_IMPLIED_UNSUPPORTED_HARDWARE.get(input);
return usesFeature != null
&& !mAllUnsupportedTvUsesFeatures.contains(usesFeature);
}
});
List permissionsWithoutUsesFeatures =
findPermissionElements(filteredPermissions, xmlContext.document);
for (Element permissionElement : permissionsWithoutUsesFeatures) {
String name = permissionElement.getAttributeNS(ANDROID_URI, ATTR_NAME);
String unsupportedHardwareName =
PERMISSIONS_TO_IMPLIED_UNSUPPORTED_HARDWARE.get(name);
if (unsupportedHardwareName != null) {
String message = String.format(
USES_HARDWARE_ERROR_MESSAGE_FORMAT, unsupportedHardwareName);
xmlContext.report(PERMISSION_IMPLIES_UNSUPPORTED_HARDWARE,
permissionElement,
xmlContext.getLocation(permissionElement), message);
}
}
}
}
}
private static List findPermissionElements(Collection permissions,
Document document) {
Node manifestElement = document.getDocumentElement();
if (manifestElement == null) {
return Collections.emptyList();
}
List nodes = new ArrayList(permissions.size());
for (Element child : LintUtils.getChildren(manifestElement)) {
if (TAG_USES_PERMISSION.equals(child.getTagName())
&& permissions.contains(child.getAttributeNS(ANDROID_URI, ATTR_NAME))) {
nodes.add(child);
}
}
return nodes;
}
/**
* Method to find all matching uses-feature elements in one go.
* Rather than iterating over the entire list of child nodes only to return the one that
* match a particular featureName, we use this method to iterate and return all the
* uses-feature elements of interest in a single iteration of the manifest element's children.
*
* @param featureNames The set of all features to look for inside the
* <manifest> node of the document.
* @param document The document/root node to use for iterating.
* @return A list of all <uses-feature> elements that match the featureNames.
*/
private static List findUsesFeatureElements(@NonNull Set featureNames,
@NonNull Document document) {
Node manifestElement = document.getDocumentElement();
if (manifestElement == null) {
return Collections.emptyList();
}
List nodes = new ArrayList(featureNames.size());
for (Element child : LintUtils.getChildren(manifestElement)) {
if (TAG_USES_FEATURE.equals(child.getTagName())
&& featureNames.contains(child.getAttributeNS(ANDROID_URI, ATTR_NAME))) {
nodes.add(child);
}
}
return nodes;
}
/**
* @param document The root of the document.
* @return The Node pointing to the {@link com.android.xml.AndroidManifest#NODE_APPLICATION}
* of the document.
*/
private static Node getApplicationElement(Document document) {
Node manifestNode = document.getDocumentElement();
if (manifestNode != null) {
return getElementWithTagName(NODE_APPLICATION, manifestNode);
}
return null;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String elementName = element.getTagName();
if (NODE_APPLICATION.equals(elementName)) {
mHasApplicationBanner = element.hasAttributeNS(ANDROID_URI, ATTR_BANNER);
} else if (NODE_USES_FEATURE.equals(elementName)) {
// Ensures that unsupported hardware features aren't required.
Attr name = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME);
if (name != null) {
String featureName = name.getValue();
if (isUnsupportedHardwareFeature(featureName)) {
mAllUnsupportedTvUsesFeatures.add(featureName);
Attr required =
element.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_REQUIRED);
if (required == null || Boolean.parseBoolean(required.getValue())) {
mUnsupportedTvUsesFeatures.add(featureName);
}
}
}
if (!mHasLeanbackSupport && hasLeanbackSupport(element)) {
mHasLeanbackSupport = true;
}
} else if (NODE_ACTIVITY.equals(elementName) && hasLeanbackIntentFilter(element)) {
mHasLeanbackLauncherActivity = true;
// Since this activity has a leanback launcher intent filter,
// Make sure it has a home screen banner
if (!element.hasAttributeNS(ANDROID_URI, ATTR_BANNER)) {
mLeanbackActivitiesWithoutBanners++;
}
} else if (NODE_USES_PERMISSION.equals(elementName)) {
// Store all tags that imply unsupported hardware)
String permissionName = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (PERMISSIONS_TO_IMPLIED_UNSUPPORTED_HARDWARE.containsKey(permissionName)) {
mUnsupportedHardwareImpliedPermissions.add(permissionName);
}
}
}
private static boolean hasLeanbackSupport(Element element) {
assert NODE_USES_FEATURE.equals(element.getTagName()) : element.getTagName();
return SOFTWARE_FEATURE_LEANBACK.equals(element.getAttributeNS(ANDROID_URI, ATTR_NAME));
}
private static boolean isUnsupportedHardwareFeature(@NonNull String featureName) {
for (String prefix : UNSUPPORTED_HARDWARE_FEATURES) {
if (featureName.startsWith(prefix)) {
return true;
}
}
return false;
}
private static boolean hasLeanbackIntentFilter(@NonNull Node activityNode) {
assert NODE_ACTIVITY.equals(activityNode.getNodeName()) : activityNode.getNodeName();
// Visit every intent filter
for (Element activityChild : LintUtils.getChildren(activityNode)) {
if (NODE_INTENT.equals(activityChild.getNodeName())) {
for (Element intentFilterChild : LintUtils.getChildren(activityChild)) {
// Check to see if the category is the leanback launcher
String attrName = intentFilterChild.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (NODE_CATEGORY.equals(intentFilterChild.getNodeName())
&& CATEGORY_LEANBACK_LAUNCHER.equals(attrName)) {
return true;
}
}
}
}
return false;
}
/**
* Assumes that the node is a direct child of the given Node.
*/
private static Node getElementWithTagName(@NonNull String tagName, @NonNull Node node) {
for (Element child : LintUtils.getChildren(node)) {
if (tagName.equals(child.getTagName())) {
return child;
}
}
return null;
}
/**
* Given an error message created by this lint check, return the corresponding featureName
* that it suggests should be added.
* (Intended to support quickfix implementations for this lint check.)
*
* @param errorMessage The error message originally produced by this detector.
* @param format The format of the error message.
* @return the corresponding featureName, or null if not recognized
*/
@SuppressWarnings("unused") // Used by the IDE
@Nullable
public static String getHardwareFeature(@NonNull String errorMessage,
@NonNull TextFormat format) {
List parameters = LintUtils.getFormattedParameters(
RAW.convertTo(USES_HARDWARE_ERROR_MESSAGE_FORMAT, format),
errorMessage);
if (parameters.size() == 1) {
return parameters.get(0);
}
return null;
}
}