blob: 80d3c7d6acca5819042916c07e693a96ee93a20f [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_STRING_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_BACKGROUND;
import static com.android.SdkConstants.ATTR_ID;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF;
import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF;
import static com.android.SdkConstants.ATTR_NAME;
import static com.android.SdkConstants.ATTR_ORIENTATION;
import static com.android.SdkConstants.ATTR_STYLE;
import static com.android.SdkConstants.ATTR_TEXT;
import static com.android.SdkConstants.BUTTON;
import static com.android.SdkConstants.LINEAR_LAYOUT;
import static com.android.SdkConstants.RELATIVE_LAYOUT;
import static com.android.SdkConstants.STRING_PREFIX;
import static com.android.SdkConstants.TABLE_ROW;
import static com.android.SdkConstants.TAG_STRING;
import static com.android.SdkConstants.VALUE_SELECTABLE_ITEM_BACKGROUND;
import static com.android.SdkConstants.VALUE_TRUE;
import static com.android.SdkConstants.VALUE_VERTICAL;
import static com.android.tools.lint.checks.RequiredAttributeDetector.PERCENT_RELATIVE_LAYOUT;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.resources.ResourceFolderType;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.Lint;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.ResourceXmlDetector;
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.utils.XmlUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* Check which looks at the order of buttons in dialogs and makes sure that "the dismissive action
* of a dialog is always on the left whereas the affirmative actions are on the right."
*
* <p>This only looks for the affirmative and dismissive actions named "OK" and "Cancel"; "Cancel"
* usually works, but the affirmative action often has many other names -- "Done", "Send", "Go",
* etc.
*
* <p>TODO: Perhaps we should look for Yes/No dialogs and suggested they be rephrased as Cancel/OK
* dialogs? Similarly, consider "Abort" a synonym for "Cancel" ?
*/
public class ButtonDetector extends ResourceXmlDetector {
/** Name of cancel value ("Cancel") */
private static final String CANCEL_LABEL = "Cancel";
/** Name of OK value ("Cancel") */
private static final String OK_LABEL = "OK";
/** Name of Back value ("Back") */
private static final String BACK_LABEL = "Back";
/** Yes */
private static final String YES_LABEL = "Yes";
/** No */
private static final String NO_LABEL = "No";
/** Layout text attribute reference to {@code @android:string/ok} */
private static final String ANDROID_OK_RESOURCE = ANDROID_STRING_PREFIX + "ok";
/** Layout text attribute reference to {@code @android:string/cancel} */
private static final String ANDROID_CANCEL_RESOURCE = ANDROID_STRING_PREFIX + "cancel";
/** Layout text attribute reference to {@code @android:string/yes} */
private static final String ANDROID_YES_RESOURCE = ANDROID_STRING_PREFIX + "yes";
/** Layout text attribute reference to {@code @android:string/no} */
private static final String ANDROID_NO_RESOURCE = ANDROID_STRING_PREFIX + "no";
private static final Implementation IMPLEMENTATION =
new Implementation(ButtonDetector.class, Scope.RESOURCE_FILE_SCOPE);
/** The main issue discovered by this detector */
public static final Issue ORDER =
Issue.create(
"ButtonOrder",
"Button order",
"According to the Android Design Guide,\n"
+ "\n"
+ "\"Action buttons are typically Cancel and/or OK, with OK indicating the preferred "
+ "or most likely action. However, if the options consist of specific actions such "
+ "as Close or Wait rather than a confirmation or cancellation of the action "
+ "described in the content, then all the buttons should be active verbs. As a rule, "
+ "the dismissive action of a dialog is always on the left whereas the affirmative "
+ "actions are on the right.\"\n"
+ "\n"
+ "This check looks for button bars and buttons which look like cancel buttons, "
+ "and makes sure that these are on the left.",
Category.USABILITY,
8,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo("https://material.io/components/dialogs/");
/** The main issue discovered by this detector */
public static final Issue STYLE =
Issue.create(
"ButtonStyle",
"Button should be borderless",
"Button bars typically use a borderless style for the buttons. Set the "
+ "`style=\"?android:attr/buttonBarButtonStyle\"` attribute "
+ "on each of the buttons, and set `style=\"?android:attr/buttonBarStyle\"` on "
+ "the parent layout",
Category.USABILITY,
5,
Severity.WARNING,
IMPLEMENTATION)
.addMoreInfo("https://material.io/components/dialogs/");
/** The main issue discovered by this detector */
public static final Issue BACK_BUTTON =
Issue.create(
"BackButton",
"Back button",
// TODO: Look for ">" as label suffixes as well
"According to the Android Design Guide,\n"
+ "\n"
+ "\"Other platforms use an explicit back button with label to allow the user "
+ "to navigate up the application's hierarchy. Instead, Android uses the main "
+ "action bar's app icon for hierarchical navigation and the navigation bar's "
+ "back button for temporal navigation.\""
+ "\n"
+ "This check is not very sophisticated (it just looks for buttons with the "
+ "label \"Back\"), so it is disabled by default to not trigger on common "
+ "scenarios like pairs of Back/Next buttons to paginate through screens.",
Category.USABILITY,
6,
Severity.WARNING,
IMPLEMENTATION)
.setEnabledByDefault(false)
.addMoreInfo("https://material.io/design/");
/** The main issue discovered by this detector */
public static final Issue CASE =
Issue.create(
"ButtonCase",
"Cancel/OK dialog button capitalization",
//noinspection LintImplTextFormat
"The standard capitalization for OK/Cancel dialogs is \"OK\" and \"Cancel\". "
+ "To ensure that your dialogs use the standard strings, you can use "
+ "the resource strings @android:string/ok and @android:string/cancel.",
Category.USABILITY,
2,
Severity.WARNING,
IMPLEMENTATION);
/** Set of resource names whose value was either OK or Cancel */
private Set<String> mApplicableResources;
/**
* Map of resource names we'd like resolved into strings in phase 2. The values should be filled
* in with the actual string contents.
*/
private Map<String, String> mKeyToLabel;
/**
* Set of elements we've already warned about. If we've already complained about a cancel
* button, don't also report the OK button (since it's listed for the warnings on OK buttons).
*/
private Set<Element> mIgnore;
/** Constructs a new {@link ButtonDetector} */
public ButtonDetector() {}
@Override
public Collection<String> getApplicableElements() {
return Arrays.asList(BUTTON, TAG_STRING);
}
@Override
public boolean appliesTo(@NonNull ResourceFolderType folderType) {
return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES;
}
@Override
public void afterCheckRootProject(@NonNull Context context) {
int phase = context.getPhase();
if (phase == 1 && mApplicableResources != null) {
// We found resources for the string "Cancel"; perform a second pass
// where we check layout text attributes against these strings.
context.getDriver().requestRepeat(this, Scope.RESOURCE_FILE_SCOPE);
}
}
private static String stripLabel(String text) {
text = text.trim();
if (text.length() > 2
&& (text.charAt(0) == '"' || text.charAt(0) == '\'')
&& (text.charAt(0) == text.charAt(text.length() - 1))) {
text = text.substring(1, text.length() - 1);
}
return text;
}
@Override
public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
// This detector works in two passes.
// In pass 1, it looks in layout files for hardcoded strings of "Cancel", or
// references to @string/cancel or @android:string/cancel.
// It also looks in values/ files for strings whose value is "Cancel",
// and if found, stores the corresponding keys in a map. (This is necessary
// since value files are processed after layout files).
// Then, if at the end of phase 1 any "Cancel" string resources were
// found in the value files, then it requests a *second* phase,
// where it looks only for <Button>'s whose text matches one of the
// cancel string resources.
int phase = context.getPhase();
String tagName = element.getTagName();
if (phase == 1 && tagName.equals(TAG_STRING)) {
NodeList childNodes = element.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
String text = child.getNodeValue();
for (int j = 0, len = text.length(); j < len; j++) {
char c = text.charAt(j);
if (!Character.isWhitespace(c)) {
if (c == '"' || c == '\'') {
continue;
}
if (Lint.startsWith(text, CANCEL_LABEL, j)) {
String label = stripLabel(text);
if (label.equalsIgnoreCase(CANCEL_LABEL)) {
String name = element.getAttribute(ATTR_NAME);
foundResource(context, name, element);
if (!label.equals(CANCEL_LABEL)
&& Lint.isEnglishResource(context, true)
&& context.isEnabled(CASE)) {
assert label.trim().equalsIgnoreCase(CANCEL_LABEL) : label;
context.report(
CASE,
child,
context.getLocation(child),
String.format(
"The standard Android way to capitalize %1$s "
+ "is \"Cancel\" (tip: use `@android:string/cancel` instead)",
label));
}
}
} else if (Lint.startsWith(text, OK_LABEL, j)) {
String label = stripLabel(text);
if (label.equalsIgnoreCase(OK_LABEL)) {
String name = element.getAttribute(ATTR_NAME);
foundResource(context, name, element);
if (!label.equals(OK_LABEL)
&& Lint.isEnglishResource(context, true)
&& context.isEnabled(CASE)) {
assert label.trim().equalsIgnoreCase(OK_LABEL) : label;
context.report(
CASE,
child,
context.getLocation(child),
//noinspection LintImplTextFormat
String.format(
"The standard Android way to capitalize %1$s "
+ "is \"OK\" (tip: use `@android:string/ok` instead)",
label));
}
}
} else if (Lint.startsWith(text, BACK_LABEL, j)
&& stripLabel(text).equalsIgnoreCase(BACK_LABEL)) {
String name = element.getAttribute(ATTR_NAME);
foundResource(context, name, element);
}
break;
}
}
}
}
} else if (tagName.equals(BUTTON)) {
if (phase == 1) {
if (isInButtonBar(element)
&& !element.hasAttribute(ATTR_STYLE)
&& !VALUE_SELECTABLE_ITEM_BACKGROUND.equals(
element.getAttributeNS(ANDROID_URI, ATTR_BACKGROUND))
&& (context.getProject().getMinSdk() >= 11
|| context.getFolderVersion() >= 11)
&& context.isEnabled(STYLE)
&& !parentDefinesSelectableItem(element)) {
context.report(
STYLE,
element,
context.getElementLocation(element),
"Buttons in button bars should be borderless; use "
+ "`style=\"?android:attr/buttonBarButtonStyle\"` (and "
+ "`?android:attr/buttonBarStyle` on the parent)");
}
}
String text = element.getAttributeNS(ANDROID_URI, ATTR_TEXT);
if (phase == 2) {
if (mApplicableResources.contains(text)) {
String key = text;
if (key.startsWith(STRING_PREFIX)) {
key = key.substring(STRING_PREFIX.length());
}
String label = mKeyToLabel.get(key);
boolean isCancel = CANCEL_LABEL.equalsIgnoreCase(label);
if (isCancel) {
if (isWrongCancelPosition(element)) {
reportCancelPosition(context, element);
}
} else if (OK_LABEL.equalsIgnoreCase(label)) {
if (isWrongOkPosition(element)) {
reportOkPosition(context, element);
}
} else if (BACK_LABEL.equalsIgnoreCase(label)) {
if (context.isEnabled(BACK_BUTTON)) {
Location location = context.getElementLocation(element);
context.report(
BACK_BUTTON,
element,
location,
"Back buttons are not standard on Android; see design guide's "
+ "navigation section");
}
}
}
} else if (text.equals(CANCEL_LABEL) || text.equals(ANDROID_CANCEL_RESOURCE)) {
if (isWrongCancelPosition(element)) {
reportCancelPosition(context, element);
}
} else if (text.equals(OK_LABEL) || text.equals(ANDROID_OK_RESOURCE)) {
if (isWrongOkPosition(element)) {
reportOkPosition(context, element);
}
} else {
boolean isYes = text.equals(ANDROID_YES_RESOURCE);
if (isYes || text.equals(ANDROID_NO_RESOURCE)) {
Attr attribute = element.getAttributeNodeNS(ANDROID_URI, ATTR_TEXT);
Location location = context.getLocation(attribute);
String message =
String.format(
"%1$s actually returns \"%2$s\", not \"%3$s\"; "
+ "use %4$s instead or create a local string resource for %5$s",
text,
isYes ? OK_LABEL : CANCEL_LABEL,
isYes ? YES_LABEL : NO_LABEL,
isYes ? ANDROID_OK_RESOURCE : ANDROID_CANCEL_RESOURCE,
isYes ? YES_LABEL : NO_LABEL);
context.report(CASE, element, location, message);
}
}
}
}
private static boolean parentDefinesSelectableItem(Element element) {
String background = element.getAttributeNS(ANDROID_URI, ATTR_BACKGROUND);
if (VALUE_SELECTABLE_ITEM_BACKGROUND.equals(background)) {
return true;
}
Node parent = element.getParentNode();
if (parent != null && parent.getNodeType() == Node.ELEMENT_NODE) {
return parentDefinesSelectableItem((Element) parent);
}
return false;
}
/** Report the given OK button as being in the wrong position */
private void reportOkPosition(XmlContext context, Element element) {
report(context, element, false /*isCancel*/);
}
/** Report the given Cancel button as being in the wrong position */
private void reportCancelPosition(XmlContext context, Element element) {
report(context, element, true /*isCancel*/);
}
/**
* We've found a resource reference to some label we're interested in ("OK", "Cancel", "Back",
* ...). Record the corresponding name such that in the next pass through the layouts we can
* check the context (for OK/Cancel the button order etc).
*/
private void foundResource(XmlContext context, String name, Element element) {
if (!Lint.isEnglishResource(context, true)) {
return;
}
if (!context.getProject().getReportIssues()) {
// If this is a library project not being analyzed, ignore it
return;
}
if (mApplicableResources == null) {
mApplicableResources = new HashSet<>();
}
mApplicableResources.add(STRING_PREFIX + name);
// ALSO record all the other string resources in this file to pick up other
// labels. If you define "OK" in one resource file and "Cancel" in another
// this won't work, but that's probably not common and has lower overhead.
Node parentNode = element.getParentNode();
if (mKeyToLabel == null) {
mKeyToLabel = new HashMap<>();
}
for (Element item : XmlUtils.getSubTags(parentNode)) {
String itemName = item.getAttribute(ATTR_NAME);
NodeList childNodes = item.getChildNodes();
for (int i = 0, n = childNodes.getLength(); i < n; i++) {
Node child = childNodes.item(i);
if (child.getNodeType() == Node.TEXT_NODE) {
String text = stripLabel(child.getNodeValue());
if (!text.isEmpty()) {
mKeyToLabel.put(itemName, text);
break;
}
}
}
}
}
/** Report the given OK/Cancel button as being in the wrong position */
private void report(XmlContext context, Element element, boolean isCancel) {
if (!context.isEnabled(ORDER)) {
return;
}
if (mIgnore != null && mIgnore.contains(element)) {
return;
}
int target = context.getProject().getTargetSdk();
if (target < 14) {
// If you're only targeting pre-ICS UI's, this is not an issue
return;
}
boolean mustCreateIcsLayout = false;
if (context.getProject().getMinSdk() < 14) {
// If you're *also* targeting pre-ICS UIs, then this reverse button
// order is correct for layouts intended for pre-ICS and incorrect for
// ICS layouts.
//
// Therefore, we need to know if this layout is an ICS layout or
// a pre-ICS layout.
boolean isIcsLayout = context.getFolderVersion() >= 14;
if (!isIcsLayout) {
// This layout is not an ICS layout. However, there *must* also be
// an ICS layout here, or this button order will be wrong:
File res = context.file.getParentFile().getParentFile();
File[] resFolders = res.listFiles();
String fileName = context.file.getName();
if (resFolders != null) {
for (File folder : resFolders) {
String folderName = folder.getName();
if (folderName.startsWith(SdkConstants.FD_RES_LAYOUT)
&& folderName.contains("-v14")) {
File layout = new File(folder, fileName);
if (layout.exists()) {
// Yes, a v14 specific layout is available so this pre-ICS
// layout order is not a problem
return;
}
}
}
}
mustCreateIcsLayout = true;
}
}
List<Element> buttons = Lint.getChildren(element.getParentNode());
if (mIgnore == null) {
mIgnore = new HashSet<>();
}
// Mark all the siblings in the ignore list to ensure that we don't
// report *both* the Cancel and the OK button in "OK | Cancel"
mIgnore.addAll(buttons);
String message;
if (isCancel) {
message = "Cancel button should be on the left";
} else {
message = "OK button should be on the right";
}
if (mustCreateIcsLayout) {
message =
String.format(
"Layout uses the wrong button order for API >= 14: Create a "
+ "`layout-v14/%1$s` file with opposite order: %2$s",
context.file.getName(), message);
}
// Show existing button order? We can only do that for LinearLayouts
// since in for example a RelativeLayout the order of the elements may
// not be the same as the visual order
String layout = element.getParentNode().getNodeName();
if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) {
List<String> labelList = getLabelList(buttons);
String wrong = describeButtons(labelList);
sortButtons(labelList);
String right = describeButtons(labelList);
message += String.format(" (was \"%1$s\", should be \"%2$s\")", wrong, right);
}
Location location = context.getElementLocation(element);
context.report(ORDER, element, location, message);
}
/** Sort a list of label buttons into the expected order (Cancel on the left, OK on the right */
private static void sortButtons(List<String> labelList) {
for (int i = 0, n = labelList.size(); i < n; i++) {
String label = labelList.get(i);
if (label.equalsIgnoreCase(CANCEL_LABEL) && i > 0) {
swap(labelList, 0, i);
} else if (label.equalsIgnoreCase(OK_LABEL) && i < n - 1) {
swap(labelList, n - 1, i);
}
}
}
/** Swaps the strings at positions i and j */
private static void swap(List<String> strings, int i, int j) {
if (i != j) {
String temp = strings.get(i);
strings.set(i, strings.get(j));
strings.set(j, temp);
}
}
/** Creates a display string for a list of button labels, such as "Cancel | OK" */
private static String describeButtons(List<String> labelList) {
StringBuilder sb = new StringBuilder(80);
for (String label : labelList) {
if (sb.length() > 0) {
sb.append(" | ");
}
sb.append(label);
}
return sb.toString();
}
/** Returns the ordered list of button labels */
private List<String> getLabelList(List<Element> views) {
List<String> labels = new ArrayList<>();
if (mIgnore == null) {
mIgnore = new HashSet<>();
}
for (Element view : views) {
if (view.getTagName().equals(BUTTON)) {
String text = view.getAttributeNS(ANDROID_URI, ATTR_TEXT);
String label = getLabel(text);
labels.add(label);
// Mark all the siblings in the ignore list to ensure that we don't
// report *both* the Cancel and the OK button in "OK | Cancel"
mIgnore.add(view);
}
}
return labels;
}
private String getLabel(String key) {
String label = null;
if (key.startsWith(ANDROID_STRING_PREFIX)) {
if (key.equals(ANDROID_OK_RESOURCE)) {
label = OK_LABEL;
} else if (key.equals(ANDROID_CANCEL_RESOURCE)) {
label = CANCEL_LABEL;
}
} else if (mKeyToLabel != null) {
if (key.startsWith(STRING_PREFIX)) {
label = mKeyToLabel.get(key.substring(STRING_PREFIX.length()));
}
}
if (label == null) {
label = key;
}
if (label.indexOf(' ') != -1 && label.indexOf('"') == -1) {
label = '"' + label + '"';
}
return label;
}
/** Is the cancel button in the wrong position? It has to be on the left. */
private static boolean isWrongCancelPosition(Element element) {
return isWrongPosition(element, true /*isCancel*/);
}
/** Is the OK button in the wrong position? It has to be on the right. */
private static boolean isWrongOkPosition(Element element) {
return isWrongPosition(element, false /*isCancel*/);
}
private static boolean isInButtonBar(Element element) {
assert element.getTagName().equals(BUTTON) : element.getTagName();
Node parentNode = element.getParentNode();
if (parentNode.getNodeType() != Node.ELEMENT_NODE) {
return false;
}
Element parent = (Element) parentNode;
String style = parent.getAttribute(ATTR_STYLE);
if (style != null && style.contains("buttonBarStyle")) {
return true;
}
// Don't warn about single Cancel / OK buttons
if (Lint.getChildCount(parent) < 2) {
return false;
}
String layout = parent.getTagName();
if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) {
String orientation = parent.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION);
if (VALUE_VERTICAL.equals(orientation)) {
return false;
}
} else {
return false;
}
// Ensure that all the children are buttons
Node n = parent.getFirstChild();
while (n != null) {
if (n.getNodeType() == Node.ELEMENT_NODE) {
if (!BUTTON.equals(n.getNodeName())) {
return false;
}
}
n = n.getNextSibling();
}
return true;
}
/** Is the given button in the wrong position? */
private static boolean isWrongPosition(Element element, boolean isCancel) {
Node parentNode = element.getParentNode();
if (parentNode.getNodeType() != Node.ELEMENT_NODE) {
return false;
}
Element parent = (Element) parentNode;
// Don't warn about single Cancel / OK buttons
if (Lint.getChildCount(parent) < 2) {
return false;
}
String layout = parent.getTagName();
if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) {
String orientation = parent.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION);
if (VALUE_VERTICAL.equals(orientation)) {
return false;
}
if (isCancel) {
Node n = element.getPreviousSibling();
while (n != null) {
if (n.getNodeType() == Node.ELEMENT_NODE) {
return true;
}
n = n.getPreviousSibling();
}
} else {
Node n = element.getNextSibling();
while (n != null) {
if (n.getNodeType() == Node.ELEMENT_NODE) {
return true;
}
n = n.getNextSibling();
}
}
return false;
} else if (layout.equals(RELATIVE_LAYOUT) || layout.equals(PERCENT_RELATIVE_LAYOUT)) {
// In RelativeLayouts, look for attachments which look like a clear sign
// that the OK or Cancel buttons are out of order:
// -- a left attachment on a Cancel button (where the left attachment
// is a button; we don't want to complain if it's pointing to a spacer
// or image or progress indicator etc)
// -- a right-side parent attachment on a Cancel button (unless it's also
// attached on the left, e.g. a cancel button stretching across the
// layout)
// etc.
if (isCancel) {
if (element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF)
&& isButtonId(
parent,
element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF))) {
return true;
}
if (isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_RIGHT)
&& !isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_LEFT)) {
return true;
}
} else {
if (element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF)
&& isButtonId(
parent,
element.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF))) {
return true;
}
if (isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_LEFT)
&& !isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) {
return true;
}
}
return false;
} else {
// TODO: Consider other button layouts - GridLayouts, custom views extending
// LinearLayout etc?
return false;
}
}
/**
* Returns true if the given attribute (in the Android namespace) is set to true on the given
* element
*/
private static boolean isTrue(Element element, String attribute) {
return VALUE_TRUE.equals(element.getAttributeNS(ANDROID_URI, attribute));
}
/** Is the given target id the id of a {@code <Button>} within this RelativeLayout? */
private static boolean isButtonId(Element parent, String targetId) {
for (Element child : XmlUtils.getSubTags(parent)) {
String id = child.getAttributeNS(ANDROID_URI, ATTR_ID);
if (Lint.idReferencesMatch(id, targetId)) {
return child.getTagName().equals(BUTTON);
}
}
return false;
}
}