1/*
2 * Copyright (C) 2017 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.provider;
18
19import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.annotation.SuppressLint;
22import android.content.ContentResolver;
23import android.content.ContentUris;
24import android.content.Context;
25import android.content.pm.PackageInfo;
26import android.content.pm.PackageManager;
27import android.content.pm.ProviderInfo;
28import android.content.pm.Signature;
29import android.content.res.Resources;
30import android.database.Cursor;
31import android.graphics.Typeface;
32import android.net.Uri;
33import android.os.Build;
34import android.os.CancellationSignal;
35import android.os.Handler;
36import android.os.ParcelFileDescriptor;
37import android.os.Process;
38import android.provider.BaseColumns;
39
40import androidx.annotation.GuardedBy;
41import androidx.annotation.IntDef;
42import androidx.annotation.IntRange;
43import androidx.annotation.NonNull;
44import androidx.annotation.Nullable;
45import androidx.annotation.RequiresApi;
46import androidx.annotation.RestrictTo;
47import androidx.annotation.VisibleForTesting;
48import androidx.collection.LruCache;
49import androidx.collection.SimpleArrayMap;
50import androidx.core.content.res.FontResourcesParserCompat;
51import androidx.core.content.res.ResourcesCompat;
52import androidx.core.graphics.TypefaceCompat;
53import androidx.core.graphics.TypefaceCompatUtil;
54import androidx.core.provider.SelfDestructiveThread.ReplyCallback;
55import androidx.core.util.Preconditions;
56
57import java.lang.annotation.Retention;
58import java.lang.annotation.RetentionPolicy;
59import java.nio.ByteBuffer;
60import java.util.ArrayList;
61import java.util.Arrays;
62import java.util.Collections;
63import java.util.Comparator;
64import java.util.HashMap;
65import java.util.List;
66import java.util.Map;
67import java.util.concurrent.Callable;
68
69/**
70 * Utility class to deal with Font ContentProviders.
71 */
72public class FontsContractCompat {
73    private static final String TAG = "FontsContractCompat";
74
75    private FontsContractCompat() { }
76
77    /**
78     * Defines the constants used in a response from a Font Provider. The cursor returned from the
79     * query should have the ID column populated with the content uri ID for the resulting font.
80     * This should point to a real file or shared memory, as the client will mmap the given file
81     * descriptor. Pipes, sockets and other non-mmap-able file descriptors will fail to load in the
82     * client application.
83     */
84    public static final class Columns implements BaseColumns {
85        /**
86         * Constant used to request data from a font provider. The cursor returned from the query
87         * may populate this column with a long for the font file ID. The client will request a file
88         * descriptor to "file/FILE_ID" with this ID immediately under the top-level content URI. If
89         * not present, the client will request a file descriptor to the top-level URI with the
90         * given base font ID. Note that several results may return the same file ID, e.g. for TTC
91         * files with different indices.
92         */
93        public static final String FILE_ID = "file_id";
94        /**
95         * Constant used to request data from a font provider. The cursor returned from the query
96         * should have this column populated with an int for the ttc index for the resulting font.
97         */
98        public static final String TTC_INDEX = android.provider.FontsContract.Columns.TTC_INDEX;
99        /**
100         * Constant used to request data from a font provider. The cursor returned from the query
101         * may populate this column with the font variation settings String information for the
102         * font.
103         */
104        public static final String VARIATION_SETTINGS =
105                android.provider.FontsContract.Columns.VARIATION_SETTINGS;
106        /**
107         * Constant used to request data from a font provider. The cursor returned from the query
108         * should have this column populated with the int weight for the resulting font. This value
109         * should be between 100 and 900. The most common values are 400 for regular weight and 700
110         * for bold weight.
111         */
112        public static final String WEIGHT = android.provider.FontsContract.Columns.WEIGHT;
113        /**
114         * Constant used to request data from a font provider. The cursor returned from the query
115         * should have this column populated with the int italic for the resulting font. This should
116         * be 0 for regular style and 1 for italic.
117         */
118        public static final String ITALIC = android.provider.FontsContract.Columns.ITALIC;
119        /**
120         * Constant used to request data from a font provider. The cursor returned from the query
121         * should have this column populated to indicate the result status of the
122         * query. This will be checked before any other data in the cursor. Possible values are
123         * {@link #RESULT_CODE_OK}, {@link #RESULT_CODE_FONT_NOT_FOUND},
124         * {@link #RESULT_CODE_MALFORMED_QUERY} and {@link #RESULT_CODE_FONT_UNAVAILABLE}. If not
125         * present, {@link #RESULT_CODE_OK} will be assumed.
126         */
127        public static final String RESULT_CODE = android.provider.FontsContract.Columns.RESULT_CODE;
128
129        /**
130         * Constant used to represent a result was retrieved successfully. The given fonts will be
131         * attempted to retrieve immediately via
132         * {@link android.content.ContentProvider#openFile(Uri, String)}. See {@link #RESULT_CODE}.
133         */
134        public static final int RESULT_CODE_OK =
135                android.provider.FontsContract.Columns.RESULT_CODE_OK;
136        /**
137         * Constant used to represent a result was not found. See {@link #RESULT_CODE}.
138         */
139        public static final int RESULT_CODE_FONT_NOT_FOUND =
140                android.provider.FontsContract.Columns.RESULT_CODE_FONT_NOT_FOUND;
141        /**
142         * Constant used to represent a result was found, but cannot be provided at this moment. Use
143         * this to indicate, for example, that a font needs to be fetched from the network. See
144         * {@link #RESULT_CODE}.
145         */
146        public static final int RESULT_CODE_FONT_UNAVAILABLE =
147                android.provider.FontsContract.Columns.RESULT_CODE_FONT_UNAVAILABLE;
148        /**
149         * Constant used to represent that the query was not in a supported format by the provider.
150         * See {@link #RESULT_CODE}.
151         */
152        public static final int RESULT_CODE_MALFORMED_QUERY =
153                android.provider.FontsContract.Columns.RESULT_CODE_MALFORMED_QUERY;
154    }
155
156    /**
157     * Constant used to identify the List of {@link ParcelFileDescriptor} item in the Bundle
158     * returned to the ResultReceiver in getFont.
159     * @hide
160     */
161    @RestrictTo(LIBRARY_GROUP)
162    public static final String PARCEL_FONT_RESULTS = "font_results";
163
164    // Error codes internal to the system, which can not come from a provider. To keep the number
165    // space open for new provider codes, these should all be negative numbers.
166    /** @hide */
167    @RestrictTo(LIBRARY_GROUP)
168    /* package */ static final int RESULT_CODE_PROVIDER_NOT_FOUND = -1;
169    /** @hide */
170    @RestrictTo(LIBRARY_GROUP)
171    /* package */ static final int RESULT_CODE_WRONG_CERTIFICATES = -2;
172    // Note -3 is used by FontRequestCallback to indicate the font failed to load.
173
174    static final LruCache<String, Typeface> sTypefaceCache = new LruCache<>(16);
175
176    private static final int BACKGROUND_THREAD_KEEP_ALIVE_DURATION_MS = 10000;
177    private static final SelfDestructiveThread sBackgroundThread =
178            new SelfDestructiveThread("fonts", Process.THREAD_PRIORITY_BACKGROUND,
179                    BACKGROUND_THREAD_KEEP_ALIVE_DURATION_MS);
180
181    @NonNull
182    static TypefaceResult getFontInternal(final Context context, final FontRequest request,
183            int style) {
184        FontFamilyResult result;
185        try {
186            result = fetchFonts(context, null /* CancellationSignal */, request);
187        } catch (PackageManager.NameNotFoundException e) {
188            return new TypefaceResult(null, FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND);
189        }
190        if (result.getStatusCode() == FontFamilyResult.STATUS_OK) {
191            final Typeface typeface = TypefaceCompat.createFromFontInfo(
192                    context, null /* CancellationSignal */, result.getFonts(), style);
193            return new TypefaceResult(typeface, typeface != null
194                    ? FontRequestCallback.RESULT_OK
195                    : FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
196        }
197        int resultCode = result.getStatusCode() == FontFamilyResult.STATUS_WRONG_CERTIFICATES
198                ? FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES
199                : FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR;
200        return new TypefaceResult(null, resultCode);
201    }
202
203    static final Object sLock = new Object();
204    @GuardedBy("sLock")
205    static final SimpleArrayMap<String, ArrayList<ReplyCallback<TypefaceResult>>>
206            sPendingReplies = new SimpleArrayMap<>();
207
208    private static final class TypefaceResult {
209        final Typeface mTypeface;
210        @FontRequestCallback.FontRequestFailReason final int mResult;
211
212        TypefaceResult(@Nullable Typeface typeface,
213                @FontRequestCallback.FontRequestFailReason int result) {
214            mTypeface = typeface;
215            mResult = result;
216        }
217    }
218
219    /**
220     * Used for tests, should not be used otherwise.
221     * @hide
222     **/
223    @RestrictTo(LIBRARY_GROUP)
224    public static void resetCache() {
225        sTypefaceCache.evictAll();
226    }
227
228    /** @hide */
229    @RestrictTo(LIBRARY_GROUP)
230    public static Typeface getFontSync(final Context context, final FontRequest request,
231            final @Nullable ResourcesCompat.FontCallback fontCallback,
232            final @Nullable Handler handler, boolean isBlockingFetch, int timeout,
233            final int style) {
234        final String id = request.getIdentifier() + "-" + style;
235        Typeface cached = sTypefaceCache.get(id);
236        if (cached != null) {
237            if (fontCallback != null) {
238                fontCallback.onFontRetrieved(cached);
239            }
240            return cached;
241        }
242
243        if (isBlockingFetch && timeout == FontResourcesParserCompat.INFINITE_TIMEOUT_VALUE) {
244            // Wait forever. No need to post to the thread.
245            TypefaceResult typefaceResult = getFontInternal(context, request, style);
246            if (fontCallback != null) {
247                if (typefaceResult.mResult == FontFamilyResult.STATUS_OK) {
248                    fontCallback.callbackSuccessAsync(typefaceResult.mTypeface, handler);
249                } else {
250                    fontCallback.callbackFailAsync(typefaceResult.mResult, handler);
251                }
252            }
253            return typefaceResult.mTypeface;
254        }
255
256        final Callable<TypefaceResult> fetcher = new Callable<TypefaceResult>() {
257            @Override
258            public TypefaceResult call() throws Exception {
259                TypefaceResult typeface = getFontInternal(context, request, style);
260                if (typeface.mTypeface != null) {
261                    sTypefaceCache.put(id, typeface.mTypeface);
262                }
263                return typeface;
264            }
265        };
266
267        if (isBlockingFetch) {
268            try {
269                return sBackgroundThread.postAndWait(fetcher, timeout).mTypeface;
270            } catch (InterruptedException e) {
271                return null;
272            }
273        } else {
274            final ReplyCallback<TypefaceResult> reply = fontCallback == null ? null
275                    : new ReplyCallback<TypefaceResult>() {
276                        @Override
277                        public void onReply(final TypefaceResult typeface) {
278                            if (typeface == null) {
279                                fontCallback.callbackFailAsync(
280                                        FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND, handler);
281                            } else if (typeface.mResult == FontFamilyResult.STATUS_OK) {
282                                fontCallback.callbackSuccessAsync(typeface.mTypeface, handler);
283                            } else {
284                                fontCallback.callbackFailAsync(typeface.mResult, handler);
285                            }
286                        }
287                    };
288
289            synchronized (sLock) {
290                if (sPendingReplies.containsKey(id)) {
291                    // Already requested. Do not request the same provider again and insert the
292                    // reply to the queue instead.
293                    if (reply != null) {
294                        sPendingReplies.get(id).add(reply);
295                    }
296                    return null;
297                }
298                if (reply != null) {
299                    ArrayList<ReplyCallback<TypefaceResult>> pendingReplies = new ArrayList<>();
300                    pendingReplies.add(reply);
301                    sPendingReplies.put(id, pendingReplies);
302                }
303            }
304            sBackgroundThread.postAndReply(fetcher, new ReplyCallback<TypefaceResult>() {
305                @Override
306                public void onReply(final TypefaceResult typeface) {
307                    final ArrayList<ReplyCallback<TypefaceResult>> replies;
308                    synchronized (sLock) {
309                        replies = sPendingReplies.get(id);
310                        if (replies == null) {
311                            return;  // Nobody requested replies. Do nothing.
312                        }
313                        sPendingReplies.remove(id);
314                    }
315                    for (int i = 0; i < replies.size(); ++i) {
316                        replies.get(i).onReply(typeface);
317                    }
318                }
319            });
320            return null;
321        }
322    }
323
324    /**
325     * Object represent a font entry in the family returned from {@link #fetchFonts}.
326     */
327    public static class FontInfo {
328        private final Uri mUri;
329        private final int mTtcIndex;
330        private final int mWeight;
331        private final boolean mItalic;
332        private final int mResultCode;
333
334        /**
335         * Creates a Font with all the information needed about a provided font.
336         * @param uri A URI associated to the font file.
337         * @param ttcIndex If providing a TTC_INDEX file, the index to point to. Otherwise, 0.
338         * @param weight An integer that indicates the font weight.
339         * @param italic A boolean that indicates the font is italic style or not.
340         * @param resultCode A boolean that indicates the font contents is ready.
341         *
342         * @hide
343         */
344        @RestrictTo(LIBRARY_GROUP)
345        public FontInfo(@NonNull Uri uri, @IntRange(from = 0) int ttcIndex,
346                @IntRange(from = 1, to = 1000) int weight,
347                boolean italic, int resultCode) {
348            mUri = Preconditions.checkNotNull(uri);
349            mTtcIndex = ttcIndex;
350            mWeight = weight;
351            mItalic = italic;
352            mResultCode = resultCode;
353        }
354
355        /**
356         * Returns a URI associated to this record.
357         */
358        public @NonNull Uri getUri() {
359            return mUri;
360        }
361
362        /**
363         * Returns the index to be used to access this font when accessing a TTC file.
364         */
365        public @IntRange(from = 0) int getTtcIndex() {
366            return mTtcIndex;
367        }
368
369        /**
370         * Returns the weight value for this font.
371         */
372        public @IntRange(from = 1, to = 1000) int getWeight() {
373            return mWeight;
374        }
375
376        /**
377         * Returns whether this font is italic.
378         */
379        public boolean isItalic() {
380            return mItalic;
381        }
382
383        /**
384         * Returns result code.
385         *
386         * {@link FontsContractCompat.Columns#RESULT_CODE}
387         */
388        public int getResultCode() {
389            return mResultCode;
390        }
391    }
392
393    /**
394     * Object returned from {@link #fetchFonts}.
395     */
396    public static class FontFamilyResult {
397        /**
398         * Constant represents that the font was successfully retrieved. Note that when this value
399         * is set and {@link #getFonts} returns an empty array, it means there were no fonts
400         * matching the given query.
401         */
402        public static final int STATUS_OK = 0;
403
404        /**
405         * Constant represents that the given certificate was not matched with the provider's
406         * signature. {@link #getFonts} returns null if this status was set.
407         */
408        public static final int STATUS_WRONG_CERTIFICATES = 1;
409
410        /**
411         * Constant represents that the provider returns unexpected data. {@link #getFonts} returns
412         * null if this status was set. For example, this value is set when the font provider
413         * gives invalid format of variation settings.
414         */
415        public static final int STATUS_UNEXPECTED_DATA_PROVIDED = 2;
416
417        /** @hide */
418        @RestrictTo(LIBRARY_GROUP)
419        @IntDef({STATUS_OK, STATUS_WRONG_CERTIFICATES, STATUS_UNEXPECTED_DATA_PROVIDED})
420        @Retention(RetentionPolicy.SOURCE)
421        @interface FontResultStatus {}
422
423        private final @FontResultStatus int mStatusCode;
424        private final FontInfo[] mFonts;
425
426        /** @hide */
427        @RestrictTo(LIBRARY_GROUP)
428        public FontFamilyResult(@FontResultStatus int statusCode, @Nullable FontInfo[] fonts) {
429            mStatusCode = statusCode;
430            mFonts = fonts;
431        }
432
433        public @FontResultStatus int getStatusCode() {
434            return mStatusCode;
435        }
436
437        public FontInfo[] getFonts() {
438            return mFonts;
439        }
440    }
441
442    /**
443     * Interface used to receive asynchronously fetched typefaces.
444     */
445    public static class FontRequestCallback {
446        /** @hide */
447        @RestrictTo(LIBRARY_GROUP)
448        public static final int RESULT_OK = Columns.RESULT_CODE_OK;
449        /**
450         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
451         * provider was not found on the device.
452         */
453        public static final int FAIL_REASON_PROVIDER_NOT_FOUND = RESULT_CODE_PROVIDER_NOT_FOUND;
454        /**
455         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
456         * provider must be authenticated and the given certificates do not match its signature.
457         */
458        public static final int FAIL_REASON_WRONG_CERTIFICATES = RESULT_CODE_WRONG_CERTIFICATES;
459        /**
460         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
461         * returned by the provider was not loaded properly.
462         */
463        public static final int FAIL_REASON_FONT_LOAD_ERROR = -3;
464        /**
465         * Constant that signals that the font was not loaded due to security issues. This usually
466         * means the font was attempted to load on a restricted context.
467         */
468        public static final int FAIL_REASON_SECURITY_VIOLATION = -4;
469        /**
470         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
471         * provider did not return any results for the given query.
472         */
473        public static final int FAIL_REASON_FONT_NOT_FOUND = Columns.RESULT_CODE_FONT_NOT_FOUND;
474        /**
475         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
476         * provider found the queried font, but it is currently unavailable.
477         */
478        public static final int FAIL_REASON_FONT_UNAVAILABLE = Columns.RESULT_CODE_FONT_UNAVAILABLE;
479        /**
480         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
481         * query was not supported by the provider.
482         */
483        public static final int FAIL_REASON_MALFORMED_QUERY = Columns.RESULT_CODE_MALFORMED_QUERY;
484
485        /** @hide */
486        @RestrictTo(LIBRARY_GROUP)
487        @IntDef({ FAIL_REASON_PROVIDER_NOT_FOUND, FAIL_REASON_FONT_LOAD_ERROR,
488                FAIL_REASON_FONT_NOT_FOUND, FAIL_REASON_FONT_UNAVAILABLE,
489                FAIL_REASON_MALFORMED_QUERY, FAIL_REASON_WRONG_CERTIFICATES,
490                FAIL_REASON_SECURITY_VIOLATION, RESULT_OK })
491        @Retention(RetentionPolicy.SOURCE)
492        public @interface FontRequestFailReason {}
493
494        public FontRequestCallback() {}
495
496        /**
497         * Called then a Typeface request done via {@link #requestFont(Context, FontRequest,
498         * FontRequestCallback, Handler)} is complete. Note that this method will not be called if
499         * {@link #onTypefaceRequestFailed(int)} is called instead.
500         * @param typeface  The Typeface object retrieved.
501         */
502        public void onTypefaceRetrieved(Typeface typeface) {}
503
504        /**
505         * Called when a Typeface request done via {@link #requestFont(Context, FontRequest,
506         * FontRequestCallback, Handler)} fails.
507         * @param reason May be one of {@link #FAIL_REASON_PROVIDER_NOT_FOUND},
508         *               {@link #FAIL_REASON_FONT_NOT_FOUND},
509         *               {@link #FAIL_REASON_FONT_LOAD_ERROR},
510         *               {@link #FAIL_REASON_FONT_UNAVAILABLE},
511         *               {@link #FAIL_REASON_MALFORMED_QUERY} or
512         *               {@link #FAIL_REASON_WRONG_CERTIFICATES}, or a provider defined positive
513         *               code number.
514         */
515        public void onTypefaceRequestFailed(@FontRequestFailReason int reason) {}
516    }
517
518    /**
519     * Create a typeface object given a font request. The font will be asynchronously fetched,
520     * therefore the result is delivered to the given callback. See {@link FontRequest}.
521     * Only one of the methods in callback will be invoked, depending on whether the request
522     * succeeds or fails. These calls will happen on the caller thread.
523     * @param context A context to be used for fetching from font provider.
524     * @param request A {@link FontRequest} object that identifies the provider and query for the
525     *                request. May not be null.
526     * @param callback A callback that will be triggered when results are obtained. May not be null.
527     * @param handler A handler to be processed the font fetching.
528     */
529    public static void requestFont(final @NonNull Context context,
530            final @NonNull FontRequest request, final @NonNull FontRequestCallback callback,
531            final @NonNull Handler handler) {
532        final Handler callerThreadHandler = new Handler();
533        handler.post(new Runnable() {
534            @Override
535            public void run() {
536                // TODO: Cache the result.
537                FontFamilyResult result;
538                try {
539                    result = fetchFonts(context, null /* cancellation signal */, request);
540                } catch (PackageManager.NameNotFoundException e) {
541                    callerThreadHandler.post(new Runnable() {
542                        @Override
543                        public void run() {
544                            callback.onTypefaceRequestFailed(
545                                    FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND);
546                        }
547                    });
548                    return;
549                }
550
551                if (result.getStatusCode() != FontFamilyResult.STATUS_OK) {
552                    switch (result.getStatusCode()) {
553                        case FontFamilyResult.STATUS_WRONG_CERTIFICATES:
554                            callerThreadHandler.post(new Runnable() {
555                                @Override
556                                public void run() {
557                                    callback.onTypefaceRequestFailed(
558                                            FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES);
559                                }
560                            });
561                            return;
562                        case FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED:
563                            callerThreadHandler.post(new Runnable() {
564                                @Override
565                                public void run() {
566                                    callback.onTypefaceRequestFailed(
567                                            FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
568                                }
569                            });
570                            return;
571                        default:
572                            // fetchFont returns unexpected status type. Fallback to load error.
573                            callerThreadHandler.post(new Runnable() {
574                                @Override
575                                public void run() {
576                                    callback.onTypefaceRequestFailed(
577                                            FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
578                                }
579                            });
580                            return;
581                    }
582                }
583
584                final FontInfo[] fonts = result.getFonts();
585                if (fonts == null || fonts.length == 0) {
586                    callerThreadHandler.post(new Runnable() {
587                        @Override
588                        public void run() {
589                            callback.onTypefaceRequestFailed(
590                                    FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND);
591                        }
592                    });
593                    return;
594                }
595                for (final FontInfo font : fonts) {
596                    if (font.getResultCode() != Columns.RESULT_CODE_OK) {
597                        // We proceed if all font entry is ready to use. Otherwise report the first
598                        // error.
599                        final int resultCode = font.getResultCode();
600                        if (resultCode < 0) {
601                            // Negative values are reserved for internal errors. Fallback to load
602                            // error.
603                            callerThreadHandler.post(new Runnable() {
604                                @Override
605                                public void run() {
606                                    callback.onTypefaceRequestFailed(
607                                            FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
608                                }
609                            });
610                        } else {
611                            callerThreadHandler.post(new Runnable() {
612                                @Override
613                                public void run() {
614                                    callback.onTypefaceRequestFailed(resultCode);
615                                }
616                            });
617                        }
618                        return;
619                    }
620                }
621
622                final Typeface typeface = buildTypeface(context, null /* cancellation signal */,
623                        fonts);
624                if (typeface == null) {
625                    // Something went wrong during reading font files. This happens if the given
626                    // font file is an unsupported font type.
627                    callerThreadHandler.post(new Runnable() {
628                        @Override
629                        public void run() {
630                            callback.onTypefaceRequestFailed(
631                                    FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
632                        }
633                    });
634                    return;
635                }
636
637                callerThreadHandler.post(new Runnable() {
638                    @Override
639                    public void run() {
640                        callback.onTypefaceRetrieved(typeface);
641                    }
642                });
643            }
644        });
645    }
646
647    /**
648     * Build a Typeface from an array of {@link FontInfo}
649     *
650     * Results that are marked as not ready will be skipped.
651     *
652     * @param context A {@link Context} that will be used to fetch the font contents.
653     * @param cancellationSignal A signal to cancel the operation in progress, or null if none. If
654     *                           the operation is canceled, then {@link
655     *                           android.os.OperationCanceledException} will be thrown.
656     * @param fonts An array of {@link FontInfo} to be used to create a Typeface.
657     * @return A Typeface object. Returns null if typeface creation fails.
658     */
659    @Nullable
660    public static Typeface buildTypeface(@NonNull Context context,
661            @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts) {
662        return TypefaceCompat.createFromFontInfo(context, cancellationSignal, fonts,
663                Typeface.NORMAL);
664    }
665
666    /**
667     * A helper function to create a mapping from {@link Uri} to {@link ByteBuffer}.
668     *
669     * Skip if the file contents is not ready to be read.
670     *
671     * @param context A {@link Context} to be used for resolving content URI in
672     *                {@link FontInfo}.
673     * @param fonts An array of {@link FontInfo}.
674     * @return A map from {@link Uri} to {@link ByteBuffer}.
675     * @hide
676     */
677    @RestrictTo(LIBRARY_GROUP)
678    @RequiresApi(19)
679    public static Map<Uri, ByteBuffer> prepareFontData(Context context, FontInfo[] fonts,
680            CancellationSignal cancellationSignal) {
681        final HashMap<Uri, ByteBuffer> out = new HashMap<>();
682
683        for (FontInfo font : fonts) {
684            if (font.getResultCode() != Columns.RESULT_CODE_OK) {
685                continue;
686            }
687
688            final Uri uri = font.getUri();
689            if (out.containsKey(uri)) {
690                continue;
691            }
692
693            ByteBuffer buffer = TypefaceCompatUtil.mmap(context, cancellationSignal, uri);
694            out.put(uri, buffer);
695        }
696        return Collections.unmodifiableMap(out);
697    }
698
699    /**
700     * Fetch fonts given a font request.
701     *
702     * @param context A {@link Context} to be used for fetching fonts.
703     * @param cancellationSignal A signal to cancel the operation in progress, or null if none. If
704     *                           the operation is canceled, then {@link
705     *                           android.os.OperationCanceledException} will be thrown when the
706     *                           query is executed.
707     * @param request A {@link FontRequest} object that identifies the provider and query for the
708     *                request.
709     *
710     * @return {@link FontFamilyResult}
711     *
712     * @throws PackageManager.NameNotFoundException If requested package or authority was not found
713     *      in the system.
714     */
715    @NonNull
716    public static FontFamilyResult fetchFonts(@NonNull Context context,
717            @Nullable CancellationSignal cancellationSignal, @NonNull FontRequest request)
718            throws PackageManager.NameNotFoundException {
719        ProviderInfo providerInfo = getProvider(
720                context.getPackageManager(), request, context.getResources());
721        if (providerInfo == null) {
722            return new FontFamilyResult(FontFamilyResult.STATUS_WRONG_CERTIFICATES, null);
723
724        }
725        FontInfo[] fonts = getFontFromProvider(
726                context, request, providerInfo.authority, cancellationSignal);
727        return new FontFamilyResult(FontFamilyResult.STATUS_OK, fonts);
728    }
729
730    /** @hide */
731    @VisibleForTesting
732    @RestrictTo(LIBRARY_GROUP)
733    public static @Nullable ProviderInfo getProvider(@NonNull PackageManager packageManager,
734            @NonNull FontRequest request, @Nullable Resources resources)
735            throws PackageManager.NameNotFoundException {
736        String providerAuthority = request.getProviderAuthority();
737        ProviderInfo info = packageManager.resolveContentProvider(providerAuthority, 0);
738        if (info == null) {
739            throw new PackageManager.NameNotFoundException("No package found for authority: "
740                    + providerAuthority);
741        }
742
743        if (!info.packageName.equals(request.getProviderPackage())) {
744            throw new PackageManager.NameNotFoundException("Found content provider "
745                    + providerAuthority
746                    + ", but package was not " + request.getProviderPackage());
747        }
748
749        List<byte[]> signatures;
750        // We correctly check all signatures returned, as advised in the lint error.
751        @SuppressLint("PackageManagerGetSignatures")
752        PackageInfo packageInfo = packageManager.getPackageInfo(info.packageName,
753                PackageManager.GET_SIGNATURES);
754        signatures = convertToByteArrayList(packageInfo.signatures);
755        Collections.sort(signatures, sByteArrayComparator);
756        List<List<byte[]>> requestCertificatesList = getCertificates(request, resources);
757        for (int i = 0; i < requestCertificatesList.size(); ++i) {
758            // Make a copy so we can sort it without modifying the incoming data.
759            List<byte[]> requestSignatures = new ArrayList<>(requestCertificatesList.get(i));
760            Collections.sort(requestSignatures, sByteArrayComparator);
761            if (equalsByteArrayList(signatures, requestSignatures)) {
762                return info;
763            }
764        }
765        return null;
766    }
767
768    private static List<List<byte[]>> getCertificates(FontRequest request, Resources resources) {
769        if (request.getCertificates() != null) {
770            return request.getCertificates();
771        }
772        int resourceId = request.getCertificatesArrayResId();
773        return FontResourcesParserCompat.readCerts(resources, resourceId);
774    }
775
776    private static final Comparator<byte[]> sByteArrayComparator = new Comparator<byte[]>() {
777        @Override
778        public int compare(byte[] l, byte[] r) {
779            if (l.length != r.length) {
780                return l.length - r.length;
781            }
782            for (int i = 0; i < l.length; ++i) {
783                if (l[i] != r[i]) {
784                    return l[i] - r[i];
785                }
786            }
787            return 0;
788        }
789    };
790
791    private static boolean equalsByteArrayList(List<byte[]> signatures,
792            List<byte[]> requestSignatures) {
793        if (signatures.size() != requestSignatures.size()) {
794            return false;
795        }
796        for (int i = 0; i < signatures.size(); ++i) {
797            if (!Arrays.equals(signatures.get(i), requestSignatures.get(i))) {
798                return false;
799            }
800        }
801        return true;
802    }
803
804    private static List<byte[]> convertToByteArrayList(Signature[] signatures) {
805        List<byte[]> shas = new ArrayList<>();
806        for (int i = 0; i < signatures.length; ++i) {
807            shas.add(signatures[i].toByteArray());
808        }
809        return shas;
810    }
811
812    @VisibleForTesting
813    @NonNull
814    static FontInfo[] getFontFromProvider(Context context, FontRequest request, String authority,
815            CancellationSignal cancellationSignal) {
816        ArrayList<FontInfo> result = new ArrayList<>();
817        final Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
818                .authority(authority)
819                .build();
820        final Uri fileBaseUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
821                .authority(authority)
822                .appendPath("file")
823                .build();
824        Cursor cursor = null;
825        try {
826            if (Build.VERSION.SDK_INT > 16) {
827                cursor = context.getContentResolver().query(uri, new String[] {
828                        Columns._ID, Columns.FILE_ID, Columns.TTC_INDEX,
829                        Columns.VARIATION_SETTINGS, Columns.WEIGHT, Columns.ITALIC,
830                        Columns.RESULT_CODE },
831                "query = ?", new String[] { request.getQuery() }, null, cancellationSignal);
832            } else {
833                // No cancellation signal.
834                cursor = context.getContentResolver().query(uri, new String[] {
835                        Columns._ID, Columns.FILE_ID, Columns.TTC_INDEX,
836                        Columns.VARIATION_SETTINGS, Columns.WEIGHT, Columns.ITALIC,
837                        Columns.RESULT_CODE },
838                "query = ?", new String[] { request.getQuery() }, null);
839            }
840            if (cursor != null && cursor.getCount() > 0) {
841                final int resultCodeColumnIndex = cursor.getColumnIndex(Columns.RESULT_CODE);
842                result = new ArrayList<>();
843                final int idColumnIndex = cursor.getColumnIndex(Columns._ID);
844                final int fileIdColumnIndex = cursor.getColumnIndex(Columns.FILE_ID);
845                final int ttcIndexColumnIndex = cursor.getColumnIndex(Columns.TTC_INDEX);
846                final int weightColumnIndex = cursor.getColumnIndex(Columns.WEIGHT);
847                final int italicColumnIndex = cursor.getColumnIndex(Columns.ITALIC);
848                while (cursor.moveToNext()) {
849                    int resultCode = resultCodeColumnIndex != -1
850                            ? cursor.getInt(resultCodeColumnIndex) : Columns.RESULT_CODE_OK;
851                    final int ttcIndex = ttcIndexColumnIndex != -1
852                            ? cursor.getInt(ttcIndexColumnIndex) : 0;
853                    Uri fileUri;
854                    if (fileIdColumnIndex == -1) {
855                        long id = cursor.getLong(idColumnIndex);
856                        fileUri = ContentUris.withAppendedId(uri, id);
857                    } else {
858                        long id = cursor.getLong(fileIdColumnIndex);
859                        fileUri = ContentUris.withAppendedId(fileBaseUri, id);
860                    }
861
862                    int weight = weightColumnIndex != -1 ? cursor.getInt(weightColumnIndex) : 400;
863                    boolean italic = italicColumnIndex != -1 && cursor.getInt(italicColumnIndex)
864                            == 1;
865                    result.add(new FontInfo(fileUri, ttcIndex, weight, italic, resultCode));
866                }
867            }
868        } finally {
869            if (cursor != null) {
870                cursor.close();
871            }
872        }
873        return result.toArray(new FontInfo[0]);
874    }
875}
876