/* * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.modules.utils.build; import android.os.Build; import android.util.ArraySet; import android.util.SparseArray; import androidx.annotation.NonNull; import com.android.internal.annotations.VisibleForTesting; import java.util.Set; /** * Utility class to check SDK level on a device. * *

Prefer using {@link SdkLevel} if the version is known at build time. This should only be used * when a dynamic runtime check is needed. */ public final class UnboundedSdkLevel { /** * Checks if the device is running on a given or newer version of Android. */ public static boolean isAtLeast(@NonNull String version) { return sInstance.isAtLeastInternal(version); } /** * Checks if the device is running on a given or older version of Android. */ public static boolean isAtMost(@NonNull String version) { return sInstance.isAtMostInternal(version); } private static final SparseArray> PREVIOUS_CODENAMES = new SparseArray<>(4); static { PREVIOUS_CODENAMES.put(29, setOf("Q")); PREVIOUS_CODENAMES.put(30, setOf("Q", "R")); PREVIOUS_CODENAMES.put(31, setOf("Q", "R", "S")); PREVIOUS_CODENAMES.put(32, setOf("Q", "R", "S", "Sv2")); } private static Set setOf(String ... contents) { if (SdkLevel.isAtLeastR()) { return Set.of(contents); } // legacy code for Q Set set = new ArraySet(contents.length); for (String codename : contents) { set.add(codename); } return set; } private static final UnboundedSdkLevel sInstance = new UnboundedSdkLevel( Build.VERSION.SDK_INT, Build.VERSION.CODENAME, SdkLevel.isAtLeastT() ? Build.VERSION.KNOWN_CODENAMES : PREVIOUS_CODENAMES.get(Build.VERSION.SDK_INT)); private final int mSdkInt; private final String mCodename; private final boolean mIsReleaseBuild; private final Set mKnownCodenames; @VisibleForTesting UnboundedSdkLevel(int sdkInt, String codename, Set knownCodenames) { mSdkInt = sdkInt; mCodename = codename; mIsReleaseBuild = "REL".equals(codename); mKnownCodenames = knownCodenames; } @VisibleForTesting boolean isAtLeastInternal(@NonNull String version) { version = removeFingerprint(version); if (mIsReleaseBuild) { 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 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. return mSdkInt >= Integer.parseInt(version); } @VisibleForTesting boolean isAtMostInternal(@NonNull String version) { version = removeFingerprint(version); if (mIsReleaseBuild) { 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 !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 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 <=. return mSdkInt < Integer.parseInt(version); } /** * Checks if a string is a codename and contains a fingerprint. Returns the codename without the * fingerprint if that is the case. Returns the original string otherwise. */ @VisibleForTesting String removeFingerprint(@NonNull String version) { if (isCodename(version)) { int index = version.indexOf('.'); if (index != -1) { return version.substring(0, index); } } return version; } private boolean isCodename(String version) { if (version.length() == 0) { throw new IllegalArgumentException(); } // assume Android codenames start with upper case letters. return Character.isUpperCase((version.charAt(0))); } }