• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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