@Override
  public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
    int declaredRowCount = getInt(element, ATTR_ROW_COUNT, -1);
    int declaredColumnCount = getInt(element, ATTR_COLUMN_COUNT, -1);

    if (declaredColumnCount != -1 || declaredRowCount != -1) {
      for (Element child : LintUtils.getChildren(element)) {
        if (declaredColumnCount != -1) {
          int column = getInt(child, ATTR_LAYOUT_COLUMN, -1);
          if (column >= declaredColumnCount) {
            Attr node = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_COLUMN);
            context.report(
                ISSUE,
                node,
                context.getLocation(node),
                String.format(
                    "Column attribute (%1$d) exceeds declared grid column count (%2$d)",
                    column, declaredColumnCount));
          }
        }
        if (declaredRowCount != -1) {
          int row = getInt(child, ATTR_LAYOUT_ROW, -1);
          if (row > declaredRowCount) {
            Attr node = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_ROW);
            context.report(
                ISSUE,
                node,
                context.getLocation(node),
                String.format(
                    "Row attribute (%1$d) exceeds declared grid row count (%2$d)",
                    row, declaredRowCount));
          }
        }
      }
    }

    if (element.getTagName().equals(FQCN_GRID_LAYOUT_V7)) {
      // Make sure that we're not using android: namespace attributes where we should
      // be using app namespace attributes!
      ensureAppNamespace(context, element, ATTR_COLUMN_COUNT);
      ensureAppNamespace(context, element, ATTR_ORIENTATION);
      ensureAppNamespace(context, element, ATTR_ROW_COUNT);
      ensureAppNamespace(context, element, ATTR_USE_DEFAULT_MARGINS);
      ensureAppNamespace(context, element, "alignmentMode");
      ensureAppNamespace(context, element, "columnOrderPreserved");
      ensureAppNamespace(context, element, "rowOrderPreserved");

      for (Element child : LintUtils.getChildren(element)) {
        ensureAppNamespace(context, child, ATTR_LAYOUT_COLUMN);
        ensureAppNamespace(context, child, ATTR_LAYOUT_COLUMN_SPAN);
        ensureAppNamespace(context, child, ATTR_LAYOUT_GRAVITY);
        ensureAppNamespace(context, child, ATTR_LAYOUT_ROW);
        ensureAppNamespace(context, child, ATTR_LAYOUT_ROW_SPAN);
      }
    }
  }
  @Override
  public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
    assert attribute.getName().equals(ATTR_ID) || attribute.getLocalName().equals(ATTR_ID);
    String id = attribute.getValue();
    if (context.getPhase() == 1) {
      if (mIds.contains(id)) {
        Location location = context.getLocation(attribute);

        Attr first = findIdAttribute(attribute.getOwnerDocument(), id);
        if (first != null && first != attribute) {
          Location secondLocation = context.getLocation(first);
          secondLocation.setMessage(String.format("%1$s originally defined here", id));
          location.setSecondary(secondLocation);
        }

        context.report(
            WITHIN_LAYOUT,
            attribute,
            location,
            String.format("Duplicate id %1$s, already defined earlier in this layout", id),
            null);
      } else if (id.startsWith(NEW_ID_PREFIX)) {
        // Skip id's on include tags
        if (attribute.getOwnerElement().getTagName().equals(VIEW_INCLUDE)) {
          return;
        }

        mIds.add(id);
      }
    } else {
      Collection<Multimap<String, Occurrence>> maps = mLocations.get(context.file);
      if (maps != null && !maps.isEmpty()) {
        for (Multimap<String, Occurrence> map : maps) {
          if (!maps.isEmpty()) {
            Collection<Occurrence> occurrences = map.get(id);
            if (occurrences != null && !occurrences.isEmpty()) {
              for (Occurrence occurrence : occurrences) {
                if (context.getDriver().isSuppressed(CROSS_LAYOUT, attribute)) {
                  return;
                }
                Location location = context.getLocation(attribute);
                location.setClientData(attribute);
                location.setMessage(occurrence.message);
                location.setSecondary(occurrence.location);
                occurrence.location = location;
              }
            }
          }
        }
      }
    }
  }
  @Override
  public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
    String value = attribute.getValue();
    if (value.isEmpty() || value.trim().isEmpty()) {
      context.report(
          ISSUE,
          attribute,
          context.getLocation(attribute),
          "onClick attribute value cannot be empty",
          null);
    } else if (!value.equals(value.trim())) {
      context.report(
          ISSUE,
          attribute,
          context.getLocation(attribute),
          "There should be no whitespace around attribute values",
          null);
    } else {
      if (mNames == null) {
        mNames = new HashMap<String, Location.Handle>();
      }
      Handle handle = context.parser.createLocationHandle(context, attribute);
      handle.setClientData(attribute);

      // Replace unicode characters with the actual value since that's how they
      // appear in the ASM signatures
      if (value.contains("\\u")) { // $NON-NLS-1$
        Pattern pattern = Pattern.compile("\\\\u(\\d\\d\\d\\d)"); // $NON-NLS-1$
        Matcher matcher = pattern.matcher(value);
        StringBuilder sb = new StringBuilder(value.length());
        int remainder = 0;
        while (matcher.find()) {
          sb.append(value.substring(0, matcher.start()));
          String unicode = matcher.group(1);
          int hex = Integer.parseInt(unicode, 16);
          sb.append((char) hex);
          remainder = matcher.end();
        }
        sb.append(value.substring(remainder));
        value = sb.toString();
      }

      mNames.put(value, handle);
    }
  }
  @Override
  public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
    // Traverse all child elements
    NodeList childNodes = element.getChildNodes();
    int count = childNodes.getLength();
    Map<String, LayoutNode> nodes = Maps.newHashMap();
    for (int i = 0; i < count; i++) {
      Node node = childNodes.item(i);
      if (node.getNodeType() == Node.ELEMENT_NODE) {
        LayoutNode ln = new LayoutNode((Element) node, i);
        nodes.put(ln.getNodeId(), ln);
      }
    }

    // Node map is populated, recalculate nodes sizes
    for (LayoutNode ln : nodes.values()) {
      ln.processNode(nodes);
    }
    for (LayoutNode right : nodes.values()) {
      if (!right.mLastLeft || right.skip()) {
        continue;
      }
      Set<LayoutNode> canGrowLeft = right.canGrowLeft();
      for (LayoutNode left : nodes.values()) {
        if (left == right || !left.mLastRight || left.skip() || !left.sameBucket(right)) {
          continue;
        }
        Set<LayoutNode> canGrowRight = left.canGrowRight();
        if (canGrowLeft.size() > 0 || canGrowRight.size() > 0) {
          canGrowRight.addAll(canGrowLeft);
          LayoutNode nodeToBlame = right;
          LayoutNode otherNode = left;
          if (!canGrowRight.contains(right) && canGrowRight.contains(left)) {
            nodeToBlame = left;
            otherNode = right;
          }
          context.report(
              ISSUE,
              nodeToBlame.getNode(),
              context.getLocation(nodeToBlame.getNode()),
              String.format(
                  "`%1$s` can overlap `%2$s` if %3$s %4$s due to localized text expansion",
                  nodeToBlame.getNodeId(),
                  otherNode.getNodeId(),
                  Joiner.on(", ").join(canGrowRight),
                  canGrowRight.size() > 1 ? "grow" : "grows"));
        }
      }
    }
  }
  @Override
  public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) {
    String value = attribute.getValue();
    if (!value.isEmpty() && (value.charAt(0) != '@' && value.charAt(0) != '?')) {
      // Make sure this is really one of the android: attributes
      if (!ANDROID_URI.equals(attribute.getNamespaceURI())) {
        return;
      }

      context.report(
          ISSUE,
          attribute,
          context.getLocation(attribute),
          String.format("[I18N] Hardcoded string \"%1$s\", should use `@string` resource", value));
    }
  }
  @Override
  public void visitElement(@NonNull XmlContext context, @NonNull Element element) {
    // Record include graph such that we can look for inter-layout duplicates after the
    // project has been fully checked

    String layout = element.getAttribute(ATTR_LAYOUT); // NOTE: Not in android: namespace
    if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { // Ignore @android:layout/ layouts
      layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length());

      if (context.getPhase() == 1) {
        if (!context.getProject().getReportIssues()) {
          // If this is a library project not being analyzed, ignore it
          return;
        }

        List<String> to = mIncludes.get(context.file);
        if (to == null) {
          to = new ArrayList<String>();
          mIncludes.put(context.file, to);
        }
        to.add(layout);
      } else {
        assert context.getPhase() == 2;

        Collection<Multimap<String, Occurrence>> maps = mLocations.get(context.file);
        if (maps != null && !maps.isEmpty()) {
          for (Multimap<String, Occurrence> map : maps) {
            if (!maps.isEmpty()) {
              Collection<Occurrence> occurrences = map.get(layout);
              if (occurrences != null && !occurrences.isEmpty()) {
                for (Occurrence occurrence : occurrences) {
                  Location location = context.getLocation(element);
                  location.setClientData(element);
                  location.setMessage(occurrence.message);
                  location.setSecondary(occurrence.location);
                  occurrence.location = location;
                }
              }
            }
          }
        }
      }
    }
  }
  private static void ensureAppNamespace(XmlContext context, Element element, String name) {
    Attr attribute = element.getAttributeNodeNS(ANDROID_URI, name);
    if (attribute != null) {
      String prefix = getNamespacePrefix(element.getOwnerDocument(), AUTO_URI);
      boolean haveNamespace = prefix != null;
      if (!haveNamespace) {
        prefix = "app";
      }

      StringBuilder sb = new StringBuilder();
      sb.append("Wrong namespace; with v7 `GridLayout` you should use ")
          .append(prefix)
          .append(":")
          .append(name);
      if (!haveNamespace) {
        sb.append(" (and add `xmlns:app=\"").append(AUTO_URI).append("\"` to your root element.)");
      }
      String message = sb.toString();

      context.report(ISSUE, attribute, context.getLocation(attribute), message);
    }
  }