blob: 6d9de1c63aa840e905c556ddd4915304ba1a64a8 [file] [log] [blame]
/*
* Copyright (C) 2012 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_AUTO_TEXT;
import static com.android.SdkConstants.ATTR_BUFFER_TYPE;
import static com.android.SdkConstants.ATTR_CAPITALIZE;
import static com.android.SdkConstants.ATTR_CURSOR_VISIBLE;
import static com.android.SdkConstants.ATTR_DIGITS;
import static com.android.SdkConstants.ATTR_EDITABLE;
import static com.android.SdkConstants.ATTR_EDITOR_EXTRAS;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_IME_ACTION_ID;
import static com.android.SdkConstants.ATTR_IME_ACTION_LABEL;
import static com.android.SdkConstants.ATTR_IME_OPTIONS;
import static com.android.SdkConstants.ATTR_INPUT_METHOD;
import static com.android.SdkConstants.ATTR_INPUT_TYPE;
import static com.android.SdkConstants.ATTR_NUMERIC;
import static com.android.SdkConstants.ATTR_ON_CLICK;
import static com.android.SdkConstants.ATTR_PASSWORD;
import static com.android.SdkConstants.ATTR_PHONE_NUMBER;
import static com.android.SdkConstants.ATTR_PRIVATE_IME_OPTIONS;
import static com.android.SdkConstants.ATTR_TEXT;
import static com.android.SdkConstants.ATTR_TEXT_IS_SELECTABLE;
import static com.android.SdkConstants.ATTR_VISIBILITY;
import static com.android.SdkConstants.BUTTON;
import static com.android.SdkConstants.CHECKED_TEXT_VIEW;
import static com.android.SdkConstants.CHECK_BOX;
import static com.android.SdkConstants.RADIO_BUTTON;
import static com.android.SdkConstants.SWITCH;
import static com.android.SdkConstants.TEXT_VIEW;
import static com.android.SdkConstants.TOGGLE_BUTTON;
import static com.android.SdkConstants.VALUE_EDITABLE;
import static com.android.SdkConstants.VALUE_NONE;
import static com.android.SdkConstants.VALUE_TRUE;
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.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 java.util.Arrays;
import java.util.Collection;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
/** Checks for cases where a TextView should probably be an EditText instead */
public class TextViewDetector extends LayoutDetector {
private static final Implementation IMPLEMENTATION =
new Implementation(TextViewDetector.class, Scope.RESOURCE_FILE_SCOPE);
/** The main issue discovered by this detector */
public static final Issue ISSUE =
Issue.create(
"TextViewEdits",
"TextView should probably be an EditText instead",
"Using a `<TextView>` to input text is generally an error, you should be "
+ "using `<EditText>` instead. `EditText` is a subclass of `TextView`, and some "
+ "of the editing support is provided by `TextView`, so it's possible to set "
+ "some input-related properties on a `TextView`. However, using a `TextView` "
+ "along with input attributes is usually a cut & paste error. To input "
+ "text you should be using `<EditText>`.\n"
+ "\n"
+ "This check also checks subclasses of `TextView`, such as `Button` and `CheckBox`, "
+ "since these have the same issue: they should not be used with editable "
+ "attributes.",
Category.CORRECTNESS,
7,
Severity.WARNING,
IMPLEMENTATION);
/** Text could be selectable */
public static final Issue SELECTABLE =
Issue.create(
"SelectableText",
"Dynamic text should probably be selectable",
"If a `<TextView>` is used to display data, the user might want to copy that "
+ "data and paste it elsewhere. To allow this, the `<TextView>` should specify "
+ "`android:textIsSelectable=\"true\"`.\n"
+ "\n"
+ "This lint check looks for TextViews which are likely to be displaying data: "
+ "views whose text is set dynamically. This value will be ignored on platforms "
+ "older than API 11, so it is okay to set it regardless of your `minSdkVersion`.",
Category.USABILITY,
7,
Severity.WARNING,
IMPLEMENTATION)
// Apparently setting this can have some undesirable side effects
.setEnabledByDefault(false);
/** Constructs a new {@link TextViewDetector} */
public TextViewDetector() {}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(
TEXT_VIEW,
BUTTON,
TOGGLE_BUTTON,
CHECK_BOX,
RADIO_BUTTON,
CHECKED_TEXT_VIEW,
SWITCH);
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
if (element.getTagName().equals(TEXT_VIEW)) {
if (!element.hasAttributeNS(ANDROID_URI, ATTR_TEXT)
&& element.hasAttributeNS(ANDROID_URI, ATTR_ID)
&& !element.hasAttributeNS(ANDROID_URI, ATTR_TEXT_IS_SELECTABLE)
&& !element.hasAttributeNS(ANDROID_URI, ATTR_VISIBILITY)
&& !element.hasAttributeNS(ANDROID_URI, ATTR_ON_CLICK)
&& context.getMainProject().getTargetSdk() >= 11
&& context.isEnabled(SELECTABLE)) {
LintFix fix = fix().set(ANDROID_URI, ATTR_TEXT_IS_SELECTABLE, VALUE_TRUE).build();
context.report(
SELECTABLE,
element,
context.getNameLocation(element),
"Consider making the text value selectable by specifying "
+ "`android:textIsSelectable=\"true\"`",
fix);
}
}
NamedNodeMap attributes = element.getAttributes();
for (int i = 0, n = attributes.getLength(); i < n; i++) {
Attr attribute = (Attr) attributes.item(i);
String name = attribute.getLocalName();
if (name == null || name.isEmpty()) {
// Attribute not in a namespace; we only care about the android: ones
continue;
}
boolean isEditAttribute = false;
switch (name.charAt(0)) {
case 'a':
{
isEditAttribute = name.equals(ATTR_AUTO_TEXT);
break;
}
case 'b':
{
isEditAttribute =
name.equals(ATTR_BUFFER_TYPE)
&& attribute.getValue().equals(VALUE_EDITABLE);
break;
}
case 'p':
{
isEditAttribute =
name.equals(ATTR_PASSWORD)
|| name.equals(ATTR_PHONE_NUMBER)
|| name.equals(ATTR_PRIVATE_IME_OPTIONS);
break;
}
case 'c':
{
isEditAttribute =
name.equals(ATTR_CAPITALIZE) || name.equals(ATTR_CURSOR_VISIBLE);
break;
}
case 'd':
{
isEditAttribute = name.equals(ATTR_DIGITS);
break;
}
case 'e':
{
if (name.equals(ATTR_EDITABLE)) {
isEditAttribute = attribute.getValue().equals(VALUE_TRUE);
} else {
isEditAttribute = name.equals(ATTR_EDITOR_EXTRAS);
}
break;
}
case 'i':
{
if (name.equals(ATTR_INPUT_TYPE)) {
String value = attribute.getValue();
isEditAttribute = !value.isEmpty() && !value.equals(VALUE_NONE);
} else {
isEditAttribute =
name.equals(ATTR_INPUT_TYPE)
|| name.equals(ATTR_IME_OPTIONS)
|| name.equals(ATTR_IME_ACTION_LABEL)
|| name.equals(ATTR_IME_ACTION_ID)
|| name.equals(ATTR_INPUT_METHOD);
}
break;
}
case 'n':
{
isEditAttribute = name.equals(ATTR_NUMERIC);
break;
}
}
if (isEditAttribute
&& ANDROID_URI.equals(attribute.getNamespaceURI())
&& context.isEnabled(ISSUE)) {
Location location = context.getLocation(attribute);
String message;
String view = element.getTagName();
if (view.equals(TEXT_VIEW)) {
message =
String.format(
"Attribute `%1$s` should not be used with `<TextView>`: "
+ "Change element type to `<EditText>` ?",
attribute.getName());
} else {
message =
String.format(
"Attribute `%1$s` should not be used with `<%2$s>`: "
+ "intended for editable text widgets",
attribute.getName(), view);
}
context.report(ISSUE, attribute, location, message);
}
}
}
}