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.ArraySet; 21 import android.util.SparseArray; 22 23 import androidx.annotation.NonNull; 24 25 import com.android.internal.annotations.VisibleForTesting; 26 27 import java.util.Set; 28 29 /** 30 * Utility class to check SDK level on a device. 31 * 32 * <p>Prefer using {@link SdkLevel} if the version is known at build time. This should only be used 33 * when a dynamic runtime check is needed. 34 */ 35 public final class UnboundedSdkLevel { 36 37 /** 38 * Checks if the device is running on a given or newer version of Android. 39 */ isAtLeast(@onNull String version)40 public static boolean isAtLeast(@NonNull String version) { 41 return sInstance.isAtLeastInternal(version); 42 } 43 44 /** 45 * Checks if the device is running on a given or older version of Android. 46 */ isAtMost(@onNull String version)47 public static boolean isAtMost(@NonNull String version) { 48 return sInstance.isAtMostInternal(version); 49 } 50 51 private static final SparseArray<Set<String>> PREVIOUS_CODENAMES = new SparseArray<>(4); 52 53 static { 54 PREVIOUS_CODENAMES.put(29, setOf("Q")); 55 PREVIOUS_CODENAMES.put(30, setOf("Q", "R")); 56 PREVIOUS_CODENAMES.put(31, setOf("Q", "R", "S")); 57 PREVIOUS_CODENAMES.put(32, setOf("Q", "R", "S", "Sv2")); 58 } 59 setOf(String .... contents)60 private static Set<String> setOf(String ... contents) { 61 if (SdkLevel.isAtLeastR()) { 62 return Set.of(contents); 63 } 64 // legacy code for Q 65 Set<String> set = new ArraySet(contents.length); 66 for (String codename : contents) { 67 set.add(codename); 68 } 69 return set; 70 } 71 72 private static final UnboundedSdkLevel sInstance = 73 new UnboundedSdkLevel( 74 Build.VERSION.SDK_INT, 75 Build.VERSION.CODENAME, 76 SdkLevel.isAtLeastT() 77 ? Build.VERSION.KNOWN_CODENAMES 78 : PREVIOUS_CODENAMES.get(Build.VERSION.SDK_INT)); 79 80 private final int mSdkInt; 81 private final String mCodename; 82 private final boolean mIsReleaseBuild; 83 private final Set<String> mKnownCodenames; 84 85 @VisibleForTesting UnboundedSdkLevel(int sdkInt, String codename, Set<String> knownCodenames)86 UnboundedSdkLevel(int sdkInt, String codename, Set<String> knownCodenames) { 87 mSdkInt = sdkInt; 88 mCodename = codename; 89 mIsReleaseBuild = "REL".equals(codename); 90 mKnownCodenames = knownCodenames; 91 } 92 93 @VisibleForTesting isAtLeastInternal(@onNull String version)94 boolean isAtLeastInternal(@NonNull String version) { 95 version = removeFingerprint(version); 96 if (mIsReleaseBuild) { 97 if (isCodename(version)) { 98 // On release builds only accept future codenames 99 if (mKnownCodenames.contains(version)) { 100 throw new IllegalArgumentException("Artifact with a known codename " + version 101 + " must be recompiled with a finalized integer version."); 102 } 103 // mSdkInt is always less than future codenames 104 return false; 105 } 106 return mSdkInt >= Integer.parseInt(version); 107 } 108 if (isCodename(version)) { 109 return mKnownCodenames.contains(version); 110 } 111 // Never assume what the next SDK level is until SDK finalization completes. 112 // SDK_INT is always assigned the latest finalized value of the SDK. 113 return mSdkInt >= Integer.parseInt(version); 114 } 115 116 @VisibleForTesting isAtMostInternal(@onNull String version)117 boolean isAtMostInternal(@NonNull String version) { 118 version = removeFingerprint(version); 119 if (mIsReleaseBuild) { 120 if (isCodename(version)) { 121 // On release builds only accept future codenames 122 if (mKnownCodenames.contains(version)) { 123 throw new IllegalArgumentException("Artifact with a known codename " + version 124 + " must be recompiled with a finalized integer version."); 125 } 126 // mSdkInt is always less than future codenames 127 return true; 128 } 129 return mSdkInt <= Integer.parseInt(version); 130 } 131 if (isCodename(version)) { 132 return !mKnownCodenames.contains(version) || mCodename.equals(version); 133 } 134 // Never assume what the next SDK level is until SDK finalization completes. 135 // SDK_INT is always assigned the latest finalized value of the SDK. 136 // 137 // Note: multiple releases can be in development at the same time. For example, during 138 // Sv2 and Tiramisu development, both builds have SDK_INT=31 which is not sufficient 139 // information to differentiate between them. Also, "31" at that point already corresponds 140 // to a previously finalized API level, meaning that the current build is not at most "31". 141 // This is why the comparison is strict, instead of <=. 142 return mSdkInt < Integer.parseInt(version); 143 } 144 145 /** 146 * Checks if a string is a codename and contains a fingerprint. Returns the codename without the 147 * fingerprint if that is the case. Returns the original string otherwise. 148 */ 149 @VisibleForTesting removeFingerprint(@onNull String version)150 String removeFingerprint(@NonNull String version) { 151 if (isCodename(version)) { 152 int index = version.indexOf('.'); 153 if (index != -1) { 154 return version.substring(0, index); 155 } 156 } 157 return version; 158 } 159 isCodename(String version)160 private boolean isCodename(String version) { 161 if (version.length() == 0) { 162 throw new IllegalArgumentException(); 163 } 164 // assume Android codenames start with upper case letters. 165 return Character.isUpperCase((version.charAt(0))); 166 } 167 168 } 169