blob: 9425ea33e20c0cecf020275577e0b4ac367e7e3a [file] [log] [blame]
/*
* Copyright (C) 2016 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.VALUE_FALSE;
import static com.android.xml.AndroidManifest.ATTRIBUTE_REQUIRED;
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.LintFix;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.XmlContext;
import com.android.tools.lint.detector.api.XmlScanner;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
/** Detects various issues for Chrome OS devices. */
public class ChromeOsDetector extends Detector implements XmlScanner {
private static final Implementation IMPLEMENTATION =
new Implementation(ChromeOsDetector.class, Scope.MANIFEST_SCOPE);
/** Using hardware unsupported by Chrome OS devices */
public static final Issue UNSUPPORTED_CHROME_OS_HARDWARE =
Issue.create(
"UnsupportedChromeOsHardware",
"Unsupported Chrome OS Hardware Feature",
"The `<uses-feature>` element should not require this unsupported Chrome OS 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 Chrome OS device are reviewed and marked as "
+ "not required in the manifest.",
Category.CHROME_OS,
6,
Severity.ERROR,
IMPLEMENTATION)
.setEnabledByDefault(false)
.addMoreInfo(
"https://developer.android.com/topic/arc/manifest.html#incompat-entries");
/** Permission implies required hardware unsupported by Chrome OS */
public static final Issue PERMISSION_IMPLIES_UNSUPPORTED_HARDWARE =
Issue.create(
"PermissionImpliesUnsupportedChromeOsHardware",
"Permission Implies Unsupported Chrome OS Hardware",
"The `<uses-permission>` element should not require a permission that implies an "
+ "unsupported Chrome OS 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.CHROME_OS,
3,
Severity.ERROR,
IMPLEMENTATION)
.setEnabledByDefault(false)
.addMoreInfo(
"https://developer.android.com/topic/arc/manifest.html#implied-features");
private static final String HARDWARE_FEATURE_CAMERA = "android.hardware.camera";
private static final String HARDWARE_FEATURE_CAMERA_AUTOFOCUS =
"android.hardware.camera.autofocus";
private static final String HARDWARE_FEATURE_TELEPHONY = "android.hardware.telephony";
private static final String ANDROID_PERMISSION_CAMERA = "android.permission.CAMERA";
// https://developer.android.com/topic/arc/manifest.html#incompat-entries
private static final String[] UNSUPPORTED_HARDWARE_FEATURES =
new String[] {
HARDWARE_FEATURE_CAMERA,
HARDWARE_FEATURE_CAMERA_AUTOFOCUS,
"android.hardware.camera.capability.manual_post_processing",
"android.hardware.camera.capability.manual_sensor",
"android.hardware.camera.capability.raw",
"android.hardware.camera.flash",
"android.hardware.camera.level.full",
"android.hardware.consumerir",
"android.hardware.location.gps",
"android.hardware.nfc",
"android.hardware.nfc.hce",
"android.hardware.sensor.barometer",
HARDWARE_FEATURE_TELEPHONY,
"android.hardware.telephony.cdma",
"android.hardware.telephony.gsm",
"android.hardware.touchscreen",
"android.hardware.type.automotive",
"android.hardware.type.television",
"android.hardware.usb.accessory",
"android.hardware.usb.host",
// Partially-supported, only on some Chrome OS devices.
"android.hardware.sensor.accelerometer",
"android.hardware.sensor.compass",
"android.hardware.sensor.gyroscope",
"android.hardware.sensor.light",
"android.hardware.sensor.proximity",
"android.hardware.sensor.stepcounter",
"android.hardware.sensor.stepdetector",
// Software features not currently supported on Chrome OS devices.
"android.software.app_widgets",
"android.software.device_admin",
"android.software.home_screen",
"android.software.input_methods",
"android.software.leanback",
"android.software.live_wallpaper",
"android.software.live_tv",
"android.software.managed_users",
"android.software.midi",
"android.software.sip",
"android.software.sip.voip"
};
/** Constructs a new {@link ChromeOsDetector} check */
public ChromeOsDetector() {}
/** Used for {@link #PERMISSION_IMPLIES_UNSUPPORTED_HARDWARE} */
private boolean usesFeatureCamera;
/** Used for {@link #PERMISSION_IMPLIES_UNSUPPORTED_HARDWARE} */
private boolean usesFeatureCameraAutofocus;
/** All permissions that imply unsupported Chrome OS hardware. */
private List<String> unsupportedHardwareImpliedPermissions;
/** All Unsupported Chrome OS uses features in use by the current manifest. */
private Set<String> allUnsupportedChromeOsUsesFeatures;
/** Set containing unsupported Chrome OS uses-features elements without required="false" */
private Set<String> unsupportedChromeOsUsesFeatures;
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(NODE_USES_FEATURE, NODE_USES_PERMISSION);
}
@Override
public void beforeCheckFile(@NonNull Context context) {
usesFeatureCamera = false;
usesFeatureCameraAutofocus = false;
unsupportedHardwareImpliedPermissions = Lists.newArrayListWithExpectedSize(2);
unsupportedChromeOsUsesFeatures = Sets.newHashSetWithExpectedSize(2);
allUnsupportedChromeOsUsesFeatures = Sets.newHashSetWithExpectedSize(2);
}
@Override
public void afterCheckFile(@NonNull Context context) {
XmlContext xmlContext = (XmlContext) context;
if (!context.getMainProject().isLibrary()) {
// Report all unsupported Chrome OS hardware uses-feature.
// These point to all unsupported Chrome OS uses features that have not be marked
// required = false;
if (!unsupportedChromeOsUsesFeatures.isEmpty()
&& xmlContext.isEnabled(UNSUPPORTED_CHROME_OS_HARDWARE)) {
List<Element> usesFeatureElements =
AndroidTvDetector.findUsesFeatureElements(
unsupportedChromeOsUsesFeatures, xmlContext.document);
for (Element element : usesFeatureElements) {
Attr attrRequired = element.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_REQUIRED);
Location location =
attrRequired == null
? xmlContext.getNameLocation(element)
: xmlContext.getLocation(attrRequired);
LintFix fix =
fix().set(ANDROID_URI, ATTRIBUTE_REQUIRED, VALUE_FALSE)
.autoFix()
.build();
xmlContext.report(
UNSUPPORTED_CHROME_OS_HARDWARE,
element,
location,
"Expecting `android:required=\"false\"` for this hardware "
+ "feature that may not be supported by all Chrome OS "
+ "devices.",
fix);
}
}
// Report permissions implying unsupported hardware
if (!unsupportedHardwareImpliedPermissions.isEmpty()
&& xmlContext.isEnabled(PERMISSION_IMPLIES_UNSUPPORTED_HARDWARE)) {
Predicate<String> p =
(String input) -> {
// Special-case handling for camera permission - needs to check that
// both camera and camera autofocus features are present and set to
// android:required="false".
if (ANDROID_PERMISSION_CAMERA.equals(input)) {
return (!usesFeatureCamera || !usesFeatureCameraAutofocus);
}
// Filter out all permissions that already have their corresponding
// implied hardware declared in the AndroidManifest.xml.
String usesFeature =
input != null ? getImpliedUnsupportedHardware(input) : null;
return usesFeature != null
&& !allUnsupportedChromeOsUsesFeatures.contains(usesFeature);
};
Collection<String> filteredPermissions =
unsupportedHardwareImpliedPermissions
.stream()
.filter(p)
.collect(Collectors.toCollection(HashSet::new));
List<Element> permissionsWithoutUsesFeatures =
AndroidTvDetector.findPermissionElements(
filteredPermissions, xmlContext.document);
for (Element permissionElement : permissionsWithoutUsesFeatures) {
String name = permissionElement.getAttributeNS(ANDROID_URI, ATTR_NAME);
String[] unsupportedHardwareNames = new String[2];
unsupportedHardwareNames[0] = getImpliedUnsupportedHardware(name);
// Special-case handling of camera permission - either or both implied features
// might be missing.
if (ANDROID_PERMISSION_CAMERA.equals(name)) {
if (usesFeatureCamera) {
unsupportedHardwareNames[0] = null;
}
if (!usesFeatureCameraAutofocus) {
unsupportedHardwareNames[1] = HARDWARE_FEATURE_CAMERA_AUTOFOCUS;
}
}
for (String unsupportedHardwareName : unsupportedHardwareNames) {
if (unsupportedHardwareName != null) {
String message =
String.format(
"Permission exists without corresponding hardware `<uses-feature "
+ "android:name=\"%1$s\" required=\"false\">` tag.",
unsupportedHardwareName);
LintFix fix = fix().data(unsupportedHardwareName);
xmlContext.report(
PERMISSION_IMPLIES_UNSUPPORTED_HARDWARE,
permissionElement,
xmlContext.getNameLocation(permissionElement),
message,
fix);
}
}
}
}
}
}
@Nullable
private static String getImpliedUnsupportedHardware(@NonNull String permission) {
switch (permission) {
case ANDROID_PERMISSION_CAMERA:
return HARDWARE_FEATURE_CAMERA;
case "android.permission.CALL_PHONE":
return HARDWARE_FEATURE_TELEPHONY;
case "android.permission.CALL_PRIVILEGED":
return HARDWARE_FEATURE_TELEPHONY;
case "android.permission.MODIFY_PHONE_STATE":
return HARDWARE_FEATURE_TELEPHONY;
case "android.permission.PROCESS_OUTGOING_CALLS":
return HARDWARE_FEATURE_TELEPHONY;
case "android.permission.READ_SMS":
return HARDWARE_FEATURE_TELEPHONY;
case "android.permission.RECEIVE_SMS":
return HARDWARE_FEATURE_TELEPHONY;
case "android.permission.RECEIVE_MMS":
return HARDWARE_FEATURE_TELEPHONY;
case "android.permission.RECEIVE_WAP_PUSH":
return HARDWARE_FEATURE_TELEPHONY;
case "android.permission.SEND_SMS":
return HARDWARE_FEATURE_TELEPHONY;
case "android.permission.WRITE_APN_SETTINGS":
return HARDWARE_FEATURE_TELEPHONY;
case "android.permission.WRITE_SMS":
return HARDWARE_FEATURE_TELEPHONY;
default:
return null;
}
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
String elementName = element.getTagName();
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)) {
allUnsupportedChromeOsUsesFeatures.add(featureName);
Attr required = element.getAttributeNodeNS(ANDROID_URI, ATTRIBUTE_REQUIRED);
if (required == null || Boolean.parseBoolean(required.getValue())) {
unsupportedChromeOsUsesFeatures.add(featureName);
}
// Special-case tracking of features implicitly needed by camera permission.
if (HARDWARE_FEATURE_CAMERA.equals(featureName)) {
usesFeatureCamera = true;
}
if (HARDWARE_FEATURE_CAMERA_AUTOFOCUS.equals(featureName)) {
usesFeatureCameraAutofocus = true;
}
}
}
} else if (NODE_USES_PERMISSION.equals(elementName)) {
// Store all <uses-permission> tags that imply unsupported hardware
String permissionName = element.getAttributeNS(ANDROID_URI, ATTR_NAME);
if (getImpliedUnsupportedHardware(permissionName) != null) {
unsupportedHardwareImpliedPermissions.add(permissionName);
}
}
}
private static boolean isUnsupportedHardwareFeature(@NonNull String featureName) {
for (String prefix : UNSUPPORTED_HARDWARE_FEATURES) {
if (featureName.startsWith(prefix)) {
return true;
}
}
return false;
}
}