Require publicAlternatives for APIs on max-sdk-q

Greylisted APIs can be added to the max-sdk-q list, and public
alternative information is required when doing this.

Bug: 133829620
Test: atest class2greylisttest
Change-Id: Iadcf75d3eb606a6137e755d1245ae2611a416003
diff --git a/tools/class2greylist/src/com/android/class2greylist/ApiResolver.java b/tools/class2greylist/src/com/android/class2greylist/ApiResolver.java
index f07a0af..b120978 100644
--- a/tools/class2greylist/src/com/android/class2greylist/ApiResolver.java
+++ b/tools/class2greylist/src/com/android/class2greylist/ApiResolver.java
@@ -31,6 +31,7 @@
 
     private static final Pattern LINK_TAG_PATTERN = Pattern.compile("\\{@link ([^\\}]+)\\}");
     private static final Pattern CODE_TAG_PATTERN = Pattern.compile("\\{@code ([^\\}]+)\\}");
+    private static final Integer MIN_SDK_REQUIRING_PUBLIC_ALTERNATIVES = 29;
 
     public ApiResolver() {
         mPotentialPublicAlternatives = null;
@@ -58,8 +59,14 @@
      * @param publicAlternativesString String containing public alternative explanations.
      * @param signature                Signature of the member that has the annotation.
      */
-    public void resolvePublicAlternatives(String publicAlternativesString, String signature)
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+    public void resolvePublicAlternatives(String publicAlternativesString, String signature,
+                                          Integer maxSdkVersion)
+            throws JavadocLinkSyntaxError, AlternativeNotFoundError,
+                    RequiredAlternativeNotSpecifiedError {
+        if (Strings.isNullOrEmpty(publicAlternativesString) && maxSdkVersion != null
+                && maxSdkVersion >= MIN_SDK_REQUIRING_PUBLIC_ALTERNATIVES) {
+            throw new RequiredAlternativeNotSpecifiedError();
+        }
         if (publicAlternativesString != null && mPotentialPublicAlternatives != null) {
             // Grab all instances of type {@link foo}
             Matcher matcher = LINK_TAG_PATTERN.matcher(publicAlternativesString);
diff --git a/tools/class2greylist/src/com/android/class2greylist/RequiredAlternativeNotSpecifiedError.java b/tools/class2greylist/src/com/android/class2greylist/RequiredAlternativeNotSpecifiedError.java
new file mode 100644
index 0000000..a65f1fe
--- /dev/null
+++ b/tools/class2greylist/src/com/android/class2greylist/RequiredAlternativeNotSpecifiedError.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.class2greylist;
+
+/**
+ * Exception to be thrown when a greylisted private api gets restricted to max-FOO (where FOO is Q
+ * or later), without providing a public API alternative.
+ */
+public class RequiredAlternativeNotSpecifiedError extends Exception {
+
+}
+
diff --git a/tools/class2greylist/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandler.java b/tools/class2greylist/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandler.java
index 17ec732..3ce00df 100644
--- a/tools/class2greylist/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandler.java
+++ b/tools/class2greylist/src/com/android/class2greylist/UnsupportedAppUsageAnnotationHandler.java
@@ -159,10 +159,17 @@
                     mSdkVersionToFlagMap.keySet());
             return;
         }
+
         try {
-            mApiResolver.resolvePublicAlternatives(publicAlternativesString, signature);
+            mApiResolver.resolvePublicAlternatives(publicAlternativesString, signature,
+                    maxTargetSdk);
         } catch (JavadocLinkSyntaxError | AlternativeNotFoundError e) {
             context.reportError(e.toString());
+        } catch (RequiredAlternativeNotSpecifiedError e) {
+            context.reportError("Signature %s moved to %s without specifying public "
+                            + "alternatives; Refer to go/unsupportedappusage-public-alternatives "
+                            + "for details.",
+                    signature, mSdkVersionToFlagMap.get(maxTargetSdk));
         }
 
         // Consume this annotation if it matches the predicate.
diff --git a/tools/class2greylist/test/src/com/android/class2greylist/ApiResolverTest.java b/tools/class2greylist/test/src/com/android/class2greylist/ApiResolverTest.java
index 8790879..888a9b5 100644
--- a/tools/class2greylist/test/src/com/android/class2greylist/ApiResolverTest.java
+++ b/tools/class2greylist/test/src/com/android/class2greylist/ApiResolverTest.java
@@ -17,12 +17,10 @@
 package com.android.class2greylist;
 
 import static com.google.common.truth.Truth.assertThat;
+
 import static org.testng.Assert.expectThrows;
 import static org.testng.Assert.assertThrows;
-import static org.junit.Assert.fail;
 
-import org.hamcrest.Description;
-import org.hamcrest.TypeSafeMatcher;
 import org.junit.Test;
 
 import java.util.Arrays;
@@ -33,48 +31,52 @@
 public class ApiResolverTest extends AnnotationHandlerTestBase {
     @Test
     public void testFindPublicAlternativeExactly()
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError {
-        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+            throws JavadocLinkSyntaxError, AlternativeNotFoundError,
+            RequiredAlternativeNotSpecifiedError {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>(
                 Arrays.asList("La/b/C;->foo(I)V", "La/b/C;->bar(I)V")));
         ApiResolver resolver = new ApiResolver(publicApis);
-        resolver.resolvePublicAlternatives("{@link a.b.C#foo(int)}", "Lb/c/D;->bar()V");
+        resolver.resolvePublicAlternatives("{@link a.b.C#foo(int)}", "Lb/c/D;->bar()V", 1);
     }
 
     @Test
     public void testFindPublicAlternativeDeducedPackageName()
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError {
-        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+            throws JavadocLinkSyntaxError, AlternativeNotFoundError,
+            RequiredAlternativeNotSpecifiedError {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>(
                 Arrays.asList("La/b/C;->foo(I)V", "La/b/C;->bar(I)V")));
         ApiResolver resolver = new ApiResolver(publicApis);
-        resolver.resolvePublicAlternatives("{@link C#foo(int)}", "La/b/D;->bar()V");
+        resolver.resolvePublicAlternatives("{@link C#foo(int)}", "La/b/D;->bar()V", 1);
     }
 
     @Test
     public void testFindPublicAlternativeDeducedPackageAndClassName()
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError {
-        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+            throws JavadocLinkSyntaxError, AlternativeNotFoundError,
+            RequiredAlternativeNotSpecifiedError {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>(
                 Arrays.asList("La/b/C;->foo(I)V", "La/b/C;->bar(I)V")));
         ApiResolver resolver = new ApiResolver(publicApis);
-        resolver.resolvePublicAlternatives("{@link #foo(int)}", "La/b/C;->bar()V");
+        resolver.resolvePublicAlternatives("{@link #foo(int)}", "La/b/C;->bar()V", 1);
     }
 
     @Test
     public void testFindPublicAlternativeDeducedParameterTypes()
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError {
-        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+            throws JavadocLinkSyntaxError, AlternativeNotFoundError,
+            RequiredAlternativeNotSpecifiedError {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>(
                 Arrays.asList("La/b/C;->foo(I)V", "La/b/C;->bar(I)V")));
         ApiResolver resolver = new ApiResolver(publicApis);
-        resolver.resolvePublicAlternatives("{@link #foo}", "La/b/C;->bar()V");
+        resolver.resolvePublicAlternatives("{@link #foo}", "La/b/C;->bar()V", 1);
     }
 
     @Test
     public void testFindPublicAlternativeFailDueToMultipleParameterTypes()
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError, SignatureSyntaxError {
-        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+            throws SignatureSyntaxError {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>(
                 Arrays.asList("La/b/C;->foo(I)V", "La/b/C;->bar(I)I", "La/b/C;->foo(II)V")));
         ApiResolver resolver = new ApiResolver(publicApis);
         MultipleAlternativesFoundError e = expectThrows(MultipleAlternativesFoundError.class,
-                () -> resolver.resolvePublicAlternatives("{@link #foo}", "La/b/C;->bar()V"));
+                () -> resolver.resolvePublicAlternatives("{@link #foo}", "La/b/C;->bar()V", 1));
         assertThat(e.almostMatches).containsExactly(
                 ApiComponents.fromDexSignature("La/b/C;->foo(I)V"),
                 ApiComponents.fromDexSignature("La/b/C;->foo(II)V")
@@ -82,59 +84,76 @@
     }
 
     @Test
-    public void testFindPublicAlternativeFailNoAlternative()
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError {
-        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+    public void testFindPublicAlternativeFailNoAlternative() {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>(
                 Arrays.asList("La/b/C;->bar(I)V")));
         ApiResolver resolver = new ApiResolver(publicApis);
         assertThrows(MemberAlternativeNotFoundError.class, ()
-                -> resolver.resolvePublicAlternatives("{@link #foo(int)}", "La/b/C;->bar()V"));
+                -> resolver.resolvePublicAlternatives("{@link #foo(int)}", "La/b/C;->bar()V", 1));
     }
 
     @Test
-    public void testFindPublicAlternativeFailNoAlternativeNoParameterTypes()
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError {
+    public void testFindPublicAlternativeFailNoAlternativeNoParameterTypes() {
 
-        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>(
                 Arrays.asList("La/b/C;->bar(I)V")));
         ApiResolver resolver = new ApiResolver(publicApis);
         assertThrows(MemberAlternativeNotFoundError.class,
-                () -> resolver.resolvePublicAlternatives("{@link #foo}", "La/b/C;->bar()V"));
+                () -> resolver.resolvePublicAlternatives("{@link #foo}", "La/b/C;->bar()V", 1));
     }
 
     @Test
