blob: cb88c2faf3447f60955e8a6f83275c488bcf96f0 [file] [log] [blame]
/*
* Copyright (C) 2018 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_AUTOFILL_HINTS;
import static com.android.SdkConstants.ATTR_IMPORTANT_FOR_AUTOFILL;
import static com.android.SdkConstants.EDIT_TEXT;
import static com.android.SdkConstants.VALUE_NO;
import static com.android.SdkConstants.VALUE_NO_EXCLUDE_DESCENDANTS;
import static com.android.SdkConstants.VALUE_YES;
import static com.android.SdkConstants.VALUE_YES_EXCLUDE_DESCENDANTS;
import com.android.annotations.NonNull;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.LayoutDetector;
import com.android.tools.lint.detector.api.LintFix;
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 java.util.Collection;
import java.util.Collections;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
public class AutofillDetector extends LayoutDetector {
private static final String MESSAGE = "Missing `autofillHints` attribute";
public static final Issue ISSUE =
Issue.create(
"Autofill",
"Use Autofill",
"Specify an `autofillHints` attribute when targeting SDK version 26 or "
+ "higher or explicitly specify that the view is not important for autofill. "
+ "Your app can help an autofill service classify the data correctly by "
+ "providing the meaning of each view that could be autofillable, such as "
+ "views representing usernames, passwords, credit card fields, email "
+ "addresses, etc.\n"
+ "\n"
+ "The hints can have any value, but it is recommended to use predefined "
+ "values like 'username' for a username or 'creditCardNumber' for a credit "
+ "card number. For a list of all predefined autofill hint constants, see the "
+ "`AUTOFILL_HINT_` constants in the `View` reference at "
+ "https://developer.android.com/reference/android/view/View.html.\n"
+ "\n"
+ "You can mark a view unimportant for autofill by specifying an "
+ "`importantForAutofill` attribute on that view or a parent view. See "
+ "https://developer.android.com/reference/android/view/View.html#setImportantForAutofill(int).",
Category.USABILITY,
3,
Severity.WARNING,
new Implementation(AutofillDetector.class, Scope.RESOURCE_FILE_SCOPE))
.addMoreInfo("https://developer.android.com/guide/topics/text/autofill.html");
public AutofillDetector() {}
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(EDIT_TEXT);
}
@Override
public void visitElement(@NonNull XmlContext xmlContext, @NonNull Element element) {
if (xmlContext.getProject().getTargetSdk() < 26) {
return;
}
String attrValue = element.getAttributeNS(ANDROID_URI, ATTR_IMPORTANT_FOR_AUTOFILL);
if (!attrValue.isEmpty()) {
if (attrValue.equals(VALUE_NO) || attrValue.equals(VALUE_NO_EXCLUDE_DESCENDANTS)) {
// The view is explicitly marked not important for autofill.
return;
}
if (attrValue.equals(VALUE_YES) || attrValue.equals(VALUE_YES_EXCLUDE_DESCENDANTS)) {
// The view is explicitly marked important for autofill. This meanas that anything
// a parent node declares about descendant autofill behavior is overridden.
checkForAutofillHints(element, xmlContext);
return;
}
}
// Check if a parent node has explicitly marked descendants unimportant for autofill.
boolean checkParent = true;
Node el = element.getParentNode();
while (el instanceof Element && checkParent) {
attrValue = ((Element) el).getAttributeNS(ANDROID_URI, ATTR_IMPORTANT_FOR_AUTOFILL);
if (!attrValue.isEmpty()) {
// Stop at the first ancestor that defines autofill behavior for descendants.
checkParent = false;
if (attrValue.equals(VALUE_NO_EXCLUDE_DESCENDANTS)
|| attrValue.equals(VALUE_YES_EXCLUDE_DESCENDANTS)) {
// The parent has explicitly marked descendants not important for autofill.
return;
}
}
el = el.getParentNode();
}
// If we get here, it means that neither the view nor a parent has excluded the view from
// autofill and the view needs to define autofill hints.
checkForAutofillHints(element, xmlContext);
}
private void checkForAutofillHints(Element element, XmlContext xmlContext) {
if (!element.hasAttributeNS(ANDROID_URI, ATTR_AUTOFILL_HINTS)) {
LintFix fix =
fix().alternatives(
// TODO: set hints based on id of view. Example, @+id/username
// suggests 'username' as hint value.
fix().set(ANDROID_URI, ATTR_AUTOFILL_HINTS, "")
.caretBegin()
.build(),
fix().set(ANDROID_URI, ATTR_IMPORTANT_FOR_AUTOFILL, "no")
.build());
xmlContext.report(ISSUE, element, xmlContext.getNameLocation(element), MESSAGE, fix);
}
}
}