Lint to verify @SystemApi permissions; more docs.

Most @SystemApi methods should be protected with system (or higher)
permissions, so verify that system service methods are annotated with
any relevant @RequiresPermission.

Auto-document new @SystemService annotation to guide developers on
how to obtain manager instances.

Test: make -j32 update-api && make -j32 offline-sdk-docs
Bug: 62263906
Change-Id: I57ae661241557970af4328ff21abec7629492c7c
diff --git a/res/assets/templates-sdk/class.cs b/res/assets/templates-sdk/class.cs
index 9ce6a6d..66134f8 100644
--- a/res/assets/templates-sdk/class.cs
+++ b/res/assets/templates-sdk/class.cs
@@ -319,6 +319,10 @@
   <p><?cs call:tag_list(class.descr) ?></p>
 <?cs /if ?>
 
+<?cs if:subcount(class.descrAux) ?>
+  <?cs call:aux_tag_list(class.descrAux) ?>
+<?cs /if ?>
+
 <?cs call:see_also_tags(class.seeAlso) ?>
 <?cs
 #################
diff --git a/res/assets/templates-sdk/macros_override.cs b/res/assets/templates-sdk/macros_override.cs
index 10fb9c1..92be480 100644
--- a/res/assets/templates-sdk/macros_override.cs
+++ b/res/assets/templates-sdk/macros_override.cs
@@ -59,6 +59,7 @@
       elif:tag.kind == "@range" ?><?cs call:dump_range(tag) ?><?cs
       elif:tag.kind == "@intDef" ?><?cs call:dump_int_def(tag) ?><?cs
       elif:tag.kind == "@permission" ?><?cs call:dump_permission(tag) ?><?cs
+      elif:tag.kind == "@service" ?><?cs call:dump_service(tag) ?><?cs
       /if ?><?cs
   /each ?></p><?cs
 /def ?><?cs
@@ -106,3 +107,13 @@
   else ?> permission.<?cs
   /if ?><?cs
 /def ?>
