• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tools.lint.checks;
18 
19 import static com.android.AndroidConstants.FD_RES_LAYOUT;
20 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_STRING_RESOURCE_PREFIX;
21 import static com.android.tools.lint.detector.api.LintConstants.ANDROID_URI;
22 import static com.android.tools.lint.detector.api.LintConstants.ATTR_ID;
23 import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT;
24 import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT;
25 import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_TO_LEFT_OF;
26 import static com.android.tools.lint.detector.api.LintConstants.ATTR_LAYOUT_TO_RIGHT_OF;
27 import static com.android.tools.lint.detector.api.LintConstants.ATTR_NAME;
28 import static com.android.tools.lint.detector.api.LintConstants.ATTR_ORIENTATION;
29 import static com.android.tools.lint.detector.api.LintConstants.ATTR_TEXT;
30 import static com.android.tools.lint.detector.api.LintConstants.BUTTON;
31 import static com.android.tools.lint.detector.api.LintConstants.LINEAR_LAYOUT;
32 import static com.android.tools.lint.detector.api.LintConstants.RELATIVE_LAYOUT;
33 import static com.android.tools.lint.detector.api.LintConstants.STRING_RESOURCE_PREFIX;
34 import static com.android.tools.lint.detector.api.LintConstants.TABLE_ROW;
35 import static com.android.tools.lint.detector.api.LintConstants.TAG_STRING;
36 import static com.android.tools.lint.detector.api.LintConstants.VALUE_TRUE;
37 import static com.android.tools.lint.detector.api.LintConstants.VALUE_VERTICAL;
38 
39 import com.android.annotations.NonNull;
40 import com.android.resources.ResourceFolderType;
41 import com.android.tools.lint.detector.api.Category;
42 import com.android.tools.lint.detector.api.Context;
43 import com.android.tools.lint.detector.api.Issue;
44 import com.android.tools.lint.detector.api.LintUtils;
45 import com.android.tools.lint.detector.api.Location;
46 import com.android.tools.lint.detector.api.ResourceXmlDetector;
47 import com.android.tools.lint.detector.api.Scope;
48 import com.android.tools.lint.detector.api.Severity;
49 import com.android.tools.lint.detector.api.Speed;
50 import com.android.tools.lint.detector.api.XmlContext;
51 
52 import org.w3c.dom.Element;
53 import org.w3c.dom.Node;
54 import org.w3c.dom.NodeList;
55 
56 import java.io.File;
57 import java.util.ArrayList;
58 import java.util.Arrays;
59 import java.util.Collection;
60 import java.util.HashMap;
61 import java.util.HashSet;
62 import java.util.List;
63 import java.util.Map;
64 import java.util.Set;
65 
66 /**
67  * Check which looks at the order of buttons in dialogs and makes sure that
68  * "the dismissive action of a dialog is always on the left whereas the affirmative actions
69  * are on the right."
70  * <p>
71  * This only looks for the affirmative and dismissive actions named "OK" and "Cancel";
72  * "Cancel" usually works, but the affirmative action often has many other names -- "Done",
73  * "Send", "Go", etc.
74  * <p>
75  * TODO: Perhaps we should look for Yes/No dialogs and suggested they be rephrased as
76  * Cancel/OK dialogs? Similarly, consider "Abort" a synonym for "Cancel" ?
77  */
78 public class ButtonDetector extends ResourceXmlDetector {
79     /** Name of cancel value ("Cancel") */
80     private static final String CANCEL_LABEL = "Cancel";
81     /** Name of OK value ("Cancel") */
82     private static final String OK_LABEL = "OK";
83     /** Name of Back value ("Back") */
84     private static final String BACK_LABEL = "Back";
85 
86     /** Layout text attribute reference to {@code @android:string/ok} */
87     private static final String ANDROID_OK_RESOURCE =
88             ANDROID_STRING_RESOURCE_PREFIX + "ok"; //$NON-NLS-1$
89     /** Layout text attribute reference to {@code @android:string/cancel} */
90     private static final String ANDROID_CANCEL_RESOURCE =
91             ANDROID_STRING_RESOURCE_PREFIX + "cancel"; //$NON-NLS-1$
92 
93     /** The main issue discovered by this detector */
94     public static final Issue ORDER = Issue.create(
95             "ButtonOrder", //$NON-NLS-1$
96             "Ensures the dismissive action of a dialog is on the left and affirmative on " +
97             "the right",
98 
99             "According to the Android Design Guide,\n" +
100             "\n" +
101             "\"Action buttons are typically Cancel and/or OK, with OK indicating the preferred " +
102             "or most likely action. However, if the options consist of specific actions such " +
103             "as Close or Wait rather than a confirmation or cancellation of the action " +
104             "described in the content, then all the buttons should be active verbs. As a rule, " +
105             "the dismissive action of a dialog is always on the left whereas the affirmative " +
106             "actions are on the right.\"\n" +
107             "\n" +
108             "This check looks for button bars and buttons which look like cancel buttons, " +
109             "and makes sure that these are on the left.",
110 
111             Category.USABILITY,
112             8,
113             Severity.WARNING,
114             ButtonDetector.class,
115             Scope.RESOURCE_FILE_SCOPE)
116             .setMoreInfo(
117                 "http://developer.android.com/design/building-blocks/dialogs.html"); //$NON-NLS-1$
118 
119     /** The main issue discovered by this detector */
120     public static final Issue BACKBUTTON = Issue.create(
121             "BackButton", //$NON-NLS-1$
122             "Looks for Back buttons, which are not common on the Android platform.",
123             // TODO: Look for ">" as label suffixes as well
124 
125             "According to the Android Design Guide,\n" +
126             "\n" +
127             "\"Other platforms use an explicit back button with label to allow the user " +
128             "to navigate up the application's hierarchy. Instead, Android uses the main " +
129             "action bar's app icon for hierarchical navigation and the navigation bar's " +
130             "back button for temporal navigation.\"" +
131             "\n" +
132             "This check is not very sophisticated (it just looks for buttons with the " +
133             "label \"Back\"), so it is disabled by default to not trigger on common " +
134             "scenarios like pairs of Back/Next buttons to paginate through screens.",
135 
136             Category.USABILITY,
137             6,
138             Severity.WARNING,
139             ButtonDetector.class,
140             Scope.RESOURCE_FILE_SCOPE)
141             .setEnabledByDefault(false)
142             .setMoreInfo(
143                 "http://developer.android.com/design/patterns/pure-android.html"); //$NON-NLS-1$
144 
145     /** The main issue discovered by this detector */
146     public static final Issue CASE = Issue.create(
147             "ButtonCase", //$NON-NLS-1$
148             "Ensures that Cancel/OK dialog buttons use the canonical capitalization",
149 
150             "The standard capitalization for OK/Cancel dialogs is \"OK\" and \"Cancel\". " +
151             "To ensure that your dialogs use the standard strings, you can use " +
152             "the resource strings @android:string/ok and @android:string/cancel.",
153 
154             Category.USABILITY,
155             2,
156             Severity.WARNING,
157             ButtonDetector.class,
158             Scope.RESOURCE_FILE_SCOPE);
159 
160     /** Set of resource names whose value was either OK or Cancel */
161     private Set<String> mApplicableResources;
162 
163     /**
164      * Map of resource names we'd like resolved into strings in phase 2. The
165      * values should be filled in with the actual string contents.
166      */
167     private Map<String, String> mKeyToLabel;
168 
169     /**
170      * Set of elements we've already warned about. If we've already complained
171      * about a cancel button, don't also report the OK button (since it's listed
172      * for the warnings on OK buttons).
173      */
174     private Set<Element> mIgnore;
175 
176     /** Constructs a new {@link ButtonDetector} */
ButtonDetector()177     public ButtonDetector() {
178     }
179 
180     @Override
getSpeed()181     public Speed getSpeed() {
182         return Speed.FAST;
183     }
184 
185     @Override
getApplicableElements()186     public Collection<String> getApplicableElements() {
187         return Arrays.asList(BUTTON, TAG_STRING);
188     }
189 
190     @Override
appliesTo(@onNull ResourceFolderType folderType)191     public boolean appliesTo(@NonNull ResourceFolderType folderType) {
192         return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES;
193     }
194 
195     @Override
afterCheckProject(Context context)196     public void afterCheckProject(Context context) {
197         int phase = context.getPhase();
198         if (phase == 1 && mApplicableResources != null) {
199             // We found resources for the string "Cancel"; perform a second pass
200             // where we check layout text attributes against these strings.
201             context.getDriver().requestRepeat(this, Scope.RESOURCE_FILE_SCOPE);
202         }
203     }
204 
stripLabel(String text)205     private String stripLabel(String text) {
206         text = text.trim();
207         if (text.length() > 2
208                 && (text.charAt(0) == '"' || text.charAt(0) == '\'')
209                 && (text.charAt(0) == text.charAt(text.length() - 1))) {
210             text = text.substring(1, text.length() - 1);
211         }
212 
213         return text;
214     }
215 
216     @Override
visitElement(XmlContext context, Element element)217     public void visitElement(XmlContext context, Element element) {
218         // This detector works in two passes.
219         // In pass 1, it looks in layout files for hardcoded strings of "Cancel", or
220         // references to @string/cancel or @android:string/cancel.
221         // It also looks in values/ files for strings whose value is "Cancel",
222         // and if found, stores the corresponding keys in a map. (This is necessary
223         // since value files are processed after layout files).
224         // Then, if at the end of phase 1 any "Cancel" string resources were
225         // found in the value files, then it requests a *second* phase,
226         // where it looks only for <Button>'s whose text matches one of the
227         // cancel string resources.
228         int phase = context.getPhase();
229         String tagName = element.getTagName();
230         if (phase == 1 && tagName.equals(TAG_STRING)) {
231             NodeList childNodes = element.getChildNodes();
232             for (int i = 0, n = childNodes.getLength(); i < n; i++) {
233                 Node child = childNodes.item(i);
234                 if (child.getNodeType() == Node.TEXT_NODE) {
235                     String text = child.getNodeValue();
236                     for (int j = 0, len = text.length(); j < len; j++) {
237                         char c = text.charAt(j);
238                         if (!Character.isWhitespace(c)) {
239                             if (c == '"' || c == '\'') {
240                                 continue;
241                             }
242                             if (LintUtils.startsWith(text, CANCEL_LABEL, j)) {
243                                 String label = stripLabel(text);
244                                 if (label.equalsIgnoreCase(CANCEL_LABEL)) {
245                                     String name = element.getAttribute(ATTR_NAME);
246                                     foundResource(context, name, element);
247 
248                                     if (!label.equals(CANCEL_LABEL)
249                                             && isEnglishResource(context)
250                                             && context.isEnabled(CASE)) {
251                                         assert label.equalsIgnoreCase(CANCEL_LABEL);
252                                         context.report(CASE, context.getLocation(element),
253                                             String.format(
254                                             "The standard Android way to capitalize %1$s " +
255                                             "is \"Cancel\" (tip: use @android:string/ok instead)",
256                                             label),  null);
257                                     }
258                                 }
259                             } else if (LintUtils.startsWith(text, OK_LABEL, j)) {
260                                 String label = stripLabel(text);
261                                 if (label.equalsIgnoreCase(OK_LABEL)) {
262                                     String name = element.getAttribute(ATTR_NAME);
263                                     foundResource(context, name, element);
264 
265                                     if (!label.equals(OK_LABEL)
266                                             && isEnglishResource(context)
267                                             && context.isEnabled(CASE)) {
268                                         assert text.equalsIgnoreCase(OK_LABEL);
269                                         context.report(CASE, context.getLocation(element),
270                                             String.format(
271                                             "The standard Android way to capitalize %1$s " +
272                                             "is \"OK\" (tip: use @android:string/ok instead)",
273                                             label),  null);
274                                     }
275                                 }
276                             } else if (LintUtils.startsWith(text, BACK_LABEL, j) &&
277                                     stripLabel(text).equalsIgnoreCase(BACK_LABEL)) {
278                                 String name = element.getAttribute(ATTR_NAME);
279                                 foundResource(context, name, element);
280                             }
281                             break;
282                         }
283                     }
284                 }
285             }
286         } else if (tagName.equals(BUTTON)) {
287             String text = element.getAttributeNS(ANDROID_URI, ATTR_TEXT);
288             if (context.getDriver().getPhase() == 2) {
289                 if (mApplicableResources.contains(text)) {
290                     String key = text;
291                     if (key.startsWith(STRING_RESOURCE_PREFIX)) {
292                         key = key.substring(STRING_RESOURCE_PREFIX.length());
293                     }
294                     String label = mKeyToLabel.get(key);
295                     boolean isCancel = CANCEL_LABEL.equalsIgnoreCase(label);
296                     if (isCancel) {
297                         if (isWrongCancelPosition(element)) {
298                             reportCancelPosition(context, element);
299                         }
300                     } else if (OK_LABEL.equalsIgnoreCase(label)) {
301                         if (isWrongOkPosition(element)) {
302                             reportOkPosition(context, element);
303                         }
304                     } else {
305                         assert BACK_LABEL.equalsIgnoreCase(label);
306                         Location location = context.getLocation(element);
307                         if (context.isEnabled(BACKBUTTON)) {
308                             context.report(BACKBUTTON, location,
309                                 "Back buttons are not standard on Android; see design guide's " +
310                                 "navigation section", null);
311                         }
312                     }
313                 }
314             } else if (text.equals(CANCEL_LABEL) || text.equals(ANDROID_CANCEL_RESOURCE)) {
315                 if (isWrongCancelPosition(element)) {
316                     reportCancelPosition(context, element);
317                 }
318             } else if (text.equals(OK_LABEL) || text.equals(ANDROID_OK_RESOURCE)) {
319                 if (isWrongOkPosition(element)) {
320                     reportOkPosition(context, element);
321                 }
322             }
323         }
324     }
325 
326     /** Report the given OK button as being in the wrong position */
reportOkPosition(XmlContext context, Element element)327     private void reportOkPosition(XmlContext context, Element element) {
328         report(context, element, false /*isCancel*/);
329     }
330 
331     /** Report the given Cancel button as being in the wrong position */
reportCancelPosition(XmlContext context, Element element)332     private void reportCancelPosition(XmlContext context, Element element) {
333         report(context, element, true /*isCancel*/);
334     }
335 
336 
337     /** The Ok/Cancel detector only works with default and English locales currently.
338       * TODO: Add in patterns for other languages. We can use the
339      * @android:string/ok and @android:string/cancel localizations to look
340      * up the canonical ones. */
isEnglishResource(XmlContext context)341     private boolean isEnglishResource(XmlContext context) {
342         String folder = context.file.getParentFile().getName();
343         if (folder.indexOf('-') != -1) {
344             String[] qualifiers = folder.split("-"); //$NON-NLS-1$
345             for (String qualifier : qualifiers) {
346                 if (qualifier.equals("en")) { //$NON-NLS-1$
347                     return true;
348                 }
349             }
350             return false;
351         }
352 
353         // Default folder ("values") - may not be English but we'll consider matches
354         // on "OK", "Cancel" and "Back" as matches there
355         return true;
356     }
357 
358     /**
359      * We've found a resource reference to some label we're interested in ("OK",
360      * "Cancel", "Back", ...). Record the corresponding name such that in the
361      * next pass through the layouts we can check the context (for OK/Cancel the
362      * button order etc).
363      */
foundResource(XmlContext context, String name, Element element)364     private void foundResource(XmlContext context, String name, Element element) {
365         if (!isEnglishResource(context)) {
366             return;
367         }
368 
369         if (mApplicableResources == null) {
370             mApplicableResources = new HashSet<String>();
371         }
372 
373         mApplicableResources.add(STRING_RESOURCE_PREFIX + name);
374 
375         // ALSO record all the other string resources in this file to pick up other
376         // labels. If you define "OK" in one resource file and "Cancel" in another
377         // this won't work, but that's probably not common and has lower overhead.
378         Node parentNode = element.getParentNode();
379 
380         List<Element> items = LintUtils.getChildren(parentNode);
381         if (mKeyToLabel == null) {
382             mKeyToLabel = new HashMap<String, String>(items.size());
383         }
384         for (Element item : items) {
385             String itemName = item.getAttribute(ATTR_NAME);
386             NodeList childNodes = item.getChildNodes();
387             for (int i = 0, n = childNodes.getLength(); i < n; i++) {
388                 Node child = childNodes.item(i);
389                 if (child.getNodeType() == Node.TEXT_NODE) {
390                     String text = stripLabel(child.getNodeValue());
391                     if (text.length() > 0) {
392                         mKeyToLabel.put(itemName, text);
393                         break;
394                     }
395                 }
396             }
397         }
398     }
399 
400     /** Report the given OK/Cancel button as being in the wrong position */
report(XmlContext context, Element element, boolean isCancel)401     private void report(XmlContext context, Element element, boolean isCancel) {
402         if (!context.isEnabled(ORDER)) {
403             return;
404         }
405 
406         if (mIgnore != null && mIgnore.contains(element)) {
407             return;
408         }
409 
410         int target = context.getProject().getTargetSdk();
411         if (target < 14) {
412             // If you're only targeting pre-ICS UI's, this is not an issue
413             return;
414         }
415 
416         boolean mustCreateIcsLayout = false;
417         if (context.getProject().getMinSdk() < 14) {
418             // If you're *also* targeting pre-ICS UIs, then this reverse button
419             // order is correct for layouts intended for pre-ICS and incorrect for
420             // ICS layouts.
421             //
422             // Therefore, we need to know if this layout is an ICS layout or
423             // a pre-ICS layout.
424             boolean isIcsLayout = context.getFolderVersion() >= 14;
425             if (!isIcsLayout) {
426                 // This layout is not an ICS layout. However, there *must* also be
427                 // an ICS layout here, or this button order will be wrong:
428                 File res = context.file.getParentFile().getParentFile();
429                 File[] resFolders = res.listFiles();
430                 String fileName = context.file.getName();
431                 if (resFolders != null) {
432                     for (File folder : resFolders) {
433                         String folderName = folder.getName();
434                         if (folderName.startsWith(FD_RES_LAYOUT)
435                                 && folderName.contains("-v14")) { //$NON-NLS-1$
436                             File layout = new File(folder, fileName);
437                             if (layout.exists()) {
438                                 // Yes, a v14 specific layout is available so this pre-ICS
439                                 // layout order is not a problem
440                                 return;
441                             }
442                         }
443                     }
444                 }
445                 mustCreateIcsLayout = true;
446             }
447         }
448 
449         List<Element> buttons = LintUtils.getChildren(element.getParentNode());
450 
451         if (mIgnore == null) {
452             mIgnore = new HashSet<Element>();
453         }
454         for (Element button : buttons) {
455             // Mark all the siblings in the ignore list to ensure that we don't
456             // report *both* the Cancel and the OK button in "OK | Cancel"
457             mIgnore.add(button);
458         }
459 
460         String message;
461         if (isCancel) {
462             message = "Cancel button should be on the left";
463         } else {
464             message = "OK button should be on the right";
465         }
466 
467         if (mustCreateIcsLayout) {
468             message = String.format(
469                     "Layout uses the wrong button order for API >= 14: Create a " +
470                     "layout-v14/%1$s file with opposite order: %2$s",
471                     context.file.getName(), message);
472         }
473 
474         // Show existing button order? We can only do that for LinearLayouts
475         // since in for example a RelativeLayout the order of the elements may
476         // not be the same as the visual order
477         String layout = element.getParentNode().getNodeName();
478         if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) {
479             List<String> labelList = getLabelList(buttons);
480             String wrong = describeButtons(labelList);
481             sortButtons(labelList);
482             String right = describeButtons(labelList);
483             message += String.format(" (was \"%1$s\", should be \"%2$s\")", wrong, right);
484         }
485 
486         Location location = context.getLocation(element);
487         context.report(ORDER, location, message, null);
488     }
489 
490     /**
491      * Sort a list of label buttons into the expected order (Cancel on the left,
492      * OK on the right
493      */
sortButtons(List<String> labelList)494     private void sortButtons(List<String> labelList) {
495         for (int i = 0, n = labelList.size(); i < n; i++) {
496             String label = labelList.get(i);
497             if (label.equalsIgnoreCase(CANCEL_LABEL) && i > 0) {
498                 swap(labelList, 0, i);
499             } else if (label.equalsIgnoreCase(OK_LABEL) && i < n - 1) {
500                 swap(labelList, n - 1, i);
501             }
502         }
503     }
504 
505     /** Swaps the strings at positions i and j */
swap(List<String> strings, int i, int j)506     private static void swap(List<String> strings, int i, int j) {
507         if (i != j) {
508             String temp = strings.get(i);
509             strings.set(i, strings.get(j));
510             strings.set(j, temp);
511         }
512     }
513 
514     /** Creates a display string for a list of button labels, such as "Cancel | OK" */
describeButtons(List<String> labelList)515     private String describeButtons(List<String> labelList) {
516         StringBuilder sb = new StringBuilder();
517         for (String label : labelList) {
518             if (sb.length() > 0) {
519                 sb.append(" | "); //$NON-NLS-1$
520             }
521             sb.append(label);
522         }
523 
524         return sb.toString();
525     }
526 
527     /** Returns the ordered list of button labels */
getLabelList(List<Element> views)528     private List<String> getLabelList(List<Element> views) {
529         List<String> labels = new ArrayList<String>();
530 
531         if (mIgnore == null) {
532             mIgnore = new HashSet<Element>();
533         }
534 
535         for (Element view : views) {
536             if (view.getTagName().equals(BUTTON)) {
537                 String text = view.getAttributeNS(ANDROID_URI, ATTR_TEXT);
538                 String label = getLabel(text);
539                 labels.add(label);
540 
541                 // Mark all the siblings in the ignore list to ensure that we don't
542                 // report *both* the Cancel and the OK button in "OK | Cancel"
543                 mIgnore.add(view);
544             }
545         }
546 
547         return labels;
548     }
549 
getLabel(String key)550     private String getLabel(String key) {
551         String label = null;
552         if (key.startsWith(ANDROID_STRING_RESOURCE_PREFIX)) {
553             if (key.equals(ANDROID_OK_RESOURCE)) {
554                 label = OK_LABEL;
555             } else if (key.equals(ANDROID_CANCEL_RESOURCE)) {
556                 label = CANCEL_LABEL;
557             }
558         } else if (mKeyToLabel != null) {
559             if (key.startsWith(STRING_RESOURCE_PREFIX)) {
560                 label = mKeyToLabel.get(key.substring(STRING_RESOURCE_PREFIX.length()));
561             }
562         }
563 
564         if (label == null) {
565             label = key;
566         }
567 
568         if (label.indexOf(' ') != -1 && label.indexOf('"') == -1) {
569             label = '"' + label + '"';
570         }
571 
572         return label;
573     }
574 
575     /** Is the cancel button in the wrong position? It has to be on the left. */
isWrongCancelPosition(Element element)576     private boolean isWrongCancelPosition(Element element) {
577         return isWrongPosition(element, true /*isCancel*/);
578     }
579 
580     /** Is the OK button in the wrong position? It has to be on the right. */
isWrongOkPosition(Element element)581     private boolean isWrongOkPosition(Element element) {
582         return isWrongPosition(element, false /*isCancel*/);
583     }
584 
585     /** Is the given button in the wrong position? */
isWrongPosition(Element element, boolean isCancel)586     private boolean isWrongPosition(Element element, boolean isCancel) {
587         Node parentNode = element.getParentNode();
588         if (parentNode.getNodeType() != Node.ELEMENT_NODE) {
589             return false;
590         }
591         Element parent = (Element) parentNode;
592 
593         // Don't warn about single Cancel / OK buttons
594         if (LintUtils.getChildCount(parent) < 2) {
595             return false;
596         }
597 
598         String layout = parent.getTagName();
599         if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) {
600             String orientation = parent.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION);
601             if (VALUE_VERTICAL.equals(orientation)) {
602                 return false;
603             }
604 
605             if (isCancel) {
606                 Node n = element.getPreviousSibling();
607                 while (n != null) {
608                     if (n.getNodeType() == Node.ELEMENT_NODE) {
609                         return true;
610                     }
611                     n = n.getPreviousSibling();
612                 }
613             } else {
614                 Node n = element.getNextSibling();
615                 while (n != null) {
616                     if (n.getNodeType() == Node.ELEMENT_NODE) {
617                         return true;
618                     }
619                     n = n.getNextSibling();
620                 }
621             }
622 
623             return false;
624         } else if (layout.equals(RELATIVE_LAYOUT)) {
625             // In RelativeLayouts, look for attachments which look like a clear sign
626             // that the OK or Cancel buttons are out of order:
627             //   -- a left attachment on a Cancel button (where the left attachment
628             //      is a button; we don't want to complain if it's pointing to a spacer
629             //      or image or progress indicator etc)
630             //   -- a right-side parent attachment on a Cancel button (unless it's also
631             //      attached on the left, e.g. a cancel button stretching across the
632             //      layout)
633             // etc.
634             if (isCancel) {
635                 if (element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF)
636                         && isButtonId(parent, element.getAttributeNS(ANDROID_URI,
637                                 ATTR_LAYOUT_TO_RIGHT_OF))) {
638                     return true;
639                 }
640                 if (isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_RIGHT) &&
641                         !isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_LEFT)) {
642                     return true;
643                 }
644             } else {
645                 if (element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF)
646                         && isButtonId(parent, element.getAttributeNS(ANDROID_URI,
647                                 ATTR_LAYOUT_TO_RIGHT_OF))) {
648                     return true;
649                 }
650                 if (isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_LEFT) &&
651                         !isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) {
652                     return true;
653                 }
654             }
655 
656             return false;
657         } else {
658             // TODO: Consider other button layouts - GridLayouts, custom views extending
659             // LinearLayout etc?
660             return false;
661         }
662     }
663 
664     /**
665      * Returns true if the given attribute (in the Android namespace) is set to
666      * true on the given element
667      */
isTrue(Element element, String attribute)668     private static boolean isTrue(Element element, String attribute) {
669         return VALUE_TRUE.equals(element.getAttributeNS(ANDROID_URI, attribute));
670     }
671 
672     /** Is the given target id the id of a {@code <Button>} within this RelativeLayout? */
isButtonId(Element parent, String targetId)673     private boolean isButtonId(Element parent, String targetId) {
674         for (Element child : LintUtils.getChildren(parent)) {
675             String id = child.getAttributeNS(ANDROID_URI, ATTR_ID);
676             if (LintUtils.idReferencesMatch(id, targetId)) {
677                 return child.getTagName().equals(BUTTON);
678             }
679         }
680         return false;
681     }
682 }