When you are part of a multi-team project in Android, it becomes relatively hard to have a common understanding of how components should be used. This is where Android Lint can help you! In this blog we will show you how you can write your own Lint rules and test them. As an example, we create a sample Lint Detector, which is used to detect whether you have excluded the "secret data" in your application from the Android Authobackup introduced in Android Marshmallow.

 

A short introduction to Android Lint

Before moving on to the sample, let’s quickly visit Android Lint. It basically helps you to improve your code! In a longer description: Android Lint is a code scanning tool as part of Android Studio, it can help you to identify and correct problems with the quality of your application code. It will do this automatically every time you create a (release) build. Android Lint already has quite some rules by itself: it can help you to detect security issues, to improve your code correctness, internationalization, the performance of your app and much more. Lint already has quite some rules which help you doing so. But what if you want to use Lint to enforce your own rule? For this, we need to add a new Lint rule. Better said: we need to add an issue for Lint to detect.

Our sample: make sure we exclude backup data

The basic setup for your own rules

In order to extend Lint, you need to do the following:

  1. Create a Java module within your project
  2. Import the right modules into your project
  3. Create a Detector
  4. Create an IssueRegistry
  5. Configure your build.gradle
  6. Add unit tests.

Step 1: create a Java module within your project

Use Android Studio to create a new Java module within your project. Let’s call it sample-project-lint-extension.

Step 2: Import the right modules into your project

In the root of your new sample-project-lint-extension you will find your build.gradle. Add the following dependencies:

dependencies {
  //Lint dependencies:
  compile 'com.android.tools.lint:lint-api:24.3.1'
  compile 'com.android.tools.lint:lint-checks:24.3.1'
  testCompile 'com.android.tools.lint:lint-tests:24.3.1'
}

 

Step 3: Create your own detectors

Every new Lint rule is captured in an issue. An issue describes the problem that you want to find. The finding/searching/detecting entity is the so called Detector. An issue is declared like this:

public static final Issue BACKUP_ANDROID_6_EXCLUDE_IMPORTANT_DATA = 
  Issue.create( "BackupAndroidSixExcludeImportantData", 
  "You have used the full-backup-content tag, but did not exclude 'importantdata'", 
  "When you allow the Android backup system from API lvl 23 and up to do backups, "
    + "then you should exclude 'importantdata', the rule can be defined as follows: "
    + "<exclude domain=\"sharedpref\" path=\"importantdata\">", 
  Category.SECURITY, 
  6, 
  Severity.WARNING, 
  new Implementation(Android6BackupDetector.class, Scope.ALL_RESOURCES_SCOPE));

 

Most of the parameters should speak for themselves the moment you test them, just note that “6” is a priority ranging from 1 to 10 with 10 being the most important/severe.

This Issue can then be placed within a Detector class, which is used to detect the actual issue at hand.

Detectors can be used for anything within your application code: you can use them to detect issues in your Java code, issues in your res folder, issues in your Android manifest file and more. Our sample detector is used to detect whether you have excluded some of the application data from the Android Authobackup introduced in Android Marshmallow.

For this, we need to scan the XML resources and we need to create a detector that can parse through your backup definitions. The detector class itself looks like this:

public class Android6BackupDetector extends ResourceXmlDetector{
  public static final Issue BACKUP_ANDROID_6_EXCLUDE_IMPORTANT_DATA = 
    Issue.create( "BackupAndroidSixExcludeImportantData", 
      "You have used the full-backup-content tag, but did not exclude 'importantdata'", 
      "When you allow the Android backup system from API lvl 23 and up to do backups, "
      + "then you should exclude 'importantdata', the rule can be defined as follows: "
      + "<exclude domain=\"sharedpref\" path=\"importantdata\">", 
      Category.SECURITY, 
      6, Severity.WARNING, 
      new Implementation(Android6BackupDetector.class, Scope.ALL_RESOURCES_SCOPE)
    ); 

  @Override 
  public Collection <String> getApplicableElements() { 
    return Collections.singletonList("full-backup-content"); 
  } 
      
  @Override 
  public void visitElement(@NonNull XmlContext context, @NonNull Element element) { 
    boolean hasSecretDataExcluded = false; 
    if (element.hasChildNodes()) { 
      NodeList children = element.getChildNodes(); 
      for (int i = 0; i < children.getLength(); i++) { 
        Node child = children.item(i); 
        if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals("exclude")) { 
          if (child.getAttributes().getNamedItem("path").getNodeValue().equals("importantdata") 
              && child.getAttributes().getNamedItem("domain").getNodeValue().equals("sharedpref")) { 
            hasSecretDataExcluded = true; 
          } 
        } 
      } 
    } 
    if (!hasSecretDataExcluded ) { 
      context.report(BACKUP_ANDROID_6_EXCLUDE_IMPORTANT_DATA, element, context.getLocation(element), 
        "You should exclude the sharedpref of our app!"); 
    } 
  } 
}

 

