• 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.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