org.robolectric.versioning.AndroidVersions Maven / Gradle / Ivy
Show all versions of annotations Show documentation
package org.robolectric.versioning;
/*
* Copyright (C) 2023 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.
*/
import static java.util.Arrays.asList;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import javax.annotation.Nullable;
/**
* Android versioning is complicated.
* 1) There is a yearly letter release with an increasing of one alpha step each year A-> B, B-> C,
* and so on. While commonly referenced these are not the release numbers. This class calls these
* shortcodes. Also minor version number releases (usually within the same year) will start with the
* same letter.
* 2) There is an SDK_INT field in android.os.Build.VERSION that tracks a version of the internal
* SDK. While useful to track the actual released versions of Android, these are not the release
* number. More importantly, android.os.Build.VERSION uses code names to describe future versions.
* Multiple code names may be in development at once on different branches of Android.
* 3) There is a yearly release major number followed by a minor number, which may or may not be
* used.
* 4) Relevant logic and reasoning should match androidx.core.os.BuildCompat.java with the caveat
* that this class guess at the future release version number and short of the current dev branch.
*
*/
public final class AndroidVersions {
private static boolean warnOnly;
private AndroidVersions() {}
/** Representation of an android release, one that has occurred, or is expected. */
public abstract static class AndroidRelease implements Comparable {
/**
* true if this release has already occurred, false otherwise. If unreleased, the getSdkInt may
* still be that of the prior release.
*/
public abstract int getSdkInt();
/**
* single character short code for the release, multiple characters for minor releases (only
* minor version numbers increment - usually within the same year).
*/
public abstract String getShortCode();
/**
* true if this release has already occurred, false otherwise. If unreleased, the getSdkInt will
* guess at the likely sdk number. Your code will need to recompile if this value changes -
* including most modern build tools; bazle, soong all are full build systems - and as such
* organizations using them have no concerns.
*/
public abstract boolean isReleased();
/** major.minor version number as String. */
public abstract String getVersion();
/**
* Implements comparable.
*
* @param other the object to be compared.
* @return 1 if this is greater than other, 0 if equal, -1 if less
* @throws IllegalStateException if other is not an instance of AndroidRelease.
*/
@Override
public int compareTo(AndroidRelease other) {
if (other == null) {
throw new IllegalStateException(
"Only "
+ AndroidVersions.class.getName()
+ " should define Releases, illegal class "
+ other.getClass());
}
return Integer.compare(this.getSdkInt(), other.getSdkInt());
}
@Override
public String toString() {
return "Android "
+ (this.isReleased() ? "" : "Future ")
+ "Release: "
+ this.getVersion()
+ " ( sdk: "
+ this.getSdkInt()
+ " code: "
+ this.getShortCode()
+ " )";
}
}
/** A released version of Android */
public abstract static class AndroidReleased extends AndroidRelease {
@Override
public boolean isReleased() {
return true;
}
}
/** An in-development version of Android */
public abstract static class AndroidUnreleased extends AndroidRelease {
@Override
public boolean isReleased() {
return false;
}
}
/**
* Version: -1
* ShortCode: ""
* SDK API Level: ""
* release: false
*/
public static final class Unbound extends AndroidUnreleased {
public static final int SDK_INT = -1;
public static final String SHORT_CODE = "_";
public static final String VERSION = "_";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Version: 4.1
* ShortCode: J
* SDK API Level: 16
* release: true
*/
public static final class J extends AndroidReleased {
public static final int SDK_INT = 16;
public static final String SHORT_CODE = "J";
public static final String VERSION = "4.1";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Version: 4.2
* ShortCode: JMR1
* SDK API Level: 17
* release: true
*/
public static final class JMR1 extends AndroidReleased {
public static final int SDK_INT = 17;
public static final String SHORT_CODE = "JMR1";
public static final String VERSION = "4.2";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Version: 4.3
* ShortCode: JMR2
* SDK API Level: 18
* release: true
*/
public static final class JMR2 extends AndroidReleased {
public static final int SDK_INT = 18;
public static final String SHORT_CODE = "JMR2";
public static final String VERSION = "4.3";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Version: 4.4
* ShortCode: K
* SDK API Level: 19
* release: true
*/
public static final class K extends AndroidReleased {
public static final int SDK_INT = 19;
public static final String SHORT_CODE = "K";
public static final String VERSION = "4.4";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
// Skipping K Watch release, which was 20.
/**
* Version: 5.0
* ShortCode: L
* SDK API Level: 21
* release: true
*/
public static final class L extends AndroidReleased {
public static final int SDK_INT = 21;
public static final String SHORT_CODE = "L";
public static final String VERSION = "5.0";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Version: 5.1
* ShortCode: LMR1
* SDK API Level: 22
* release: true
*/
public static final class LMR1 extends AndroidReleased {
public static final int SDK_INT = 22;
public static final String SHORT_CODE = "LMR1";
public static final String VERSION = "5.1";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Version: 6.0
* ShortCode: M
* SDK API Level: 23
* release: true
*/
public static final class M extends AndroidReleased {
public static final int SDK_INT = 23;
public static final String SHORT_CODE = "M";
public static final String VERSION = "6.0";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Version: 7.0
* ShortCode: N
* SDK API Level: 24
* release: true
*/
public static final class N extends AndroidReleased {
public static final int SDK_INT = 24;
public static final String SHORT_CODE = "N";
public static final String VERSION = "7.0";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Release: 7.1
* ShortCode: NMR1
* SDK Framework: 25
* release: true
*/
public static final class NMR1 extends AndroidReleased {
public static final int SDK_INT = 25;
public static final String SHORT_CODE = "NMR1";
public static final String VERSION = "7.1";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Release: 8.0
* ShortCode: O
* SDK API Level: 26
* release: true
*/
public static final class O extends AndroidReleased {
public static final int SDK_INT = 26;
public static final String SHORT_CODE = "O";
public static final String VERSION = "8.0";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Release: 8.1
* ShortCode: OMR1
* SDK API Level: 27
* release: true
*/
public static final class OMR1 extends AndroidReleased {
public static final int SDK_INT = 27;
public static final String SHORT_CODE = "OMR1";
public static final String VERSION = "8.1";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Release: 9.0
* ShortCode: P
* SDK API Level: 28
* release: true
*/
public static final class P extends AndroidReleased {
public static final int SDK_INT = 28;
public static final String SHORT_CODE = "P";
public static final String VERSION = "9.0";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Release: 10.0
* ShortCode: Q
* SDK API Level: 29
* release: true
*/
public static final class Q extends AndroidReleased {
public static final int SDK_INT = 29;
public static final String SHORT_CODE = "Q";
public static final String VERSION = "10.0";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Release: 11.0
* ShortCode: R
* SDK API Level: 30
* release: true
*/
public static final class R extends AndroidReleased {
public static final int SDK_INT = 30;
public static final String SHORT_CODE = "R";
public static final String VERSION = "11.0";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Release: 12.0
* ShortCode: S
* SDK API Level: 31
* release: true
*/
public static final class S extends AndroidReleased {
public static final int SDK_INT = 31;
public static final String SHORT_CODE = "S";
public static final String VERSION = "12.0";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Release: 12.1
* ShortCode: Sv2
* SDK API Level: 32
* release: true
*/
@SuppressWarnings("UPPER_SNAKE_CASE")
public static final class Sv2 extends AndroidReleased {
public static final int SDK_INT = 32;
public static final String SHORT_CODE = "Sv2";
public static final String VERSION = "12.1";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Release: 13.0
* ShortCode: T
* SDK API Level: 33
* release: true
*/
public static final class T extends AndroidReleased {
public static final int SDK_INT = 33;
public static final String SHORT_CODE = "T";
public static final String VERSION = "13.0";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Potential Release: 14.0
* ShortCode: U
* SDK API Level: 34
* release: false
*/
public static final class U extends AndroidReleased {
public static final int SDK_INT = 34;
public static final String SHORT_CODE = "U";
public static final String VERSION = "14.0";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/**
* Potential Release: 15.0
* ShortCode: V
* SDK API Level: 34+
* release: false
*/
public static final class V extends AndroidUnreleased {
public static final int SDK_INT = 35;
public static final String SHORT_CODE = "V";
public static final String VERSION = "15";
@Override
public int getSdkInt() {
return SDK_INT;
}
@Override
public String getShortCode() {
return SHORT_CODE;
}
@Override
public String getVersion() {
return VERSION;
}
}
/** The current release this process is running on. */
public static final AndroidRelease CURRENT;
@Nullable
public static AndroidRelease getReleaseForSdkInt(@Nullable Integer sdkInt) {
if (sdkInt == null) {
return null;
} else {
return information.sdkIntToAllReleases.get(sdkInt);
}
}
public static List getReleases() {
List output = new ArrayList<>();
for (AndroidRelease release : information.allReleases) {
if (release.isReleased()) {
output.add(release);
}
}
return output;
}
public static List getUnreleased() {
List output = new ArrayList<>();
for (AndroidRelease release : information.allReleases) {
if (!release.isReleased()) {
output.add(release);
}
}
return output;
}
/**
* Responsible for aggregating and interpreting the static state representing the current
* AndroidReleases known to AndroidVersions class.
*/
static class SdkInformation {
final List allReleases;
final List> classesWithIllegalNames;
final AndroidRelease latestRelease;
final AndroidRelease earliestUnreleased;
// In the future we may need a multimap for sdkInts should they stay static across releases.
final Map sdkIntToAllReleases = new HashMap<>();
final Map shortCodeToAllReleases = new HashMap<>();
// detected errors
final List> sdkIntCollisions = new ArrayList<>();
Map.Entry sdkApiMisordered = null;
public SdkInformation(
List releases,
List> classesWithIllegalNames) {
this.allReleases = releases;
this.classesWithIllegalNames = classesWithIllegalNames;
AndroidRelease latestRelease = null;
AndroidRelease earliestUnreleased = null;
for (AndroidRelease release : allReleases) {
if (release.isReleased()) {
if (latestRelease == null || latestRelease.compareTo(release) > 0) {
latestRelease = release;
}
} else {
if (earliestUnreleased == null || earliestUnreleased.compareTo(release) < 0) {
earliestUnreleased = release;
}
}
}
this.latestRelease = latestRelease;
this.earliestUnreleased = earliestUnreleased;
verifyStaticInformation();
}
private void verifyStaticInformation() {
for (AndroidRelease release : this.allReleases) {
// Construct a map of all sdkInts to releases and note duplicates
AndroidRelease sdkCollision = this.sdkIntToAllReleases.put(release.getSdkInt(), release);
if (sdkCollision != null) {
this.sdkIntCollisions.add(new AbstractMap.SimpleEntry<>(release, sdkCollision));
}
// Construct a map of all short codes to releases, and note duplicates
this.shortCodeToAllReleases.put(release.getShortCode(), release);
// There is no need to check for shortCode duplicates as the Field name must match the
// short code.
}
if (earliestUnreleased != null
&& latestRelease != null
&& latestRelease.getSdkInt() >= earliestUnreleased.getSdkInt()) {
sdkApiMisordered = new AbstractMap.SimpleEntry<>(latestRelease, earliestUnreleased);
}
}
private void handleStaticErrors() {
StringBuilder errors = new StringBuilder();
if (!this.classesWithIllegalNames.isEmpty()) {
errors
.append("The following classes do not follow the naming criteria for ")
.append("releases or do not have the short codes in ")
.append("their internal fields. Please correct them: ")
.append(this.classesWithIllegalNames)
.append("\n");
}
if (sdkApiMisordered != null) {
errors
.append("The latest released sdk ")
.append(sdkApiMisordered.getKey().getShortCode())
.append(" has a sdkInt greater than the earliest unreleased sdk ")
.append(sdkApiMisordered.getValue().getShortCode())
.append("this implies sdks were released out of order which is highly unlikely.\n");
}
if (!sdkIntCollisions.isEmpty()) {
errors.append(
"The following sdks have different shortCodes, but identical sdkInt " + "versions:\n");
for (Map.Entry entry : sdkIntCollisions) {
errors
.append("Both ")
.append(entry.getKey().getShortCode())
.append(" and ")
.append(entry.getValue().getShortCode())
.append("have the same sdkInt value of ")
.append(entry.getKey().getSdkInt())
.append("\n");
}
}
if (errors.length() > 0) {
errorMessage(
errors
.append("Please check the AndroidReleases defined ")
.append("in ")
.append(AndroidVersions.class.getName())
.append("and ensure they are aligned with the versions of")
.append(" Android.")
.toString(),
null);
}
}
public AndroidRelease computeCurrentSdk(
int reportedVersion, String releaseName, String codename, List activeCodeNames) {
AndroidRelease current = null;
// Special case "REL", which means the build is not a pre-release build.
if (Objects.equals(codename, "REL")) {
// the first letter of the code name equal to the release number.
current = sdkIntToAllReleases.get(reportedVersion);
if (current != null && !current.isReleased()) {
errorMessage(
"The current sdk "
+ current.getShortCode()
+ " has been released. Please update the contents of "
+ AndroidVersions.class.getName()
+ " to mark sdk "
+ current.getShortCode()
+ " as released.",
null);
}
} else {
// Get known active code name letters
List activeCodenameLetter = new ArrayList<>();
for (String name : activeCodeNames) {
activeCodenameLetter.add(name.toUpperCase(Locale.getDefault()).substring(0, 1));
}
// If the process is operating with a code name.
if (codename != null) {
StringBuilder detectedProblems = new StringBuilder();
// This is safe for minor releases ( X.1 ) as long as they have added an entry
// corresponding to the sdk of that release and the prior major release is marked as
// "released" on its entry in this file. If not this class will fail to initialize.
// The assumption is that only one of the major or minor version of a code name
// is under development and unreleased at any give time (S or Sv2).
String foundCode = codename.toUpperCase(Locale.getDefault()).substring(0, 1);
int loc = activeCodenameLetter.indexOf(foundCode);
if (loc == -1) {
detectedProblems
.append("The current codename's (")
.append(codename)
.append(") first letter (")
.append(foundCode)
.append(") is not in the list of active code's first letters: ")
.append(activeCodenameLetter)
.append("\n");
} else {
// attempt to find assume the fullname is the "shortCode", aka "Sv2", "OMR1".
current = shortCodeToAllReleases.get(codename);
// else, assume the fullname is the first letter is correct.
if (current == null) {
current = shortCodeToAllReleases.get(String.valueOf(foundCode));
}
}
if (current == null) {
detectedProblems
.append("No known release is associated with the shortCode of \"")
.append(foundCode)
.append("\" or \"")
.append(codename)
.append("\"\n");
} else if (current.isReleased()) {
detectedProblems
.append("The current sdk ")
.append(current.getShortCode())
.append(" has been been marked as released. Please update the ")
.append("contents of current sdk jar to the released version.\n");
}
if (detectedProblems.length() > 0) {
errorMessage(detectedProblems.toString(), null);
}
if (current == null) { // only possible in warning mode
current =
new AndroidUnreleased() {
@Override
public int getSdkInt() {
return 10000; // the super large unknown sdk value.
}
@Override
public String getShortCode() {
return codename.toUpperCase(Locale.getDefault()).substring(0, 1);
}
@Override
public String getVersion() {
return "";
}
};
}
}
}
return current;
}
}
/**
* Reads all AndroidReleases in this class and populates SdkInformation, checking for sanity in
* the shortCode, sdkInt, and release information.
*
* All errors are stored and can be reported at once by asking the SdkInformation to throw a
* IllegalStateException after it has been populated.
*/
static SdkInformation gatherStaticSdkInformationFromThisClass() {
List allReleases = new ArrayList<>();
List> classesWithIllegalNames = new ArrayList<>();
for (Class> clazz : AndroidVersions.class.getClasses()) {
if (AndroidRelease.class.isAssignableFrom(clazz)
&& !clazz.isInterface()
&& !Modifier.isAbstract(clazz.getModifiers())
&& clazz != Unbound.class) {
try {
AndroidRelease rel = (AndroidRelease) clazz.getDeclaredConstructor().newInstance();
allReleases.add(rel);
// inspect field name - as this is our only chance to inspect it.
if (!rel.getClass().getSimpleName().equals(rel.getShortCode())) {
classesWithIllegalNames.add(rel.getClass());
}
} catch (NoSuchMethodException
| InstantiationException
| IllegalArgumentException
| IllegalAccessException
| InvocationTargetException ex) {
errorMessage(
"Classes "
+ clazz.getName()
+ "should be accessible via "
+ AndroidVersions.class.getCanonicalName()
+ " and have a default public no-op constructor ",
ex);
}
}
}
Collections.sort(allReleases, AndroidRelease::compareTo);
SdkInformation sdkInformation = new SdkInformation(allReleases, classesWithIllegalNames);
sdkInformation.handleStaticErrors();
return sdkInformation;
}
static AndroidRelease computeReleaseVersion(JarFile jarFile) throws IOException {
ZipEntry buildProp = jarFile.getEntry("build.prop");
Properties buildProps = new Properties();
buildProps.load(jarFile.getInputStream(buildProp));
return computeCurrentSdkFromBuildProps(buildProps);
}
static AndroidRelease computeCurrentSdkFromBuildProps(Properties buildProps) {
// 33, 34, 35 ....
String sdkVersionString = buildProps.getProperty("ro.build.version.sdk");
int sdk = sdkVersionString == null ? 0 : Integer.parseInt(sdkVersionString);
// "REL"
String release = buildProps.getProperty("ro.build.version.release");
// "Tiramasu", "UpsideDownCake"
String codename = buildProps.getProperty("ro.build.version.codename");
// "Tiramasu,UpsideDownCake", "UpsideDownCake", "REL"
String codenames = buildProps.getProperty("ro.build.version.all_codenames");
String[] allCodeNames = codenames == null ? new String[0] : codenames.split(",");
String[] activeCodeNames =
allCodeNames.length > 0 && allCodeNames[0].equals("REL") ? new String[0] : allCodeNames;
return information.computeCurrentSdk(sdk, release, codename, asList(activeCodeNames));
}
private static final SdkInformation information;
private static final void errorMessage(String errorMessage, @Nullable Exception ex) {
if (warnOnly) {
System.err.println(errorMessage);
} else {
throw new IllegalStateException(errorMessage, ex);
}
}
static {
// We shouldn't break in annotation processors, only test runs.
String cmd = System.getProperty("sun.java.command");
// We appear to be in an annotation processor, so only warn users.
if (cmd.contains("-Aorg.robolectric.annotation.processing.")) {
System.err.println(
"Robolectric's AndroidVersions is running in warning mode,"
+ " no errors will be thrown.");
warnOnly = true;
} else {
warnOnly = false;
}
AndroidRelease currentRelease = null;
information = gatherStaticSdkInformationFromThisClass();
try {
InputStream is = AndroidVersions.class.getClassLoader().getResourceAsStream("build.prop");
if (is != null) {
Properties buildProps = new Properties();
buildProps.load(is);
currentRelease = computeCurrentSdkFromBuildProps(buildProps);
}
} catch (IOException ioe) {
// No op, this class should be usable outside of a Robolectric sandbox.
}
CURRENT = currentRelease;
}
}