1/* 2 * Copyright (C) 2014 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 17package androidx.core.content.res; 18 19import static android.os.Build.VERSION.SDK_INT; 20 21import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 22 23import android.content.Context; 24import android.content.res.ColorStateList; 25import android.content.res.Resources; 26import android.content.res.Resources.NotFoundException; 27import android.content.res.Resources.Theme; 28import android.content.res.XmlResourceParser; 29import android.graphics.Typeface; 30import android.graphics.drawable.Drawable; 31import android.os.Handler; 32import android.os.Looper; 33import android.util.Log; 34import android.util.TypedValue; 35 36import androidx.annotation.ColorInt; 37import androidx.annotation.ColorRes; 38import androidx.annotation.DrawableRes; 39import androidx.annotation.FontRes; 40import androidx.annotation.NonNull; 41import androidx.annotation.Nullable; 42import androidx.annotation.RestrictTo; 43import androidx.core.content.res.FontResourcesParserCompat.FamilyResourceEntry; 44import androidx.core.graphics.TypefaceCompat; 45import androidx.core.provider.FontsContractCompat.FontRequestCallback; 46import androidx.core.provider.FontsContractCompat.FontRequestCallback.FontRequestFailReason; 47import androidx.core.util.Preconditions; 48 49import org.xmlpull.v1.XmlPullParserException; 50 51import java.io.IOException; 52 53/** 54 * Helper for accessing features in {@link android.content.res.Resources}. 55 */ 56public final class ResourcesCompat { 57 private static final String TAG = "ResourcesCompat"; 58 59 /** 60 * Return a drawable object associated with a particular resource ID and 61 * styled for the specified theme. Various types of objects will be 62 * returned depending on the underlying resource -- for example, a solid 63 * color, PNG image, scalable image, etc. 64 * <p> 65 * Prior to API level 21, the theme will not be applied and this method 66 * simply calls through to {@link Resources#getDrawable(int)}. 67 * 68 * @param id The desired resource identifier, as generated by the aapt 69 * tool. This integer encodes the package, type, and resource 70 * entry. The value 0 is an invalid identifier. 71 * @param theme The theme used to style the drawable attributes, may be 72 * {@code null}. 73 * @return Drawable An object that can be used to draw this resource. 74 * @throws NotFoundException Throws NotFoundException if the given ID does 75 * not exist. 76 */ 77 @Nullable 78 @SuppressWarnings("deprecation") 79 public static Drawable getDrawable(@NonNull Resources res, @DrawableRes int id, 80 @Nullable Theme theme) throws NotFoundException { 81 if (SDK_INT >= 21) { 82 return res.getDrawable(id, theme); 83 } else { 84 return res.getDrawable(id); 85 } 86 } 87 88 89 /** 90 * Return a drawable object associated with a particular resource ID for 91 * the given screen density in DPI and styled for the specified theme. 92 * <p> 93 * Prior to API level 15, the theme and density will not be applied and 94 * this method simply calls through to {@link Resources#getDrawable(int)}. 95 * <p> 96 * Prior to API level 21, the theme will not be applied and this method 97 * calls through to Resources#getDrawableForDensity(int, int). 98 * 99 * @param id The desired resource identifier, as generated by the aapt 100 * tool. This integer encodes the package, type, and resource 101 * entry. The value 0 is an invalid identifier. 102 * @param density The desired screen density indicated by the resource as 103 * found in {@link android.util.DisplayMetrics}. 104 * @param theme The theme used to style the drawable attributes, may be 105 * {@code null}. 106 * @return Drawable An object that can be used to draw this resource. 107 * @throws NotFoundException Throws NotFoundException if the given ID does 108 * not exist. 109 */ 110 @Nullable 111 @SuppressWarnings("deprecation") 112 public static Drawable getDrawableForDensity(@NonNull Resources res, @DrawableRes int id, 113 int density, @Nullable Theme theme) throws NotFoundException { 114 if (SDK_INT >= 21) { 115 return res.getDrawableForDensity(id, density, theme); 116 } else if (SDK_INT >= 15) { 117 return res.getDrawableForDensity(id, density); 118 } else { 119 return res.getDrawable(id); 120 } 121 } 122 123 /** 124 * Returns a themed color integer associated with a particular resource ID. 125 * If the resource holds a complex {@link ColorStateList}, then the default 126 * color from the set is returned. 127 * <p> 128 * Prior to API level 23, the theme will not be applied and this method 129 * calls through to {@link Resources#getColor(int)}. 130 * 131 * @param id The desired resource identifier, as generated by the aapt 132 * tool. This integer encodes the package, type, and resource 133 * entry. The value 0 is an invalid identifier. 134 * @param theme The theme used to style the color attributes, may be 135 * {@code null}. 136 * @return A single color value in the form {@code 0xAARRGGBB}. 137 * @throws NotFoundException Throws NotFoundException if the given ID does 138 * not exist. 139 */ 140 @ColorInt 141 @SuppressWarnings("deprecation") 142 public static int getColor(@NonNull Resources res, @ColorRes int id, @Nullable Theme theme) 143 throws NotFoundException { 144 if (SDK_INT >= 23) { 145 return res.getColor(id, theme); 146 } else { 147 return res.getColor(id); 148 } 149 } 150 151 /** 152 * Returns a themed color state list associated with a particular resource 153 * ID. The resource may contain either a single raw color value or a 154 * complex {@link ColorStateList} holding multiple possible colors. 155 * <p> 156 * Prior to API level 23, the theme will not be applied and this method 157 * calls through to {@link Resources#getColorStateList(int)}. 158 * 159 * @param id The desired resource identifier of a {@link ColorStateList}, 160 * as generated by the aapt tool. This integer encodes the 161 * package, type, and resource entry. The value 0 is an invalid 162 * identifier. 163 * @param theme The theme used to style the color attributes, may be 164 * {@code null}. 165 * @return A themed ColorStateList object containing either a single solid 166 * color or multiple colors that can be selected based on a state. 167 * @throws NotFoundException Throws NotFoundException if the given ID does 168 * not exist. 169 */ 170 @Nullable 171 @SuppressWarnings("deprecation") 172 public static ColorStateList getColorStateList(@NonNull Resources res, @ColorRes int id, 173 @Nullable Theme theme) throws NotFoundException { 174 if (SDK_INT >= 23) { 175 return res.getColorStateList(id, theme); 176 } else { 177 return res.getColorStateList(id); 178 } 179 } 180 181 /** 182 * Returns a font Typeface associated with a particular resource ID. 183 * <p> 184 * This method will block the calling thread to retrieve the requested font, including if it 185 * is from a font provider. If you wish to not have this behavior, use 186 * {@link #getFont(Context, int, FontCallback, Handler)} instead. 187 * <p> 188 * Prior to API level 23, font resources with more than one font in a family will only load the 189 * font closest to a regular weight typeface. 190 * 191 * @param context A context to retrieve the Resources from. 192 * @param id The desired resource identifier of a {@link Typeface}, 193 * as generated by the aapt tool. This integer encodes the 194 * package, type, and resource entry. The value 0 is an invalid 195 * identifier. 196 * @return A font Typeface object. 197 * @throws NotFoundException Throws NotFoundException if the given ID does not exist. 198 * 199 * @see #getFont(Context, int, FontCallback, Handler) 200 */ 201 @Nullable 202 public static Typeface getFont(@NonNull Context context, @FontRes int id) 203 throws NotFoundException { 204 if (context.isRestricted()) { 205 return null; 206 } 207 return loadFont(context, id, new TypedValue(), Typeface.NORMAL, null /* callback */, 208 null /* handler */, false /* isXmlRequest */); 209 } 210 211 /** 212 * Interface used to receive asynchronous font fetching events. 213 */ 214 public abstract static class FontCallback { 215 216 /** 217 * Called when an asynchronous font was finished loading. 218 * 219 * @param typeface The font that was loaded. 220 */ 221 public abstract void onFontRetrieved(@NonNull Typeface typeface); 222 223 /** 224 * Called when an asynchronous font failed to load. 225 * 226 * @param reason The reason the font failed to load. One of 227 * {@link FontRequestFailReason#FAIL_REASON_PROVIDER_NOT_FOUND}, 228 * {@link FontRequestFailReason#FAIL_REASON_WRONG_CERTIFICATES}, 229 * {@link FontRequestFailReason#FAIL_REASON_FONT_LOAD_ERROR}, 230 * {@link FontRequestFailReason#FAIL_REASON_SECURITY_VIOLATION}, 231 * {@link FontRequestFailReason#FAIL_REASON_FONT_NOT_FOUND}, 232 * {@link FontRequestFailReason#FAIL_REASON_FONT_UNAVAILABLE} or 233 * {@link FontRequestFailReason#FAIL_REASON_MALFORMED_QUERY}. 234 */ 235 public abstract void onFontRetrievalFailed(@FontRequestFailReason int reason); 236 237 /** 238 * Call {@link #onFontRetrieved(Typeface)} on the handler given, or the Ui Thread if it is 239 * null. 240 * @hide 241 */ 242 @RestrictTo(LIBRARY_GROUP) 243 public final void callbackSuccessAsync(final Typeface typeface, @Nullable Handler handler) { 244 if (handler == null) { 245 handler = new Handler(Looper.getMainLooper()); 246 } 247 handler.post(new Runnable() { 248 @Override 249 public void run() { 250 onFontRetrieved(typeface); 251 } 252 }); 253 } 254 255 /** 256 * Call {@link #onFontRetrievalFailed(int)} on the handler given, or the Ui Thread if it is 257 * null. 258 * @hide 259 */ 260 @RestrictTo(LIBRARY_GROUP) 261 public final void callbackFailAsync( 262 @FontRequestFailReason final int reason, @Nullable Handler handler) { 263 if (handler == null) { 264 handler = new Handler(Looper.getMainLooper()); 265 } 266 handler.post(new Runnable() { 267 @Override 268 public void run() { 269 onFontRetrievalFailed(reason); 270 } 271 }); 272 } 273 } 274 275 /** 276 * Returns a font Typeface associated with a particular resource ID asynchronously. 277 * <p> 278 * Prior to API level 23, font resources with more than one font in a family will only load the 279 * font closest to a regular weight typeface. 280 * </p> 281 * 282 * @param context A context to retrieve the Resources from. 283 * @param id The desired resource identifier of a {@link Typeface}, as generated by the aapt 284 * tool. This integer encodes the package, type, and resource entry. The value 0 is an 285 * invalid identifier. 286 * @param fontCallback A callback to receive async fetching of this font. The callback will be 287 * triggered on the UI thread. 288 * @param handler A handler for the thread the callback should be called on. If null, the 289 * callback will be called on the UI thread. 290 * @throws NotFoundException Throws NotFoundException if the given ID does not exist. 291 */ 292 public static void getFont(@NonNull Context context, @FontRes int id, 293 @NonNull FontCallback fontCallback, @Nullable Handler handler) 294 throws NotFoundException { 295 Preconditions.checkNotNull(fontCallback); 296 if (context.isRestricted()) { 297 fontCallback.callbackFailAsync( 298 FontRequestCallback.FAIL_REASON_SECURITY_VIOLATION, handler); 299 return; 300 } 301 loadFont(context, id, new TypedValue(), Typeface.NORMAL, fontCallback, handler, 302 false /* isXmlRequest */); 303 } 304 305 /** 306 * Used by TintTypedArray. 307 * 308 * @hide 309 */ 310 @RestrictTo(LIBRARY_GROUP) 311 public static Typeface getFont(@NonNull Context context, @FontRes int id, TypedValue value, 312 int style, @Nullable FontCallback fontCallback) throws NotFoundException { 313 if (context.isRestricted()) { 314 return null; 315 } 316 return loadFont(context, id, value, style, fontCallback, null /* handler */, 317 true /* isXmlRequest */); 318 } 319 320 /** 321 * 322 * @param context The Context to get Resources from 323 * @param id The Resource id to load 324 * @param value A TypedValue to use in the fetching 325 * @param style The font style to load 326 * @param fontCallback A callback to trigger when the font is fetched or an error occurs 327 * @param handler A handler to the thread the callback should be called on 328 * @param isRequestFromLayoutInflator Whether this request originated from XML. This is used to 329 * determine if we use or ignore the fontProviderFetchStrategy attribute in 330 * font provider XML fonts. 331 * @return 332 */ 333 private static Typeface loadFont(@NonNull Context context, int id, TypedValue value, 334 int style, @Nullable FontCallback fontCallback, @Nullable Handler handler, 335 boolean isRequestFromLayoutInflator) { 336 final Resources resources = context.getResources(); 337 resources.getValue(id, value, true); 338 Typeface typeface = loadFont(context, resources, value, id, style, fontCallback, handler, 339 isRequestFromLayoutInflator); 340 if (typeface == null && fontCallback == null) { 341 throw new NotFoundException("Font resource ID #0x" 342 + Integer.toHexString(id) + " could not be retrieved."); 343 } 344 return typeface; 345 } 346 347 /** 348 * Load the given font. This method will always return null for asynchronous requests, which 349 * provide a fontCallback, as there is no immediate result. When the callback is not provided, 350 * the request is treated as synchronous and fails if async loading is required. 351 */ 352 private static Typeface loadFont( 353 @NonNull Context context, Resources wrapper, TypedValue value, int id, int style, 354 @Nullable FontCallback fontCallback, @Nullable Handler handler, 355 boolean isRequestFromLayoutInflator) { 356 if (value.string == null) { 357 throw new NotFoundException("Resource \"" + wrapper.getResourceName(id) + "\" (" 358 + Integer.toHexString(id) + ") is not a Font: " + value); 359 } 360 361 final String file = value.string.toString(); 362 if (!file.startsWith("res/")) { 363 // Early exit if the specified string is unlikely to be a resource path. 364 if (fontCallback != null) { 365 fontCallback.callbackFailAsync( 366 FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler); 367 } 368 return null; 369 } 370 Typeface typeface = TypefaceCompat.findFromCache(wrapper, id, style); 371 372 if (typeface != null) { 373 if (fontCallback != null) { 374 fontCallback.callbackSuccessAsync(typeface, handler); 375 } 376 return typeface; 377 } 378 379 try { 380 if (file.toLowerCase().endsWith(".xml")) { 381 final XmlResourceParser rp = wrapper.getXml(id); 382 final FamilyResourceEntry familyEntry = 383 FontResourcesParserCompat.parse(rp, wrapper); 384 if (familyEntry == null) { 385 Log.e(TAG, "Failed to find font-family tag"); 386 if (fontCallback != null) { 387 fontCallback.callbackFailAsync( 388 FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler); 389 } 390 return null; 391 } 392 return TypefaceCompat.createFromResourcesFamilyXml(context, familyEntry, wrapper, 393 id, style, fontCallback, handler, isRequestFromLayoutInflator); 394 } 395 typeface = TypefaceCompat.createFromResourcesFontFile( 396 context, wrapper, id, file, style); 397 if (fontCallback != null) { 398 if (typeface != null) { 399 fontCallback.callbackSuccessAsync(typeface, handler); 400 } else { 401 fontCallback.callbackFailAsync( 402 FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler); 403 } 404 } 405 return typeface; 406 } catch (XmlPullParserException e) { 407 Log.e(TAG, "Failed to parse xml resource " + file, e); 408 } catch (IOException e) { 409 Log.e(TAG, "Failed to read xml resource " + file, e); 410 } 411 if (fontCallback != null) { 412 fontCallback.callbackFailAsync( 413 FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR, handler); 414 } 415 return null; 416 } 417 418 private ResourcesCompat() {} 419} 420