+
+# Print output for @service tags ?><?cs
+def:dump_service(tag) ?>Instances of this class must be obtained using <?cs
+  loop:i = #0, subcount(tag.values) - 1, #2 ?><?cs
+    call:tag_list(tag.values[i].commentTags) ?> with the argument <?cs
+    call:tag_list(tag.values[i+1].commentTags) ?><?cs
+    if i < subcount(tag.values) - 2 ?> or <?cs
+    /if ?><?cs
+  /loop ?>.<?cs
+/def ?>
diff --git a/src/com/google/doclava/AndroidAuxSource.java b/src/com/google/doclava/AndroidAuxSource.java
index 93341ed..d006af0 100644
--- a/src/com/google/doclava/AndroidAuxSource.java
+++ b/src/com/google/doclava/AndroidAuxSource.java
@@ -22,12 +22,59 @@
 import java.util.Map;
 
 public class AndroidAuxSource implements AuxSource {
-  private static final int TYPE_METHOD = 0;
-  private static final int TYPE_FIELD = 1;
+  private static final int TYPE_FIELD = 0;
+  private static final int TYPE_METHOD = 1;
   private static final int TYPE_PARAM = 2;
   private static final int TYPE_RETURN = 3;
 
   @Override
+  public TagInfo[] classAuxTags(ClassInfo clazz) {
+    if (hasSuppress(clazz.annotations())) return TagInfo.EMPTY_ARRAY;
+    ArrayList<TagInfo> tags = new ArrayList<>();
+    for (AnnotationInstanceInfo annotation : clazz.annotations()) {
+      // Document system services
+      if (annotation.type().qualifiedNameMatches("android", "annotation.SystemService")) {
+        ArrayList<TagInfo> valueTags = new ArrayList<>();
+        valueTags
+            .add(new ParsedTagInfo("", "",
+                "{@link android.content.Context#getSystemService(Class)"
+                    + " Context.getSystemService(Class)}",
+                null, SourcePositionInfo.UNKNOWN));
+        valueTags.add(new ParsedTagInfo("", "",
+            "{@code " + clazz.name() + ".class}", null,
+            SourcePositionInfo.UNKNOWN));
+
+        ClassInfo contextClass = annotation.type().findClass("android.content.Context");
+        for (AnnotationValueInfo val : annotation.elementValues()) {
+          switch (val.element().name()) {
+            case "value":
+              final String expected = String.valueOf(val.value());
+              for (FieldInfo field : contextClass.fields()) {
+                if (field.isHiddenOrRemoved()) continue;
+                if (String.valueOf(field.constantValue()).equals(expected)) {
+                  valueTags.add(new ParsedTagInfo("", "",
+                      "{@link android.content.Context#getSystemService(String)"
+                          + " Context.getSystemService(String)}",
+                      null, SourcePositionInfo.UNKNOWN));
+                  valueTags.add(new ParsedTagInfo("", "",
+                      "{@link android.content.Context#" + field.name()
+                          + " Context." + field.name() + "}",
+                      null, SourcePositionInfo.UNKNOWN));
+                }
+              }
+              break;
+          }
+        }
+
+        Map<String, String> args = new HashMap<>();
+        tags.add(new AuxTagInfo("@service", "@service", SourcePositionInfo.UNKNOWN, args,
+            valueTags.toArray(TagInfo.getArray(valueTags.size()))));
+      }
+    }
+    return tags.toArray(TagInfo.getArray(tags.size()));
+  }
+
+  @Override
   public TagInfo[] fieldAuxTags(FieldInfo field) {
     if (hasSuppress(field)) return TagInfo.EMPTY_ARRAY;
     return auxTags(TYPE_FIELD, field.annotations());
diff --git a/src/com/google/doclava/AndroidLinter.java b/src/com/google/doclava/AndroidLinter.java
index 168448e..399226d 100644
--- a/src/com/google/doclava/AndroidLinter.java
+++ b/src/com/google/doclava/AndroidLinter.java
@@ -45,18 +45,18 @@
         if (text.contains("Broadcast Action:")
             || (text.contains("protected intent") && text.contains("system"))) {
           if (!hasBehavior) {
-            Errors.error(Errors.BROADCAST_BEHAVIOR, field.position(),
+            Errors.error(Errors.BROADCAST_BEHAVIOR, field,
                 "Field '" + field.name() + "' is missing @BroadcastBehavior");
           }
           if (!hasSdkConstant) {
-            Errors.error(Errors.SDK_CONSTANT, field.position(), "Field '" + field.name()
+            Errors.error(Errors.SDK_CONSTANT, field, "Field '" + field.name()
                 + "' is missing @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)");
           }
         }
 
         if (text.contains("Activity Action:")) {
           if (!hasSdkConstant) {
-            Errors.error(Errors.SDK_CONSTANT, field.position(), "Field '" + field.name()
+            Errors.error(Errors.SDK_CONSTANT, field, "Field '" + field.name()
                 + "' is missing @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)");
           }
         }
@@ -95,7 +95,7 @@
             String perm = String.valueOf(value.value());
             if (perm.indexOf('.') >= 0) perm = perm.substring(perm.lastIndexOf('.') + 1);
             if (text.contains(perm)) {
-              Errors.error(Errors.REQUIRES_PERMISSION, method.position(), "Method '" + method.name()
+              Errors.error(Errors.REQUIRES_PERMISSION, method, "Method '" + method.name()
                   + "' documentation mentions permissions already declared by @RequiresPermission");
             }
           }
@@ -103,7 +103,7 @@
       }
       if (text.contains("android.Manifest.permission") || text.contains("android.permission.")) {
         if (!hasAnnotation) {
-          Errors.error(Errors.REQUIRES_PERMISSION, method.position(), "Method '" + method.name()
+          Errors.error(Errors.REQUIRES_PERMISSION, method, "Method '" + method.name()
               + "' documentation mentions permissions without declaring @RequiresPermission");
         }
       }
diff --git a/src/com/google/doclava/AuxSource.java b/src/com/google/doclava/AuxSource.java
index 09215fd..03594f5 100644
--- a/src/com/google/doclava/AuxSource.java
+++ b/src/com/google/doclava/AuxSource.java
@@ -17,6 +17,7 @@
 package com.google.doclava;
 
 public interface AuxSource {
+  public TagInfo[] classAuxTags(ClassInfo clazz);
   public TagInfo[] fieldAuxTags(FieldInfo field);
   public TagInfo[] methodAuxTags(MethodInfo method);
   public TagInfo[] paramAuxTags(MethodInfo method, ParameterInfo param);
@@ -25,6 +26,11 @@
 
 class EmptyAuxSource implements AuxSource {
   @Override
+  public TagInfo[] classAuxTags(ClassInfo clazz) {
+    return TagInfo.EMPTY_ARRAY;
+  }
+
+  @Override
   public TagInfo[] fieldAuxTags(FieldInfo field) {
     return TagInfo.EMPTY_ARRAY;
   }
diff --git a/src/com/google/doclava/ClassInfo.java b/src/com/google/doclava/ClassInfo.java
index b2e3344..20addc6 100644
--- a/src/com/google/doclava/ClassInfo.java
+++ b/src/com/google/doclava/ClassInfo.java
@@ -1216,6 +1216,7 @@
 
     // class description
     TagInfo.makeHDF(data, "class.descr", inlineTags());
+    TagInfo.makeHDF(data, "class.descrAux", Doclava.auxSource.classAuxTags(this));
     TagInfo.makeHDF(data, "class.seeAlso", comment().seeTags());
     TagInfo.makeHDF(data, "class.deprecated", deprecatedTags());
 
diff --git a/src/com/google/doclava/ClearPage.java b/src/com/google/doclava/ClearPage.java
index 1b4b5ca..95392e0 100644
--- a/src/com/google/doclava/ClearPage.java
+++ b/src/com/google/doclava/ClearPage.java
@@ -177,9 +177,10 @@
       return;
     }
     if (!isValidContentType(allowExcepted, toPath, DROIDDOC_VALID_CONTENT_TYPES)) {
-        Errors.error(Errors.INVALID_CONTENT_TYPE, null, "Failed to process " + from
-                + ": Invalid file type. Please move the file to frameworks/base/docs/image_sources/... or docs/downloads/...");
-        return;
+      Errors.error(Errors.INVALID_CONTENT_TYPE, (SourcePositionInfo) null, "Failed to process "
+          + from + ": Invalid file type. Please move the file to "
+          + "frameworks/base/docs/image_sources/... or docs/downloads/...");
+      return;
     }
 
     long sizel = from.length();
diff --git a/src/com/google/doclava/Errors.java b/src/com/google/doclava/Errors.java
index de92fdf..48202a3 100644
--- a/src/com/google/doclava/Errors.java
+++ b/src/com/google/doclava/Errors.java
@@ -81,6 +81,26 @@
     }
   }
 
+  public static void error(Error error, MemberInfo mi, String text) {
+    if (error.getLevel() == Errors.LINT) {
+      final String ident = "Doclava" + error.code;
+      for (AnnotationInstanceInfo a : mi.annotations()) {
+        if (a.type().qualifiedNameMatches("android", "annotation.SuppressLint")) {
+          for (AnnotationValueInfo val : a.elementValues()) {
+            if ("value".equals(val.element().name())) {
+              for (AnnotationValueInfo inner : (ArrayList<AnnotationValueInfo>) val.value()) {
+                if (ident.equals(String.valueOf(inner.value()))) {
+                  return;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+    error(error, mi.position(), text);
+  }
+
   public static void error(Error error, SourcePositionInfo where, String text) {
     if (error.getLevel() == HIDDEN) {
       return;
diff --git a/src/com/google/doclava/FederationTagger.java b/src/com/google/doclava/FederationTagger.java
index 5eda42c..f3603a5 100644
--- a/src/com/google/doclava/FederationTagger.java
+++ b/src/com/google/doclava/FederationTagger.java
@@ -66,7 +66,8 @@
     
     for (String name : federatedXmls.keySet()) {
       if (!federatedUrls.containsKey(name)) {
-        Errors.error(Errors.NO_FEDERATION_DATA, null, "Unknown documentation site for " + name);
+        Errors.error(Errors.NO_FEDERATION_DATA, (SourcePositionInfo) null,
+            "Unknown documentation site for " + name);
       }
     }
     
@@ -83,7 +84,7 @@
         if (e.getMessage() != null) {
           error += ": " + e.getMessage();
         }
-        Errors.error(Errors.NO_FEDERATION_DATA, null, error);
+        Errors.error(Errors.NO_FEDERATION_DATA, (SourcePositionInfo) null, error);
       }
     }
     
diff --git a/src/com/google/doclava/SinceTagger.java b/src/com/google/doclava/SinceTagger.java
index b8ad418..ce2cee3 100644
--- a/src/com/google/doclava/SinceTagger.java
+++ b/src/com/google/doclava/SinceTagger.java
@@ -66,8 +66,9 @@
       } catch (ApiParseException e) {
         StringWriter stackTraceWriter = new StringWriter();
         e.printStackTrace(new PrintWriter(stackTraceWriter));
-        Errors.error(Errors.BROKEN_SINCE_FILE, null, "Failed to parse " + xmlFile
-                + " for " + versionName + " since data.\n" + stackTraceWriter.toString());
+        Errors.error(Errors.BROKEN_SINCE_FILE, (SourcePositionInfo) null,
+            "Failed to parse " + xmlFile + " for " + versionName + " since data.\n"
+                + stackTraceWriter.toString());
         continue;
       }
 
diff --git a/src/com/google/doclava/Stubs.java b/src/com/google/doclava/Stubs.java
index 5c70820..added35 100644
--- a/src/com/google/doclava/Stubs.java
+++ b/src/com/google/doclava/Stubs.java
@@ -1292,6 +1292,40 @@
       return hasWrittenPackageHead;
     }
 
+    // Look for Android @SystemApi exposed outside the normal SDK; we require
+    // that they're protected with a system permission.
+    if (Doclava.android && Doclava.showAnnotations.contains("android.annotation.SystemApi")
+        && !(predicate instanceof RemovedPredicate)) {
+      boolean systemService = false;
+      for (AnnotationInstanceInfo a : cl.annotations()) {
+        if (a.type().qualifiedNameMatches("android", "annotation.SystemService")) {
+          systemService = true;
+        }
+      }
+      if (systemService) {
+        for (MethodInfo mi : methods) {
+          boolean hasPermission = false;
+          for (AnnotationInstanceInfo a : mi.annotations()) {
+            if (a.type().qualifiedNameMatches("android", "annotation.RequiresPermission")) {
+              hasPermission = true;
+            }
+          }
+          for (ParameterInfo pi : mi.parameters()) {
+            for (AnnotationInstanceInfo a : pi.annotations()) {
+              if (a.type().qualifiedNameMatches("android", "annotation.RequiresPermission")) {
+                hasPermission = true;
+              }
+            }
+          }
+          if (!hasPermission) {
+            Errors.error(Errors.REQUIRES_PERMISSION, mi,
+                "Method '" + mi.name() + "' exposed as @SystemApi must be"
+                    + " protected with a system permission.");
+          }
+        }
+      }
+    }
+
     if (!hasWrittenPackageHead) {
       hasWrittenPackageHead = true;
       apiWriter.print("package ");