12

I have a large Android codebase and I am writing a custom lint rule that checks whether the values of certain attributes fall within a given range.

For example, I have this component:

<MyCustomComponent
    my:animation_factor="0.7"
    ...>
</MyCustomComponent>

and I want to write a lint rule that alerts developers that values of my:animation_factor >= 1 should be used with caution.

I followed the instructions at http://tools.android.com/tips/lint-custom-rules and managed to retrieve the value of my:animation_factor using this code:

import com.android.tools.lint.detector.api.*;

public class XmlInterpolatorFactorTooHighDetector {

    ....
    @Override
    public Collection<String> getApplicableElements() {
        return ImmutableList.of("MyCustomComponent");
    }

    @Override
    public void visitElement(XmlContext context, Element element) {
        String factor = element.getAttribute("my:animation_factor");
        ...
        if (value.startsWith("@dimen/")) {
            // How do I resolve @dimen/xyz to 1.85?
        } else {
            String value = Float.parseFloat(factor);
        }
    }
}

This code works fine when attributes such as my:animation_factor have literal values (e.g. 0.7).

However, when the attribute value is a resources (e.g. @dimen/standard_anim_factor) then element.getAttribute(...) returns the string value of the attribute instead of the actual resolved value.

For example, when I have a MyCustomComponent that looks like this:

<MyCustomComponent
    my:animation_factor="@dimen/standard_anim_factory"
    ...>
</MyCustomComponent>

and @dimen/standard_anim_factor is defined elsewhere:

<dimen name="standard_anim_factor">1.85</dimen>

then the string factor becomes "@dimen/standard_anim_factor" instead of "1.85".

Is there a way to resolve "@dimen/standard_anim_factor" to the actual value of resource (i.e. "1.85") while processing the MyCustomComponent element?

Mike Laren
  • 8,028
  • 17
  • 51
  • 70

3 Answers3

2

The general problem with the resolution of values is, that they depend on the Android runtime context you are in. There might be several values folders with different concrete values for your key @dimen/standard_anim_factory, so just that you are aware of.

Nevertheless, AFAIK there exist two options:

  • Perform a two phase detection:

    • Phase 1: Scan your resources
    • Scan for your attribute and put it in a list (instead of evaluating it immediately)
    • Scan your dimension values and put them in a list as well
    • Phase 2:
    • override Detector.afterProjectCheck and resolve your attributes by iterating over the two lists filled within phase 1
  • usually the LintUtils class [1] is a perfect spot for that stuff but unfortunately there is no method which resolves dimensions values. However, there is a method called getStyleAttributes which demonstrates how to resolve resource values. So you could write your own convenient method to resolve dimension values:


    private int resolveDimensionValue(String name, Context context){
       LintClient client = context.getDriver().getClient();
       LintProject project = context.getDriver().getProject();
       AbstractResourceRepository resources = client.getProjectResources(project, true);

       return Integer.valueOf(resources.getResourceItem(ResourceType.DIMEN, name).get(0).getResourceValue(false).getValue());
    }

Note: I haven't tested the above code yet. So please see it as theoretical advice :-)

  1. https://android.googlesource.com/platform/tools/base/+/master/lint/libs/lint-api/src/main/java/com/android/tools/lint/detector/api/LintUtils.java

Just one more slight advice for your custom Lint rule code, since you are only interested in the attribute:

Instead of doing something like this in visitElement:

String factor = element.getAttribute("my:animation_factor");

...you may want to do something like this:

@Override
public Collection<String> getApplicableAttributes() {
    return ImmutableList.of("my:animation_factor");
}

@Override    
void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute){
    ...
}

But it's just a matter of preference :-)

André Diermann
  • 2,725
  • 23
  • 28
  • The 2-phase approach definitely solves the problem without requiring too much work at runtime. It's a shame that Android Lint doesn't have a better interface (or just a better template) to do this. – Mike Laren May 29 '15 at 16:37
0

I believe you're looking looking for getResources().getDimension().

Source: http://developer.android.com/reference/android/content/res/Resources.html#getDimension%28int%29

ajacian81
  • 7,419
  • 9
  • 51
  • 64
  • The problem is that `getResources()` is a method of `android.content.Context` but Lint uses a `com.android.tools.lint.detector.api.Context` that has no method for getting/resolving resources. – Mike Laren May 26 '15 at 22:06
0

Assuming xml node after parsing your data, try the following

Element element = null; //It is your root node.
NamedNodeMap attrib = (NamedNodeMap) element;

int numAttrs = attrib.getLength ();

for (int i = 0; i < numAttrs; i++) {
    Attr attr = (Attr) attrib.item (i);

    String attrName = attr.getNodeName ();
    String attrValue = attr.getNodeValue ();

    System.out.println ("Found attribute: " + attrName + " with value: " + attrValue);

}
Murtaza Khursheed Hussain
  • 15,176
  • 7
  • 58
  • 83
  • Thanks Murtaza but this just prints `Found attribute: my:animation_factor with value: @dimen/standard_anim_factory` which is what I already had... I wanted it to print `Found attribute: my:animation_factor with value: 1.85`. – Mike Laren May 27 '15 at 06:30
  • it means you need to reiterate it, Hope you understand what I am trying to say. if you found the node then reiterate it that element. – Murtaza Khursheed Hussain May 27 '15 at 06:34