1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.modules.utils.build; 18 19 import android.os.Build; 20 import android.util.SparseArray; 21 22 import androidx.annotation.NonNull; 23 24 import com.android.internal.annotations.VisibleForTesting; 25 26 import java.util.Set; 27 28 /** 29 * Utility class to check SDK level on a device. 30 * 31 * <p>Prefer using {@link SdkLevel} if the version is known at build time. This should only be used 32 * when a dynamic runtime check is needed. 33 */ 34 public final class UnboundedSdkLevel { 35 36 /** 37 * Checks if the device is running on a given or newer version of Android. 38 */ isAtLeast(@onNull String version)39 public static boolean isAtLeast(@NonNull String version) { 40 return sInstance.isAtLeastInternal(version); 41 } 42 43 /** 44 * Checks if the device is running on a given or older version of Android. 45 */ isAtMost(@onNull String version)46 public static boolean isAtMost(@NonNull String version) { 47 return sInstance.isAtMostInternal(version); 48 } 49 50 private static final SparseArray<Set<String>> PREVIOUS_CODENAMES = new SparseArray<>(4); 51 52 static { 53 PREVIOUS_CODENAMES.put(29, Set.of("Q")); 54 PREVIOUS_CODENAMES.put(30, Set.of("Q", "R")); 55 PREVIOUS_CODENAMES.put(31, Set.of("Q", "R", "S")); 56 PREVIOUS_CODENAMES.put(32, Set.of("Q", "R", "S", "Sv2")); 57 } 58 59 private static final UnboundedSdkLevel sInstance = 60 new UnboundedSdkLevel( 61 Build.VERSION.SDK_INT, 62 Build.VERSION.CODENAME, 63 SdkLevel.isAtLeastT() 64 ? Build.VERSION.KNOWN_CODENAMES 65 : PREVIOUS_CODENAMES.get(Build.VERSION.SDK_INT)); 66 67 private final int mSdkInt; 68 private final String mCodename; 69 private final boolean mIsReleaseBuild; 70 private final Set<String> mKnownCodenames; 71 72 @VisibleForTesting UnboundedSdkLevel(int sdkInt, String codename, Set<String> knownCodenames)73 UnboundedSdkLevel(int sdkInt, String codename, Set<String> knownCodenames) { 74 mSdkInt = sdkInt; 75 mCodename = codename; 76 mIsReleaseBuild = "REL".equals(codename); 77 mKnownCodenames = knownCodenames; 78 } 79 80 @VisibleForTesting isAtLeastInternal(@onNull String version)81 boolean isAtLeastInternal(@NonNull String version) { 82 version = removeFingerprint(version); 83 if (mIsReleaseBuild) { 84 if (isCodename(version)) { 85 // On release builds only accept future codenames 86 if (mKnownCodenames.contains(version)) { 87 throw new IllegalArgumentException("Artifact with a known codename " + version 88 + " must be recompiled with a finalized integer version."); 89 } 90 // mSdkInt is always less than future codenames 91 return false; 92 } 93 return mSdkInt >= Integer.parseInt(version); 94 } 95 if (isCodename(version)) { 96 return mKnownCodenames.contains(version); 97 } 98 // Never assume what the next SDK level is until SDK finalization completes. 99 // SDK_INT is always assigned the latest finalized value of the SDK. 100 return mSdkInt >= Integer.parseInt(version); 101 } 102 103 @VisibleForTesting isAtMostInternal(@onNull String version)104 boolean isAtMostInternal(@NonNull String version) { 105 version = removeFingerprint(version); 106 if (mIsReleaseBuild) { 107 if (isCodename(version)) { 108 // On release builds only accept future codenames 109 if (mKnownCodenames.contains(version)) { 110 throw new IllegalArgumentException("Artifact with a known codename " + version 111 + " must be recompiled with a finalized integer version."); 112 } 113 // mSdkInt is always less than future codenames 114 return true; 115 } 116 return mSdkInt <= Integer.parseInt(version); 117 } 118 if (isCodename(version)) { 119 return !mKnownCodenames.contains(version) || mCodename.equals(version); 120 } 121 // Never assume what the next SDK level is until SDK finalization completes. 122 // SDK_INT is always assigned the latest finalized value of the SDK. 123 // 124 // Note: multiple releases can be in development at the same time. For example, during 125 // Sv2 and Tiramisu development, both builds have SDK_INT=31 which is not sufficient 126 // information to differentiate between them. Also, "31" at that point already corresponds 127 // to a previously finalized API level, meaning that the current build is not at most "31". 128 // This is why the comparison is strict, instead of <=. 129 return mSdkInt < Integer.parseInt(version); 130 } 131 132 /** 133 * Checks if a string is a codename and contains a fingerprint. Returns the codename without the 134 * fingerprint if that is the case. Returns the original string otherwise. 135 */ 136 @VisibleForTesting removeFingerprint(@onNull String version)137 String removeFingerprint(@NonNull String version) { 138 if (isCodename(version)) { 139 int index = version.indexOf('.'); 140 if (index != -1) { 141 return version.substring(0, index); 142 } 143 } 144 return version; 145 } 146 isCodename(String version)147 private boolean isCodename(String version) { 148 if (version.length() == 0) { 149 throw new IllegalArgumentException(); 150 } 151 // assume Android codenames start with upper case letters. 152 return Character.isUpperCase((version.charAt(0))); 153 } 154 155 } 156