Use Build.VERSION.KNOWN_CODENAMES in UnboundedSdkLevel.

Bug: 211747008
Test: atest UnbounbdedSdkLevelTest
Change-Id: I707d17caf578fb2219d9b236f2fec51236ef3c2d
diff --git a/java/com/android/modules/utils/build/UnboundedSdkLevel.java b/java/com/android/modules/utils/build/UnboundedSdkLevel.java
index ee44f05..76653e0 100644
--- a/java/com/android/modules/utils/build/UnboundedSdkLevel.java
+++ b/java/com/android/modules/utils/build/UnboundedSdkLevel.java
@@ -22,6 +22,9 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.Collections;
+import java.util.Set;
+
 /**
  * Utility class to check SDK level on a device.
  *
@@ -45,28 +48,42 @@
     }
 
     private static final UnboundedSdkLevel sInstance =
-            new UnboundedSdkLevel(Build.VERSION.SDK_INT, Build.VERSION.CODENAME);
+            new UnboundedSdkLevel(
+                    Build.VERSION.SDK_INT,
+                    Build.VERSION.CODENAME,
+                    SdkLevel.isAtLeastT()
+                            ? Build.VERSION.KNOWN_CODENAMES
+                            : Collections.emptySet());
 
     private final int mSdkInt;
     private final String mCodename;
     private final boolean mIsReleaseBuild;
+    private final Set<String> mKnownCodenames;
 
     @VisibleForTesting
-    UnboundedSdkLevel(int sdkInt, String codename) {
+    UnboundedSdkLevel(int sdkInt, String codename, Set<String> knownCodenames) {
         mSdkInt = sdkInt;
         mCodename = codename;
         mIsReleaseBuild = "REL".equals(codename);
+        mKnownCodenames = knownCodenames;
     }
 
     @VisibleForTesting
     boolean isAtLeastInternal(@NonNull String version) {
         if (mIsReleaseBuild) {
-            // On release builds we only expect to install artifacts meant for released
-            // Android Versions. No codenames.
+            if (isCodename(version)) {
+                // On release builds only accept future codenames
+                if (mKnownCodenames.contains(version)) {
+                    throw new IllegalArgumentException("Artifact with a known codename " + version
+                            + " must be recompiled with a finalized integer version.");
+                }
+                // mSdkInt is always less than future codenames
+                return false;
+            }
             return mSdkInt >= Integer.parseInt(version);
         }
         if (isCodename(version)) {
-            return mCodename.compareTo(version) >= 0;
+            return mKnownCodenames.contains(version);
         }
         // Never assume what the next SDK level is until SDK finalization completes.
         // SDK_INT is always assigned the latest finalized value of the SDK.
@@ -76,18 +93,25 @@
     @VisibleForTesting
     boolean isAtMostInternal(@NonNull String version) {
         if (mIsReleaseBuild) {
-            // On release builds we only expect to install artifacts meant for released
-            // Android Versions. No codenames.
+            if (isCodename(version)) {
+                // On release builds only accept future codenames
+                if (mKnownCodenames.contains(version)) {
+                    throw new IllegalArgumentException("Artifact with a known codename " + version
+                            + " must be recompiled with a finalized integer version.");
+                }
+                // mSdkInt is always less than future codenames
+                return true;
+            }
             return mSdkInt <= Integer.parseInt(version);
         }
         if (isCodename(version)) {
-            return mCodename.compareTo(version) <= 0;
+            return !mKnownCodenames.contains(version) || mCodename.equals(version);
         }
         // Never assume what the next SDK level is until SDK finalization completes.
         // SDK_INT is always assigned the latest finalized value of the SDK.
         //
         // Note: multiple releases can be in development at the same time. For example, during
-        // Sv2 and Tiramisu development, both builds have SDK_INT=31 which is not a sufficient
+        // Sv2 and Tiramisu development, both builds have SDK_INT=31 which is not sufficient
         // information to differentiate between them. Also, "31" at that point already corresponds
         // to a previously finalized API level, meaning that the current build is not at most "31".
         // This is why the comparison is strict, instead of <=.
diff --git a/javatests/com/android/modules/utils/build/UnboundedSdkLevelTest.java b/javatests/com/android/modules/utils/build/UnboundedSdkLevelTest.java
index acdde8f..c8d4a47 100644
--- a/javatests/com/android/modules/utils/build/UnboundedSdkLevelTest.java
+++ b/javatests/com/android/modules/utils/build/UnboundedSdkLevelTest.java
@@ -25,31 +25,85 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.util.Set;
+
 @RunWith(AndroidJUnit4.class)
 public class UnboundedSdkLevelTest {
 
     @Test
-    public void testFinalizedSdk() {
-        UnboundedSdkLevel sdkLevel = new UnboundedSdkLevel(31, "REL");
+    public void testFinalizedSdk_S() {
+        // Test against finalized S / 31 release
+        // Note empty set of known codenames, since on S builds Build.VERSION.KNOWN_CODENAMES
+        // does not exist. This is a realistic unit test reflecting S release builds.
+        UnboundedSdkLevel sdkLevel = new UnboundedSdkLevel(31, "REL", Set.of());
 
+        assertThat(sdkLevel.isAtLeastInternal("30")).isTrue();
         assertThat(sdkLevel.isAtLeastInternal("31")).isTrue();
         assertThat(sdkLevel.isAtLeastInternal("32")).isFalse();
 
-        assertThrows(NumberFormatException.class, () -> sdkLevel.isAtLeastInternal(""));
-        assertThrows(NumberFormatException.class, () -> sdkLevel.isAtLeastInternal("S"));
+        assertThat(sdkLevel.isAtLeastInternal("R")).isFalse();
+        assertThat(sdkLevel.isAtLeastInternal("S")).isFalse();
+        assertThat(sdkLevel.isAtLeastInternal("Sv2")).isFalse();
+        assertThat(sdkLevel.isAtLeastInternal("Tiramisu")).isFalse();
+        assertThat(sdkLevel.isAtLeastInternal("U")).isFalse();
+
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtLeastInternal(""));
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtLeastInternal("current"));
 
         assertThat(sdkLevel.isAtMostInternal("30")).isFalse();
         assertThat(sdkLevel.isAtMostInternal("31")).isTrue();
         assertThat(sdkLevel.isAtMostInternal("32")).isTrue();
 
-        assertThrows(NumberFormatException.class, () -> sdkLevel.isAtMostInternal(""));
-        assertThrows(NumberFormatException.class, () -> sdkLevel.isAtMostInternal("S"));
+        assertThat(sdkLevel.isAtMostInternal("R")).isTrue();
+        assertThat(sdkLevel.isAtMostInternal("S")).isTrue();
+        assertThat(sdkLevel.isAtMostInternal("Sv2")).isTrue();
+        assertThat(sdkLevel.isAtMostInternal("Tiramisu")).isTrue();
+        assertThat(sdkLevel.isAtMostInternal("U")).isTrue();
+
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtMostInternal(""));
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtMostInternal("current"));
+    }
+
+    @Test
+    public void testFinalizedSdk() {
+        // Test against finalized S / 31 release
+        UnboundedSdkLevel sdkLevel = new UnboundedSdkLevel(31, "REL", Set.of("Q", "R", "S"));
+
+        assertThat(sdkLevel.isAtLeastInternal("30")).isTrue();
+        assertThat(sdkLevel.isAtLeastInternal("31")).isTrue();
+        assertThat(sdkLevel.isAtLeastInternal("32")).isFalse();
+
+        // R and S must have been re-compiled and changed to integer after SDK finalization
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtLeastInternal("R"));
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtLeastInternal("S"));
+
+        // Future versions are S+
+        assertThat(sdkLevel.isAtLeastInternal("Sv2")).isFalse();
+        assertThat(sdkLevel.isAtLeastInternal("Tiramisu")).isFalse();
+        assertThat(sdkLevel.isAtLeastInternal("U")).isFalse();
+
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtLeastInternal(""));
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtLeastInternal("current"));
+
+        assertThat(sdkLevel.isAtMostInternal("30")).isFalse();
+        assertThat(sdkLevel.isAtMostInternal("31")).isTrue();
+        assertThat(sdkLevel.isAtMostInternal("32")).isTrue();
+
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtMostInternal("R"));
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtMostInternal("S"));
+        assertThat(sdkLevel.isAtMostInternal("Sv2")).isTrue();
+        assertThat(sdkLevel.isAtMostInternal("Tiramisu")).isTrue();
+        assertThat(sdkLevel.isAtMostInternal("U")).isTrue();
+
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtMostInternal(""));
+        assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtMostInternal("current"));
     }
 
     @Test
     public void testNonFinalizedSdk_Sv2() {
-        UnboundedSdkLevel sdkLevel = new UnboundedSdkLevel(31, "Sv2");
+        UnboundedSdkLevel sdkLevel = new UnboundedSdkLevel(31, "Sv2", Set.of("Q", "R", "S", "Sv2"));
 
+        assertThat(sdkLevel.isAtLeastInternal("30")).isTrue();
         assertThat(sdkLevel.isAtLeastInternal("31")).isTrue();
         assertThat(sdkLevel.isAtLeastInternal("32")).isFalse();
 
@@ -78,8 +132,10 @@
 
     @Test
     public void testNonFinalizedSdk_Tiramisu() {
-        UnboundedSdkLevel sdkLevel = new UnboundedSdkLevel(31, "Tiramisu");
+        UnboundedSdkLevel sdkLevel = new UnboundedSdkLevel(31, "Tiramisu",
+                Set.of("Q", "R", "S", "Sv2", "Tiramisu"));
 
+        assertThat(sdkLevel.isAtLeastInternal("30")).isTrue();
         assertThat(sdkLevel.isAtLeastInternal("31")).isTrue();
         assertThat(sdkLevel.isAtLeastInternal("32")).isFalse();
 
@@ -106,4 +162,6 @@
         assertThrows(IllegalArgumentException.class, () -> sdkLevel.isAtMostInternal("current"));
     }
 
+
+
 }