blob: 07547c75a230f869bbf49e773d98f984644171d4 [file] [log] [blame]
/*
* Copyright (C) 2014 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 androidx.core.content.res;
import static android.os.Build.VERSION.SDK_INT;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.content.res.Resources.Theme;
import android.content.res.XmlResourceParser;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.util.TypedValue;
import androidx.annotation.AnyRes;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorRes;
import androidx.annotation.DimenRes;
import androidx.annotation.DoNotInline;
import androidx.annotation.DrawableRes;
import androidx.annotation.FontRes;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.content.res.FontResourcesParserCompat.FamilyResourceEntry;
import androidx.core.graphics.TypefaceCompat;
import androidx.core.provider.FontsContractCompat.FontRequestCallback;
import androidx.core.provider.FontsContractCompat.FontRequestCallback.FontRequestFailReason;
import androidx.core.util.ObjectsCompat;
import androidx.core.util.Preconditions;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Iterator;
import java.util.WeakHashMap;
/**
* Helper for accessing features in {@link Resources}.
*/
public final class ResourcesCompat {
private static final String TAG = "ResourcesCompat";
private static final ThreadLocal<TypedValue> sTempTypedValue = new ThreadLocal<>();
@GuardedBy("sColorStateCacheLock")
private static final WeakHashMap<ColorStateListCacheKey, SparseArray<ColorStateListCacheEntry>>
sColorStateCaches = new WeakHashMap<>(0);
private static final Object sColorStateCacheLock = new Object();
/**
* The {@code null} resource ID. This denotes an invalid resource ID that is returned by the
* system when a resource is not found or the value is set to {@code @null} in XML.
*/
@AnyRes
public static final int ID_NULL = 0;
/**
* Clears cached values associated with the specified {@link Theme}.
* <p>
* This method allows developers to work around issues related to stale cached resources that
* may occur on API level 32 and below following a call to
* {@link Theme#applyStyle(int, boolean)}. If you are not explicitly calling {@code
* applyStyle} in your code, you probably do not need to call this method.
* <p>
* Starting in Android T, the Theme class correctly implements {@link Theme#hashCode()} and
* cached values will be appropriately cleared when the contents of a Theme object change.
*
* @param theme the theme for which associated values should be cleared from the resource cache
*/
public static void clearCachesForTheme(@NonNull Theme theme) {
synchronized (sColorStateCacheLock) {
Iterator<ColorStateListCacheKey> keys = sColorStateCaches.keySet().iterator();
while (keys.hasNext()) {
ColorStateListCacheKey key = keys.next();
if (key != null && theme.equals(key.mTheme)) {
keys.remove();
}
}
}
}
/**
* Return a drawable object associated with a particular resource ID and
* styled for the specified theme. Various types of objects will be
* returned depending on the underlying resource -- for example, a solid
* color, PNG image, scalable image, etc.
* <p>
* Prior to API level 21, the theme will not be applied and this method
* simply calls through to {@link Resources#getDrawable(int)}.
*
* @param id The desired resource identifier, as generated by the aapt
* tool. This integer encodes the package, type, and resource
* entry. The value 0 is an invalid identifier.
* @param theme The theme used to style the drawable attributes, may be
* {@code null}.
* @return Drawable An object that can be used to draw this resource.
* @throws NotFoundException Throws NotFoundException if the given ID does
* not exist.
*/
@Nullable
@SuppressWarnings("deprecation")
public static Drawable getDrawable(@NonNull Resources res, @DrawableRes int id,
@Nullable Theme theme) throws NotFoundException {
if (SDK_INT >= 21) {
return Api21Impl.getDrawable(res, id, theme);
} else {
return res.getDrawable(id);
}
}
/**
* Return a drawable object associated with a particular resource ID for
* the given screen density in DPI and styled for the specified theme.
* <p>
* Prior to API level 15, the theme and density will not be applied and
* this method simply calls through to {@link Resources#getDrawable(int)}.
* <p>
* Prior to API level 21, the theme will not be applied and this method
* calls through to Resources#getDrawableForDensity(int, int).
*
* @param id The desired resource identifier, as generated by the aapt
* tool. This integer encodes the package, type, and resource
* entry. The value 0 is an invalid identifier.
* @param density The desired screen density indicated by the resource as
* found in {@link DisplayMetrics}.
* @param theme The theme used to style the drawable attributes, may be
* {@code null}.
* @return Drawable An object that can be used to draw this resource.
* @throws NotFoundException Throws NotFoundException if the given ID does
* not exist.
*/
@Nullable
@SuppressWarnings("deprecation")
public static Drawable getDrawableForDensity(@NonNull Resources res, @DrawableRes int id,
int density, @Nullable Theme theme) throws NotFoundException {
if (SDK_INT >= 21) {
return Api21Impl.getDrawableForDensity(res, id, density, theme);
} else if (SDK_INT >= 15) {
return Api15Impl.getDrawableForDensity(res, id, density);
} else {
return res.getDrawable(id);
}
}
/**
* Returns a themed color integer associated with a particular resource ID.
* If the resource holds a complex {@link ColorStateList}, then the default
* color from the set is returned.
* <p>
* Prior to API level 23, the theme will not be applied and this method
* calls through to {@link Resources#getColor(int)}.
*
* @param id The desired resource identifier, as generated by the aapt
* tool. This integer encodes the package, type, and resource
* entry. The value 0 is an invalid identifier.
* @param theme The theme used to style the color attributes, may be
* {@code null}.
* @return A single color value in the form {@code 0xAARRGGBB}.
* @throws NotFoundException Throws NotFoundException if the given ID does
* not exist.
*/
@ColorInt
@SuppressWarnings("deprecation")
public static int getColor(@NonNull Resources res, @ColorRes int id, @Nullable Theme theme)
throws NotFoundException {
if (SDK_INT >= 23) {
return ResourcesCompat.Api23Impl.getColor(res, id, theme);
} else {
return res.getColor(id);
}
}
/**
* Returns a themed color state list associated with a particular resource
* ID. The resource may contain either a single raw color value or a
* complex {@link ColorStateList} holding multiple possible colors.
*
* @param id The desired resource identifier of a {@link ColorStateList},
* as generated by the aapt tool. This integer encodes the
* package, type, and resource entry. The value 0 is an invalid
* identifier.
* @param theme The theme used to style the color attributes, may be
* {@code null}.
* @return A themed ColorStateList object containing either a single solid
* color or multiple colors that can be selected based on a state.
* @throws NotFoundException Throws NotFoundException if the given ID does
* not exist.
*/
@Nullable
@SuppressWarnings("deprecation")
public static ColorStateList getColorStateList(@NonNull Resources res, @ColorRes int id,
@Nullable Theme theme) throws NotFoundException {
// We explicitly do not attempt to use the platform Resources impl on S+
// in case the CSL is using only app:lStar
// First, try and handle the inflation ourselves
ColorStateListCacheKey key = new ColorStateListCacheKey(res, theme);
ColorStateList csl = getCachedColorStateList(key, id);
if (csl != null) {
return csl;
}
// Cache miss, so try and inflate it ourselves
csl = inflateColorStateList(res, id, theme);
if (csl != null) {
// If we inflated it, add it to the cache and return
addColorStateListToCache(key, id, csl, theme);
return csl;
}
// If we reach here then we couldn't inflate it, so let the framework handle it
if (SDK_INT >= 23) {
return ResourcesCompat.Api23Impl.getColorStateList(res, id, theme);
} else {
return res.getColorStateList(id);
}
}
/**
* Inflates a {@link ColorStateList} from resources, honouring theme attributes.
*/
@Nullable
private static ColorStateList inflateColorStateList(Resources resources, int resId,
@Nullable Theme theme) {
if (isColorInt(resources, resId)) {
// The resource is a color int, we can't handle it so return null
return null;
}
final XmlPullParser xml = resources.getXml(resId);
try {
return ColorStateListInflaterCompat.createFromXml(resources, xml, theme);
} catch (Exception e) {
Log.w(TAG, "Failed to inflate ColorStateList, leaving it to the framework", e);
}
return null;
}
@Nullable
private static ColorStateList getCachedColorStateList(@NonNull ColorStateListCacheKey key,
@ColorRes int resId) {
synchronized (sColorStateCacheLock) {
final SparseArray<ColorStateListCacheEntry> entries = sColorStateCaches.get(key);
if (entries != null && entries.size() > 0) {
final ColorStateListCacheEntry entry = entries.get(resId);
if (entry != null) {
if (entry.mConfiguration.equals(key.mResources.getConfiguration())
&& entry.mThemeHash == key.mTheme.hashCode()) {
// If the current configuration matches the entry's, we can use it
return entry.mValue;
} else {
// Otherwise we'll remove the entry
entries.remove(resId);
}
}
}
}
return null;
}
private static void addColorStateListToCache(@NonNull ColorStateListCacheKey key,
@ColorRes int resId,
@NonNull ColorStateList value,
@Nullable Theme theme) {
synchronized (sColorStateCacheLock) {
SparseArray<ColorStateListCacheEntry> entries = sColorStateCaches.get(key);
if (entries == null) {
entries = new SparseArray<>();
sColorStateCaches.put(key, entries);
}
entries.append(resId, new ColorStateListCacheEntry(value,
key.mResources.getConfiguration(), theme));
}
}
private static boolean isColorInt(@NonNull Resources resources, @ColorRes int resId) {
final TypedValue value = getTypedValue();
resources.getValue(resId, value, true);
return value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT;
}
@NonNull
private static TypedValue getTypedValue() {
TypedValue tv = sTempTypedValue.get();
if (tv == null) {
tv = new TypedValue();
sTempTypedValue.set(tv);
}
return tv;
}
private static final class ColorStateListCacheKey {
final Resources mResources;
final Theme mTheme;
ColorStateListCacheKey(@NonNull Resources resources, @Nullable Theme theme) {
mResources = resources;
mTheme = theme;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ColorStateListCacheKey that = (ColorStateListCacheKey) o;
return mResources.equals(that.mResources)
&& ObjectsCompat.equals(mTheme, that.mTheme);
}
@Override
public int hashCode() {
return ObjectsCompat.hash(mResources, mTheme);
}
}
private static class ColorStateListCacheEntry {
final ColorStateList mValue;
final Configuration mConfiguration;
final int mThemeHash;
ColorStateListCacheEntry(@NonNull ColorStateList value,
@NonNull Configuration configuration,
@Nullable Theme theme) {
mValue = value;
mConfiguration = configuration;
mThemeHash = theme == null ? 0 : theme.hashCode();
}
}
/**
* Retrieve a floating-point value for a particular resource ID.
*
* @param id The desired resource identifier, as generated by the aapt
* tool. This integer encodes the package, type, and resource
* entry. The value 0 is an invalid identifier.
* @return Returns the floating-point value contained in the resource.
* @throws NotFoundException Throws NotFoundException if the given ID does
* not exist or is not a floating-point value.
*/
public static float getFloat(@NonNull Resources res, @DimenRes int id) {
if (SDK_INT >= 29) {
return Api29Impl.getFloat(res, id);
}
TypedValue value = getTypedValue();
res.getValue(id, value, true);
if (value.type == TypedValue.TYPE_FLOAT) {
return value.getFloat();
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
}
/**
* Returns a font Typeface associated with a particular resource ID.
* <p>
* This method will block the calling thread to retrieve the requested font, including if it
* is from a font provider. If you wish to not have this behavior, use
* {@link #getFont(Context, int, FontCallback, Handler)} instead.
* <p>
* Prior to API level 23, font resources with more than one font in a family will only load the
* font closest to a regular weight typeface.
*
* @param context A context to retrieve the Resources from.
* @param id The desired resource identifier of a {@link Typeface},
* as generated by the aapt tool. This integer encodes the
* package, type, and resource entry. The value 0 is an invalid
* identifier.
* @return A font Typeface object.
* @throws NotFoundException Throws NotFoundException if the given ID does not exist.
* @see #getFont(Context, int, FontCallback, Handler)
*/
@Nullable
public static Typeface getFont(@NonNull Context context, @FontRes int id)
throws NotFoundException {
if (context.isRestricted()) {
return null;
}
return loadFont(context, id, new TypedValue(), Typeface.NORMAL, null /* callback */,
null /* handler */, false /* isXmlRequest */, false /* isCachedOnly */);
}
/**
* Returns a cached font Typeface associated with a particular resource ID.
* <p>
* This method returns non-null Typeface if the requested font is already fetched. Otherwise
* immediately returns null without requesting to font provider.
* <p>
* Prior to API level 23, font resources with more than one font in a family will only load the
* font closest to a regular weight typeface.
*
* @param context A context to retrieve the Resources from.
* @param id The desired resource identifier of a {@link Typeface},
* as generated by the aapt tool. This integer encodes the
* package, type, and resource entry. The value 0 is an invalid
* identifier.
* @return A font Typeface object.
* @throws NotFoundException Throws NotFoundException if the given ID does not exist.
* @see #getFont(Context, int, FontCallback, Handler)
*/
@Nullable
public static Typeface getCachedFont(@NonNull Context context, @FontRes int id)
throws NotFoundException {
if (context.isRestricted()) {
return null;
}
return loadFont(context, id, new TypedValue(), Typeface.NORMAL, null /* callback */,
null /* handler */, false /* isXmlRequest */, true);
}
/**
* Interface used to receive asynchronous font fetching events.
*/
public abstract static class FontCallback {
/**
* Called when an asynchronous font was finished loading.
*
* @param typeface The font that was loaded.
*/
public abstract void onFontRetrieved(@NonNull Typeface typeface);
/**
* Called when an asynchronous font failed to load.
*
* @param reason The reason the font failed to load. One of
* {@link FontRequestFailReason#FAIL_REASON_PROVIDER_NOT_FOUND},
* {@link FontRequestFailReason#FAIL_REASON_WRONG_CERTIFICATES},
* {@link FontRequestFailReason#FAIL_REASON_FONT_LOAD_ERROR},
* {@link FontRequestFailReason#FAIL_REASON_SECURITY_VIOLATION},
* {@link FontRequestFailReason#FAIL_REASON_FONT_NOT_FOUND},
* {@link FontRequestFailReason#FAIL_REASON_FONT_UNAVAILABLE} or
* {@link FontRequestFailReason#FAIL_REASON_MALFORMED_QUERY}.
*/
public abstract void onFontRetrievalFailed(@FontRequestFailReason int reason);
/**
* Call {@link #onFontRetrieved(Typeface)} on the handler given, or the Ui Thread if it is
* null.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final void callbackSuccessAsync(final @NonNull Typeface typeface,
@Nullable Handler handler) {
getHandler(handler).post(() -> onFontRetrieved(typeface));
}
/**
* Call {@link #onFontRetrievalFailed(int)} on the handler given, or the Ui Thread if it is
* null.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final void callbackFailAsync(
@FontRequestFailReason final int reason, @Nullable Handler handler) {
getHandler(handler).post(() -> onFontRetrievalFailed(reason));
}
/** @hide */
@RestrictTo(LIBRARY)
@NonNull
public static Handler getHandler(@Nullable Handler handler) {
return handler == null ? new Handler(Looper.getMainLooper()) : handler;
}
}
/**
* Returns a font Typeface associated with a particular resource ID asynchronously.
* <p>
* Prior to API level 23, font resources with more than one font in a family will only load the
* font closest to a regular weight typeface.
* </p>
*
* @param context A context to retrieve the Resources from.
* @param id The desired resource identifier of a {@link Typeface}, as generated by
* the aapt
* tool. This integer encodes the package, type, and resource entry. The
* value 0 is an
* invalid identifier.
* @param fontCallback A callback to receive async fetching of this font. The callback will be
* triggered on the UI thread.
* @param handler A handler for the thread the callback should be called on. If null, the
* callback will be called on the UI thread.
* @throws NotFoundException Throws NotFoundException if the given ID does not exist.
*/
public static void getFont(@NonNull Context context, @FontRes int id,
@NonNull FontCallback fontCallback, @Nullable Handler handler)
throws NotFoundException {
Preconditions.checkNotNull(fontCallback);
if (context.isRestricted()) {
fontCallback.callbackFailAsync(
FontRequestCallback.FAIL_REASON_SECURITY_VIOLATION, handler);
return;
}
loadFont(context, id, new TypedValue(), Typeface.NORMAL, fontCallback, handler,
false /* isXmlRequest */, false /* isCacheOnly */);
}
/**
* Used by TintTypedArray.
*
* @hide
*/
@Nullable
@RestrictTo(LIBRARY_GROUP_PREFIX)
public static Typeface getFont(@NonNull Context context, @FontRes int id,
@NonNull TypedValue value, int style, @Nullable FontCallback fontCallback)
throws NotFoundException {
if (context.isRestricted()) {
return null;
}
return loadFont(context, id, value, style, fontCallback, null /* handler */,
true /* isXmlRequest */, false /* isCacheOnly */);
}
/**
* @param context The Context to get Resources from
* @param id The Resource id to load
* @param value A TypedValue to use in the fetching
* @param style The font style to load
* @param fontCallback A callback to trigger when the font is fetched or an
* error occurs
* @param handler A handler to the thread the callback should be called on
* @param isRequestFromLayoutInflator Whether this request originated from XML. This is used to
* determine if we use or ignore the
* fontProviderFetchStrategy attribute in
* font provider XML fonts.
* @return The font as a Typeface
* @throws NotFoundException if the resource ID could not be retrieved
*/
private static Typeface loadFont(@NonNull Context context, int id, @NonNull TypedValue value,
int style, @Nullable FontCallback fontCallback, @Nullable Handler handler,
boolean isRequestFromLayoutInflator, boolean isCachedOnly) {
final Resources resources = context.getResources();
resources.getValue(id, value, true);
Typeface typeface = loadFont(context, resources, value, id, style, fontCallback, handler,
isRequestFromLayoutInflator, isCachedOnly);
if (typeface == null && fontCallback == null && !isCachedOnly) {
throw new NotFoundException("Font resource ID #0x"
+ Integer.toHexString(id) + " could not be retrieved.");
}
return typeface;
}
/**
* Load the given font. This method will always return null for asynchronous requests, which
* provide a fontCallback, as there is no immediate result. When the callback is not provided,
* the request is treated as synchronous and fails if async loading is required.
*
* @param context The Context to get Resources from
* @param id The Resource id to load
* @param value A TypedValue to use in the fetching
* @param style The font style to load
* @param fontCallback A callback to trigger when the font is fetched or an
* error occurs
* @param handler A handler to the thread the callback should be called on
* @param isRequestFromLayoutInflator Whether this request originated from XML. This is used to
* determine if we use or ignore the
* fontProviderFetchStrategy attribute in
* font provider XML fonts.
*/
private static Typeface loadFont(
@NonNull Context context, Resources wrapper, @NonNull TypedValue value, int id,
int style, @Nullable FontCallback fontCallback, @Nullable Handler handler,
boolean isRequestFromLayoutInflator, boolean isCachedOnly) {
if (value.string == null) {
throw new NotFoundException("Resource \"" + wrapper.getResourceName(id) + "\" ("
+ Integer.toHexString(id) + ") is not a Font: " + value);
}
final String file = value.string.toString();
if (!file.startsWith("res/")) {
// Early exit if the specified string is unlikely to be a resource path.
if (fontCallback != null) {
fontCallback.callbackFailAsync(
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler);
}
return null;
}
Typeface typeface = TypefaceCompat.findFromCache(wrapper, id, style);
if (typeface != null) {
if (fontCallback != null) {
fontCallback.callbackSuccessAsync(typeface, handler);
}
return typeface;
} else if (isCachedOnly) {
return null;
}
try {
if (file.toLowerCase().endsWith(".xml")) {
final XmlResourceParser rp = wrapper.getXml(id);
final FamilyResourceEntry familyEntry =
FontResourcesParserCompat.parse(rp, wrapper);
if (familyEntry == null) {
Log.e(TAG, "Failed to find font-family tag");
if (fontCallback != null) {
fontCallback.callbackFailAsync(
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler);
}
return null;
}
return TypefaceCompat.createFromResourcesFamilyXml(context, familyEntry, wrapper,
id, style, fontCallback, handler, isRequestFromLayoutInflator);
}
typeface = TypefaceCompat.createFromResourcesFontFile(
context, wrapper, id, file, style);
if (fontCallback != null) {
if (typeface != null) {
fontCallback.callbackSuccessAsync(typeface, handler);
} else {
fontCallback.callbackFailAsync(
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler);
}
}
return typeface;
} catch (XmlPullParserException e) {
Log.e(TAG, "Failed to parse xml resource " + file, e);
} catch (IOException e) {
Log.e(TAG, "Failed to read xml resource " + file, e);
}
if (fontCallback != null) {
fontCallback.callbackFailAsync(
FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler);
}
return null;
}
@RequiresApi(29)
static class Api29Impl {
private Api29Impl() {
// This class is not instantiable.
}
@DoNotInline
static float getFloat(@NonNull Resources res, @DimenRes int id) {
return res.getFloat(id);
}
}
@RequiresApi(23)
static class Api23Impl {
private Api23Impl() {
// This class is not instantiable.
}
@DoNotInline
@NonNull
static ColorStateList getColorStateList(@NonNull Resources res, @ColorRes int id,
@Nullable Theme theme) {
return res.getColorStateList(id, theme);
}
@DoNotInline
static int getColor(Resources resources, int id, Theme theme) {
return resources.getColor(id, theme);
}
}
@RequiresApi(21)
static class Api21Impl {
private Api21Impl() {
// This class is not instantiable.
}
@DoNotInline
static Drawable getDrawable(Resources resources, int id, Theme theme) {
return resources.getDrawable(id, theme);
}
@DoNotInline
static Drawable getDrawableForDensity(Resources resources, int id, int density,
Theme theme) {
return resources.getDrawableForDensity(id, density, theme);
}
}
@RequiresApi(15)
static class Api15Impl {
private Api15Impl() {
// This class is not instantiable.
}
@DoNotInline
static Drawable getDrawableForDensity(Resources resources, int id, int density) {
return resources.getDrawableForDensity(id, density);
}
}
private ResourcesCompat() {
}
/**
* Provides backward-compatible implementations for new {@link Theme} APIs.
*/
public static final class ThemeCompat {
private ThemeCompat() {
}
/**
* Rebases the theme against the parent Resource object's current configuration by
* re-applying the styles passed to {@link Theme#applyStyle(int, boolean)}.
* <p>
* Compatibility behavior:
* <ul>
* <li>API 29 and above, this method matches platform behavior.
* <li>API 23 through 28, this method attempts to match platform behavior by calling into
* hidden platform APIs, but is not guaranteed to succeed.
* <li>API 22 and earlier, this method does nothing.
* </ul>
*
* @param theme the theme to rebase
*/
public static void rebase(@NonNull Theme theme) {
if (SDK_INT >= 29) {
Api29Impl.rebase(theme);
} else if (SDK_INT >= 23) {
Api23Impl.rebase(theme);
}
}
@RequiresApi(29)
static class Api29Impl {
private Api29Impl() {
// This class is not instantiable.
}
@DoNotInline
static void rebase(@NonNull Theme theme) {
theme.rebase();
}
}
@RequiresApi(23)
static class Api23Impl {
private Api23Impl() {
// This class is not instantiable.
}
private static final Object sRebaseMethodLock = new Object();
private static Method sRebaseMethod;
private static boolean sRebaseMethodFetched;
@SuppressLint("BanUncheckedReflection") // @RequiresApiRange(min=23, max=29)
static void rebase(@NonNull Theme theme) {
synchronized (sRebaseMethodLock) {
if (!sRebaseMethodFetched) {
try {
sRebaseMethod = Theme.class.getDeclaredMethod("rebase");
sRebaseMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
Log.i(TAG, "Failed to retrieve rebase() method", e);
}
sRebaseMethodFetched = true;
}
if (sRebaseMethod != null) {
try {
sRebaseMethod.invoke(theme);
} catch (IllegalAccessException | InvocationTargetException e) {
Log.i(TAG, "Failed to invoke rebase() method via reflection", e);
sRebaseMethod = null;
}
}
}
}
}
}
}