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