with the method “getApplicableElements” Lint will scan your xml files to find elements that comply with the criteria that you implement (in our case, we are only interested in the full-backup-content tag). Then, you can visit the given element with the visitElement method. Here we check whether the exclusion has been defined for our “importantdata” tag in the shared preferences.

Step 4: Create your own IssueRegistry

Every custom Lint jar needs to have an IssueRegistry. This is a class containing all the issues you want to detect. Our IssueRegistry looks like this:

public final class IssueRegistry extends com.android.tools.lint.client.api.IssueRegistry{ 

  @Override 
  public List <Issue> getIssues() { 
    return Arrays.asList( Android6BackupDetector.BACKUP_ANDROID_6_EXCLUDE_IMPORTANT_DATA 
      //....you can add more issues here 
    ); 
  } 
   
  ...
}

For in-depth explanation of step 3 & 4: visit http://tools.android.com/tips/lint/writing-a-lint-check and http://tools.android.com/tips/lint-custom-rules.

Step 5: Configure your build.gradle

Now you can extend your build.gradle to look like this:


apply plugin: 'java' 

configurations { 
  lintChecks 
} 

dependencies { 
  // Lint dependencies 
  compile 'com.android.tools.lint:lint-api:24.3.1'
  compile 'com.android.tools.lint:lint-checks:24.3.1'
  testCompile 'com.android.tools.lint:lint-tests:24.3.1' 
  lintChecks files(jar) 
} 

jar { 
  manifest { 
    attributes('Lint-Registry': 'sample.xebia.lint.IssueRegistry') 
  } 
}

with the jar clause you instruct where one can find the IssueRegistry.

Step 6: Add unit tests

Now we can start adding unit tests. You can either extend the AbstractCheckTest when you have a lot of additional tests, or you can directly extend the LintDetectorTest. Let’s do that for now.

We have to test at least three different scenario’s:

1. we miss the important data as an exclusion;

2. we do include the important data, but forget it is a sharedpref (and not a database);

3. we properly have included it in our exclusion path.

public class Android6BackupDetectorTest extends LintDetectorTest { 
 @Override 
 protected Detector getDetector() { 
   return new Android6BackupDetector(); 
 } 

 @Override 
 protected List <Issue> getIssues() { 
   return Collections.singletonList( Android6BackupDetector.BACKUP_ANDROID_6_EXCLUDE_IMPORTANT_DATA ); 
 } 

  public void testOk() throws Exception { 
    assertEquals("No warnings.", 
      lintProject(xml("res/xml/backup.xml", "" 
        + "<?xml version=\"1.0\" encoding=\"utf-8\"?&>\n" 
        + "<full-backup-content>\n" 
        + "<exclude domain=\"sharedpref\" path=\"importantdata\">\n" 
        + "<exclude domain=\"sharedpref\" path=\"gcm\">\n" 
        + "</full-backup-content>\n" )
      )
   ); 
 } 

  public void testBackupContainsSecretData() throws Exception { 
    assertEquals("res/xml/backup.xml:2: Warning: You should exclude the sharedpref of our app! [BackupAndroidSixExcludeImportantData]\n" 
      + "<full-backup-content>;" 
      + "\n" 
      + "0 errors, 1 warnings\n", 
        lintProject(xml("res/xml/backup.xml",
         "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" 
           + "<full-backup-content>;\n" 
           + "<exclude domain=\"database\" path=\"importantdata\">\n" 
           + "<exclude domain=\"sharedpref\" path=\"gcm\"/>\n" 
           + "</full-backup-content>\n")
        )
    ); 
  } 
   
  public void testMissing() throws Exception { 
    assertEquals("res/xml/backup.xml:2: Warning: You should exclude the sharedpref of our app! [BackupAndroidSixExcludeImportantData]\n" 
      + "<full-backup-content>\n" 
      + "^\n" + "0 errors, 1 warnings\n", 
        lintProject(xml("res/xml/backup.xml", "" 
          + "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" 
          + "<full-backup-content>\n" 
          + "<exclude domain=\"sharedpref\" path=\"gcm\"/>\n" 
          + "</full-backup-content>\n" )
        )
     ); 
  } 
}

And you’re done!

Now you can start using your tested Lint-controls! Copy the created jar file to your Android (ANDROID_SDK_HOME)/lint directory and it will be checked in the future. If you want to make sure you exported it correctly, Run “lint - - show BackupAndroidSixExcludeImportantData