-    public void testNoPublicClassAlternatives()
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError {
-        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>());
+    public void testNoPublicClassAlternatives() {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>());
         ApiResolver resolver = new ApiResolver(publicApis);
         expectThrows(NoAlternativesSpecifiedError.class,
-                () -> resolver.resolvePublicAlternatives("Foo", "La/b/C;->bar()V"));
+                () -> resolver.resolvePublicAlternatives("Foo", "La/b/C;->bar()V", 1));
     }
 
     @Test
     public void testPublicAlternativesJustPackageAndClassName()
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError {
-        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+            throws JavadocLinkSyntaxError, AlternativeNotFoundError,
+            RequiredAlternativeNotSpecifiedError {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>(
                 Arrays.asList("La/b/C;->bar(I)V")));
         ApiResolver resolver = new ApiResolver(publicApis);
-        resolver.resolvePublicAlternatives("Foo {@link a.b.C}", "Lb/c/D;->bar()V");
+        resolver.resolvePublicAlternatives("Foo {@link a.b.C}", "Lb/c/D;->bar()V", 1);
     }
 
     @Test
     public void testPublicAlternativesJustClassName()
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError {
-        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>(
+            throws JavadocLinkSyntaxError, AlternativeNotFoundError,
+            RequiredAlternativeNotSpecifiedError {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>(
                 Arrays.asList("La/b/C;->bar(I)V")));
         ApiResolver resolver = new ApiResolver(publicApis);
-        resolver.resolvePublicAlternatives("Foo {@link C}", "La/b/D;->bar()V");
+        resolver.resolvePublicAlternatives("Foo {@link C}", "La/b/D;->bar()V", 1);
     }
 
     @Test
     public void testNoPublicAlternativesButHasExplanation()
-            throws JavadocLinkSyntaxError, AlternativeNotFoundError {
-        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<String>());
+            throws JavadocLinkSyntaxError, AlternativeNotFoundError,
+            RequiredAlternativeNotSpecifiedError {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>());
         ApiResolver resolver = new ApiResolver(publicApis);
-        resolver.resolvePublicAlternatives("Foo {@code bar}", "La/b/C;->bar()V");
+        resolver.resolvePublicAlternatives("Foo {@code bar}", "La/b/C;->bar()V", 1);
+    }
+
+    @Test
+    public void testNoPublicAlternativesSpecifiedWithMaxSdk() {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>());
+        ApiResolver resolver = new ApiResolver(publicApis);
+        assertThrows(RequiredAlternativeNotSpecifiedError.class,
+                () -> resolver.resolvePublicAlternatives(null, "La/b/C;->bar()V", 29));
+    }
+
+    @Test
+    public void testNoPublicAlternativesSpecifiedWithMaxLessThanQ()
+            throws JavadocLinkSyntaxError, AlternativeNotFoundError,
+            RequiredAlternativeNotSpecifiedError {
+        Set<String> publicApis = Collections.unmodifiableSet(new HashSet<>());
+        ApiResolver resolver = new ApiResolver(publicApis);
+        resolver.resolvePublicAlternatives(null, "La/b/C;->bar()V", 28);
     }
 
 }
\ No newline at end of file