blob: 49dffbc1905d34a4b56db546b6e496bde2967290 [file] [log] [blame]
/*
* Copyright (C) 2009 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.sdklib;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.sdklib.repository.PkgProps;
import java.util.Properties;
import java.util.regex.Pattern;
/**
* Represents the version of a target or device.
* <p/>
* A version is defined by an API level and an optional code name.
* <ul><li>Release versions of the Android platform are identified by their API level (integer),
* (technically the code name for release version is "REL" but this class will return
* <code>null<code> instead.)</li>
* <li>Preview versions of the platform are identified by a code name. Their API level
* is usually set to the value of the previous platform.</li></ul>
* <p/>
* While this class contains both values, its goal is to abstract them, so that code comparing 2+
* versions doesn't have to deal with the logic of handle both values.
* <p/>
* There are some cases where ones may want to access the values directly. This can be done
* with {@link #getApiLevel()} and {@link #getCodename()}.
* <p/>
* For generic UI display of the API version, {@link #getApiString()} is to be used.
*/
public final class AndroidVersion implements Comparable<AndroidVersion> {
private final int mApiLevel;
private final String mCodename;
/** The default AndroidVersion for minSdkVersion and targetSdkVersion if not specified */
public static final AndroidVersion DEFAULT = new AndroidVersion(1, null);
/**
* Thrown when an {@link AndroidVersion} object could not be created.
* @see AndroidVersion#AndroidVersion(Properties)
*/
public static final class AndroidVersionException extends Exception {
private static final long serialVersionUID = 1L;
AndroidVersionException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Creates an {@link AndroidVersion} with the given api level and codename.
* Codename should be null for a release version, otherwise it's a preview codename.
*/
public AndroidVersion(int apiLevel, @Nullable String codename) {
mApiLevel = apiLevel;
mCodename = sanitizeCodename(codename);
}
/**
* Creates an {@link AndroidVersion} from {@link Properties}, with default values if the
* {@link Properties} object doesn't contain the expected values.
* <p/>The {@link Properties} is expected to have been filled with
* {@link #saveProperties(Properties)}.
*/
public AndroidVersion(@Nullable Properties properties,
int defaultApiLevel,
@Nullable String defaultCodeName) {
if (properties == null) {
mApiLevel = defaultApiLevel;
mCodename = sanitizeCodename(defaultCodeName);
} else {
mApiLevel = Integer.parseInt(properties.getProperty(PkgProps.VERSION_API_LEVEL,
Integer.toString(defaultApiLevel)));
mCodename = sanitizeCodename(
properties.getProperty(PkgProps.VERSION_CODENAME, defaultCodeName));
}
}
/**
* Creates an {@link AndroidVersion} from {@link Properties}. The properties must contain
* android version information, or an exception will be thrown.
* @throws AndroidVersionException if no Android version information have been found
*
* @see #saveProperties(Properties)
*/
public AndroidVersion(@NonNull Properties properties) throws AndroidVersionException {
Exception error = null;
String apiLevel = properties.getProperty(PkgProps.VERSION_API_LEVEL, null/*defaultValue*/);
if (apiLevel != null) {
try {
mApiLevel = Integer.parseInt(apiLevel);
mCodename = sanitizeCodename(properties.getProperty(PkgProps.VERSION_CODENAME,
null/*defaultValue*/));
return;
} catch (NumberFormatException e) {
error = e;
}
}
// reaching here means the Properties object did not contain the apiLevel which is required.
throw new AndroidVersionException(PkgProps.VERSION_API_LEVEL + " not found!", error);
}
/**
* Creates an {@link AndroidVersion} from a string that may be an integer API
* level or a string codename.
* <p/>
* <Em>Important</em>: An important limitation of this method is that cannot possible
* recreate the API level integer from a pure string codename. This is only OK to use
* if the caller can guarantee that only {@link #getApiString()} will be used later.
* Wrong things will happen if the caller then tries to resolve the numeric
* {@link #getApiLevel()}.
*
* @param apiOrCodename A non-null API integer or a codename in its "ALL_CAPS" format.
* "REL" is notable not a valid codename.
* @throws AndroidVersionException if the input isn't a pure integer or doesn't look like
* a valid string codename.
*/
public AndroidVersion(@NonNull String apiOrCodename) throws AndroidVersionException {
int apiLevel = 0;
String codename = null;
try {
apiLevel = Integer.parseInt(apiOrCodename);
} catch (NumberFormatException ignore) {
// We don't know the API level. Android platform codenames are all caps.
// REL is a release-reserved keyword which we can use here.
if (!SdkConstants.CODENAME_RELEASE.equals(apiOrCodename)) {
if (Pattern.matches("[A-Z_]+", apiOrCodename)) {
codename = apiOrCodename;
}
}
}
mApiLevel = apiLevel;
mCodename = sanitizeCodename(codename);
if (mApiLevel <= 0 && codename == null) {
throw new AndroidVersionException(
"Invalid android API or codename " + apiOrCodename, //$NON-NLS-1$
null);
}
}
public void saveProperties(@NonNull Properties props) {
props.setProperty(PkgProps.VERSION_API_LEVEL, Integer.toString(mApiLevel));
if (mCodename != null) {
props.setProperty(PkgProps.VERSION_CODENAME, mCodename);
}
}
/**
* Returns the api level as an integer.
* <p/>For target that are in preview mode, this can be superseded by
* {@link #getCodename()}.
* <p/>To display the API level in the UI, use {@link #getApiString()}, which will use the
* codename if applicable.
* @see #getCodename()
* @see #getApiString()
*/
public int getApiLevel() {
return mApiLevel;
}
/**
* Returns the API level as an integer. If this is a preview platform, it
* will return the expected final version of the API rather than the current API
* level. This is the "feature level" as opposed to the "release level" returned by
* {@link #getApiLevel()} in the sense that it is useful when you want
* to check the presence of a given feature from an API, and we consider the feature
* present in preview platforms as well.
*
* @return the API level of this version, +1 for preview platforms
*/
public int getFeatureLevel() {
//noinspection VariableNotUsedInsideIf
return mCodename != null ? mApiLevel + 1 : mApiLevel;
}
/**
* Returns the version code name if applicable, null otherwise.
* <p/>If the codename is non null, then the API level should be ignored, and this should be
* used as a unique identifier of the target instead.
*/
@Nullable
public String getCodename() {
return mCodename;
}
/**
* Returns a string representing the API level and/or the code name.
*/
@NonNull
public String getApiString() {
if (mCodename != null) {
return mCodename;
}
return Integer.toString(mApiLevel);
}
/**
* Returns whether or not the version is a preview version.
*/
public boolean isPreview() {
return mCodename != null;
}
/**
* Checks whether a device running a version similar to the receiver can run a project compiled
* for the given <var>version</var>.
* <p/>
* Be aware that this is not a perfect test, as other properties could break compatibility
* despite this method returning true. For a more comprehensive test, see
* {@link IAndroidTarget#canRunOn(IAndroidTarget)}.
* <p/>
* Nevertheless, when testing if an application can run on a device (where there is no
* access to the list of optional libraries), this method can give a good indication of whether
* there is a chance the application could run, or if there's a direct incompatibility.
*/
public boolean canRun(@NonNull AndroidVersion appVersion) {
// if the application is compiled for a preview version, the device must be running exactly
// the same.
if (appVersion.mCodename != null) {
return appVersion.mCodename.equals(mCodename);
}
// otherwise, we check the api level (note that a device running a preview version
// will have the api level of the previous platform).
return mApiLevel >= appVersion.mApiLevel;
}
/**
* Returns <code>true</code> if the AndroidVersion is an API level equals to
* <var>apiLevel</var>.
*/
public boolean equals(int apiLevel) {
return mCodename == null && apiLevel == mApiLevel;
}
/**
* Compares the receiver with either an {@link AndroidVersion} object or a {@link String}
* object.
* <p/>If <var>obj</var> is a {@link String}, then the method will first check if it's a string
* representation of a number, in which case it'll compare it to the api level. Otherwise, it'll
* compare it against the code name.
* <p/>For all other type of object give as parameter, this method will return
* <code>false</code>.
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof AndroidVersion) {
AndroidVersion version = (AndroidVersion)obj;
if (mCodename == null) {
return version.mCodename == null &&
mApiLevel == version.mApiLevel;
} else {
return mCodename.equals(version.mCodename) &&
mApiLevel == version.mApiLevel;
}
} else if (obj instanceof String) {
// if we have a code name, this must match.
if (mCodename != null) {
return mCodename.equals(obj);
}
// else we try to convert to a int and compare to the api level
try {
int value = Integer.parseInt((String)obj);
return value == mApiLevel;
} catch (NumberFormatException e) {
// not a number? we'll return false below.
}
}
return false;
}
@Override
public int hashCode() {
if (mCodename != null) {
return mCodename.hashCode();
}
// there may be some collisions between the hashcode of the codename and the api level
// but it's acceptable.
return mApiLevel;
}
/**
* Returns a string with the API Level and optional codename.
* Useful for debugging.
* For display purpose, please use {@link #getApiString()} instead.
*/
@Override
public String toString() {
String s = String.format("API %1$d", mApiLevel); //$NON-NLS-1$
if (isPreview()) {
s += String.format(", %1$s preview", mCodename); //$NON-NLS-1$
}
return s;
}
/**
* Compares this object with the specified object for order. Returns a
* negative integer, zero, or a positive integer as this object is less
* than, equal to, or greater than the specified object.
*
* @param o the Object to be compared.
* @return a negative integer, zero, or a positive integer as this object is
* less than, equal to, or greater than the specified object.
*/
@Override
public int compareTo(@NonNull AndroidVersion o) {
return compareTo(o.mApiLevel, o.mCodename);
}
public int compareTo(int apiLevel, @Nullable String codename) {
if (mCodename == null) {
if (codename == null) {
return mApiLevel - apiLevel;
} else {
if (mApiLevel == apiLevel) {
return -1; // same api level but argument is a preview for next version
}
return mApiLevel - apiLevel;
}
} else {
// 'this' is a preview
if (mApiLevel == apiLevel) {
if (codename == null) {
return +1;
} else {
return mCodename.compareTo(codename); // strange case where the 2 previews
// have different codename?
}
} else {
return mApiLevel - apiLevel;
}
}
}
/**
* Compares this version with the specified API and returns true if this version
* is greater or equal than the requested API -- that is the current version is a
* suitable min-api-level for the argument API.
*/
public boolean isGreaterOrEqualThan(int api) {
return compareTo(api, null /*codename*/) >= 0;
}
/**
* Sanitizes the codename string according to the following rules:
* - A codename should be {@code null} for a release version or it should be a non-empty
* string for an actual preview.
* - In input, spacing is trimmed since it is irrelevant.
* - An empty string or the special codename "REL" means a release version
* and is converted to {@code null}.
*
* @param codename A possible-null codename.
* @return Null for a release version or a non-empty codename.
*/
@Nullable
private static String sanitizeCodename(@Nullable String codename) {
if (codename != null) {
codename = codename.trim();
if (codename.isEmpty() || SdkConstants.CODENAME_RELEASE.equals(codename)) {
codename = null;
}
}
return codename;
}
}