FontsContract.java revision 54084b64b1860b652b1c50ba942b4cfc7fb28805
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 */
16package android.provider;
17
18import static android.graphics.fonts.FontVariationAxis.InvalidFormatException;
19import static java.lang.annotation.RetentionPolicy.SOURCE;
20
21import android.annotation.IntDef;
22import android.annotation.IntRange;
23import android.annotation.NonNull;
24import android.annotation.Nullable;
25import android.content.ContentResolver;
26import android.content.ContentUris;
27import android.content.Context;
28import android.content.pm.PackageInfo;
29import android.content.pm.PackageManager.NameNotFoundException;
30import android.content.pm.PackageManager;
31import android.content.pm.ProviderInfo;
32import android.content.pm.Signature;
33import android.database.Cursor;
34import android.graphics.Typeface;
35import android.graphics.fonts.FontRequest;
36import android.graphics.fonts.FontVariationAxis;
37import android.net.Uri;
38import android.os.Bundle;
39import android.os.CancellationSignal;
40import android.os.Handler;
41import android.os.HandlerThread;
42import android.os.ParcelFileDescriptor;
43import android.os.Process;
44import android.os.ResultReceiver;
45import android.util.ArraySet;
46import android.util.Log;
47import android.util.LruCache;
48
49import com.android.internal.annotations.GuardedBy;
50import com.android.internal.annotations.VisibleForTesting;
51import com.android.internal.util.Preconditions;
52
53import java.io.FileInputStream;
54import java.io.FileNotFoundException;
55import java.io.IOException;
56import java.lang.annotation.Retention;
57import java.lang.annotation.RetentionPolicy;
58import java.nio.ByteBuffer;
59import java.nio.channels.FileChannel;
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.Set;
68
69/**
70 * Utility class to deal with Font ContentProviders.
71 */
72public class FontsContract {
73    private static final String TAG = "FontsContract";
74
75    /**
76     * Defines the constants used in a response from a Font Provider. The cursor returned from the
77     * query should have the ID column populated with the content uri ID for the resulting font.
78     * This should point to a real file or shared memory, as the client will mmap the given file
79     * descriptor. Pipes, sockets and other non-mmap-able file descriptors will fail to load in the
80     * client application.
81     */
82    public static final class Columns implements BaseColumns {
83
84        // Do not instantiate.
85        private Columns() {}
86
87        /**
88         * Constant used to request data from a font provider. The cursor returned from the query
89         * may populate this column with a long for the font file ID. The client will request a file
90         * descriptor to "file/FILE_ID" with this ID immediately under the top-level content URI. If
91         * not present, the client will request a file descriptor to the top-level URI with the
92         * given base font ID. Note that several results may return the same file ID, e.g. for TTC
93         * files with different indices.
94         */
95        public static final String FILE_ID = "file_id";
96        /**
97         * Constant used to request data from a font provider. The cursor returned from the query
98         * should have this column populated with an int for the ttc index for the resulting font.
99         */
100        public static final String TTC_INDEX = "font_ttc_index";
101        /**
102         * Constant used to request data from a font provider. The cursor returned from the query
103         * may populate this column with the font variation settings String information for the
104         * font.
105         */
106        public static final String VARIATION_SETTINGS = "font_variation_settings";
107        /**
108         * Constant used to request data from a font provider. The cursor returned from the query
109         * should have this column populated with the int weight for the resulting font. This value
110         * should be between 100 and 900. The most common values are 400 for regular weight and 700
111         * for bold weight.
112         */
113        public static final String WEIGHT = "font_weight";
114        /**
115         * Constant used to request data from a font provider. The cursor returned from the query
116         * should have this column populated with the int italic for the resulting font. This should
117         * be 0 for regular style and 1 for italic.
118         */
119        public static final String ITALIC = "font_italic";
120        /**
121         * Constant used to request data from a font provider. The cursor returned from the query
122         * should have this column populated to indicate the result status of the
123         * query. This will be checked before any other data in the cursor. Possible values are
124         * {@link #RESULT_CODE_OK}, {@link #RESULT_CODE_FONT_NOT_FOUND},
125         * {@link #RESULT_CODE_MALFORMED_QUERY} and {@link #RESULT_CODE_FONT_UNAVAILABLE} for system
126         * defined values. You may also define your own values in the 0x000010000..0xFFFF0000 range.
127         * If not present, {@link #RESULT_CODE_OK} will be assumed.
128         */
129        public static final String RESULT_CODE = "result_code";
130
131        /**
132         * Constant used to represent a result was retrieved successfully. The given fonts will be
133         * attempted to retrieve immediately via
134         * {@link android.content.ContentProvider#openFile(Uri, String)}. See {@link #RESULT_CODE}.
135         */
136        public static final int RESULT_CODE_OK = 0;
137        /**
138         * Constant used to represent a result was not found. See {@link #RESULT_CODE}.
139         */
140        public static final int RESULT_CODE_FONT_NOT_FOUND = 1;
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 = 2;
147        /**
148         * Constant used to represent that the query was not in a supported format by the provider.
149         * See {@link #RESULT_CODE}.
150         */
151        public static final int RESULT_CODE_MALFORMED_QUERY = 3;
152    }
153
154    private static final Object sLock = new Object();
155    @GuardedBy("sLock")
156    private static Handler sHandler;
157    @GuardedBy("sLock")
158    private static HandlerThread sThread;
159    @GuardedBy("sLock")
160    private static Set<String> sInQueueSet;
161
162    private volatile static Context sContext;  // set once in setApplicationContextForResources
163
164    private static final LruCache<String, Typeface> sTypefaceCache = new LruCache<>(16);
165
166    private FontsContract() {
167    }
168
169    /** @hide */
170    public static void setApplicationContextForResources(Context context) {
171        sContext = context.getApplicationContext();
172    }
173
174    /**
175     * Object represent a font entry in the family returned from {@link #fetchFonts}.
176     */
177    public static class FontInfo {
178        private final Uri mUri;
179        private final int mTtcIndex;
180        private final FontVariationAxis[] mAxes;
181        private final int mWeight;
182        private final boolean mItalic;
183        private final int mResultCode;
184
185        /**
186         * Creates a Font with all the information needed about a provided font.
187         * @param uri A URI associated to the font file.
188         * @param ttcIndex If providing a TTC_INDEX file, the index to point to. Otherwise, 0.
189         * @param axes If providing a variation font, the settings for it. May be null.
190         * @param weight An integer that indicates the font weight.
191         * @param italic A boolean that indicates the font is italic style or not.
192         * @param resultCode A boolean that indicates the font contents is ready.
193         */
194        /** @hide */
195        public FontInfo(@NonNull Uri uri, @IntRange(from = 0) int ttcIndex,
196                @Nullable FontVariationAxis[] axes, @IntRange(from = 1, to = 1000) int weight,
197                boolean italic, int resultCode) {
198            mUri = Preconditions.checkNotNull(uri);
199            mTtcIndex = ttcIndex;
200            mAxes = axes;
201            mWeight = weight;
202            mItalic = italic;
203            mResultCode = resultCode;
204        }
205
206        /**
207         * Returns a URI associated to this record.
208         */
209        public @NonNull Uri getUri() {
210            return mUri;
211        }
212
213        /**
214         * Returns the index to be used to access this font when accessing a TTC file.
215         */
216        public @IntRange(from = 0) int getTtcIndex() {
217            return mTtcIndex;
218        }
219
220        /**
221         * Returns the list of axes associated to this font.
222         */
223        public @Nullable FontVariationAxis[] getAxes() {
224            return mAxes;
225        }
226
227        /**
228         * Returns the weight value for this font.
229         */
230        public @IntRange(from = 1, to = 1000) int getWeight() {
231            return mWeight;
232        }
233
234        /**
235         * Returns whether this font is italic.
236         */
237        public boolean isItalic() {
238            return mItalic;
239        }
240
241        /**
242         * Returns result code.
243         *
244         * {@link FontsContract.Columns#RESULT_CODE}
245         */
246        public int getResultCode() {
247            return mResultCode;
248        }
249    }
250
251    /**
252     * Object returned from {@link #fetchFonts}.
253     */
254    public static class FontFamilyResult {
255        /**
256         * Constant represents that the font was successfully retrieved. Note that when this value
257         * is set and {@link #getFonts} returns an empty array, it means there were no fonts
258         * matching the given query.
259         */
260        public static final int STATUS_OK = 0;
261
262        /**
263         * Constant represents that the given certificate was not matched with the provider's
264         * signature. {@link #getFonts} returns null if this status was set.
265         */
266        public static final int STATUS_WRONG_CERTIFICATES = 1;
267
268        /**
269         * Constant represents that the provider returns unexpected data. {@link #getFonts} returns
270         * null if this status was set. For example, this value is set when the font provider
271         * gives invalid format of variation settings.
272         */
273        public static final int STATUS_UNEXPECTED_DATA_PROVIDED = 2;
274
275        /**
276         * Constant represents that the fetching font data was rejected by system. This happens if
277         * the passed context is restricted.
278         */
279        public static final int STATUS_REJECTED = 3;
280
281        /** @hide */
282        @IntDef({STATUS_OK, STATUS_WRONG_CERTIFICATES, STATUS_UNEXPECTED_DATA_PROVIDED})
283        @Retention(RetentionPolicy.SOURCE)
284        @interface FontResultStatus {}
285
286        private final @FontResultStatus int mStatusCode;
287        private final FontInfo[] mFonts;
288
289        /** @hide */
290        public FontFamilyResult(@FontResultStatus int statusCode, @Nullable FontInfo[] fonts) {
291            mStatusCode = statusCode;
292            mFonts = fonts;
293        }
294
295        public @FontResultStatus int getStatusCode() {
296            return mStatusCode;
297        }
298
299        public @NonNull FontInfo[] getFonts() {
300            return mFonts;
301        }
302    }
303
304    private static final int THREAD_RENEWAL_THRESHOLD_MS = 10000;
305
306    // We use a background thread to post the content resolving work for all requests on. This
307    // thread should be quit/stopped after all requests are done.
308    // TODO: Factor out to other class. Consider to switch MessageQueue.IdleHandler.
309    private static final Runnable sReplaceDispatcherThreadRunnable = new Runnable() {
310        @Override
311        public void run() {
312            synchronized (sLock) {
313                if (sThread != null) {
314                    sThread.quitSafely();
315                    sThread = null;
316                    sHandler = null;
317                    sInQueueSet = null;
318                }
319            }
320        }
321    };
322
323    /** @hide */
324    public static Typeface getFontOrWarmUpCache(FontRequest request) {
325        final String id = request.getIdentifier();
326        Typeface cachedTypeface = sTypefaceCache.get(id);
327        if (cachedTypeface != null) {
328            return cachedTypeface;
329        }
330
331        // Unfortunately the typeface is not available at this time, but requesting from the font
332        // provider takes too much time. For now, request the font data to ensure it is in the cache
333        // next time and return.
334        synchronized (sLock) {
335            if (sHandler == null) {
336                sThread = new HandlerThread("fonts", Process.THREAD_PRIORITY_BACKGROUND);
337                sThread.start();
338                sHandler = new Handler(sThread.getLooper());
339                sInQueueSet = new ArraySet<>();
340            }
341            if (sInQueueSet.contains(id)) {
342                return null;  // Already requested.
343            }
344            sInQueueSet.add(id);
345            sHandler.post(() -> {
346                synchronized (sLock) {
347                    sInQueueSet.remove(id);
348                }
349                try {
350                    FontFamilyResult result = fetchFonts(sContext, null, request);
351                    if (result.getStatusCode() == FontFamilyResult.STATUS_OK) {
352                        Typeface typeface = buildTypeface(sContext, null, result.getFonts());
353                        if (typeface != null) {
354                            sTypefaceCache.put(id, typeface);
355                        }
356                    }
357                } catch (NameNotFoundException e) {
358                    // Ignore.
359                }
360            });
361            sHandler.removeCallbacks(sReplaceDispatcherThreadRunnable);
362            sHandler.postDelayed(sReplaceDispatcherThreadRunnable, THREAD_RENEWAL_THRESHOLD_MS);
363        }
364        return null;
365    }
366
367    /**
368     * Interface used to receive asynchronously fetched typefaces.
369     */
370    public static class FontRequestCallback {
371        /**
372         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
373         * provider was not found on the device.
374         */
375        public static final int FAIL_REASON_PROVIDER_NOT_FOUND = -1;
376        /**
377         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
378         * provider must be authenticated and the given certificates do not match its signature.
379         */
380        public static final int FAIL_REASON_WRONG_CERTIFICATES = -2;
381        /**
382         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
383         * returned by the provider was not loaded properly.
384         */
385        public static final int FAIL_REASON_FONT_LOAD_ERROR = -3;
386        /**
387         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
388         * provider did not return any results for the given query.
389         */
390        public static final int FAIL_REASON_FONT_NOT_FOUND = Columns.RESULT_CODE_FONT_NOT_FOUND;
391        /**
392         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
393         * provider found the queried font, but it is currently unavailable.
394         */
395        public static final int FAIL_REASON_FONT_UNAVAILABLE = Columns.RESULT_CODE_FONT_UNAVAILABLE;
396        /**
397         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
398         * query was not supported by the provider.
399         */
400        public static final int FAIL_REASON_MALFORMED_QUERY = Columns.RESULT_CODE_MALFORMED_QUERY;
401
402        /** @hide */
403        @IntDef({ FAIL_REASON_PROVIDER_NOT_FOUND, FAIL_REASON_FONT_LOAD_ERROR,
404                FAIL_REASON_FONT_NOT_FOUND, FAIL_REASON_FONT_UNAVAILABLE,
405                FAIL_REASON_MALFORMED_QUERY })
406        @Retention(RetentionPolicy.SOURCE)
407        @interface FontRequestFailReason {}
408
409        public FontRequestCallback() {}
410
411        /**
412         * Called then a Typeface request done via {@link #requestFont} is complete. Note that this
413         * method will not be called if {@link #onTypefaceRequestFailed(int)} is called instead.
414         * @param typeface  The Typeface object retrieved.
415         */
416        public void onTypefaceRetrieved(Typeface typeface) {}
417
418        /**
419         * Called when a Typeface request done via {@link #requestFont}} fails.
420         * @param reason One of {@link #FAIL_REASON_PROVIDER_NOT_FOUND},
421         *               {@link #FAIL_REASON_FONT_NOT_FOUND},
422         *               {@link #FAIL_REASON_FONT_LOAD_ERROR},
423         *               {@link #FAIL_REASON_FONT_UNAVAILABLE} or
424         *               {@link #FAIL_REASON_MALFORMED_QUERY} if returned by the system. May also be
425         *               a positive value greater than 0 defined by the font provider as an
426         *               additional error code. Refer to the provider's documentation for more
427         *               information on possible returned error codes.
428         */
429        public void onTypefaceRequestFailed(@FontRequestFailReason int reason) {}
430    }
431
432    /**
433     * Create a typeface object given a font request. The font will be asynchronously fetched,
434     * therefore the result is delivered to the given callback. See {@link FontRequest}.
435     * Only one of the methods in callback will be invoked, depending on whether the request
436     * succeeds or fails. These calls will happen on the caller thread.
437     *
438     * Note that the result Typeface may be cached internally and the same instance will be returned
439     * the next time you call this method with the same request. If you want to bypass this cache,
440     * use {@link #fetchFonts} and {@link #buildTypeface} instead.
441     *
442     * @param context A context to be used for fetching from font provider.
443     * @param request A {@link FontRequest} object that identifies the provider and query for the
444     *                request. May not be null.
445     * @param callback A callback that will be triggered when results are obtained. May not be null.
446     * @param handler A handler to be processed the font fetching.
447     */
448    public static void requestFont(@NonNull Context context, @NonNull FontRequest request,
449            @NonNull FontRequestCallback callback, @NonNull Handler handler) {
450
451        final Handler callerThreadHandler = new Handler();
452        final Typeface cachedTypeface = sTypefaceCache.get(request.getIdentifier());
453        if (cachedTypeface != null) {
454            callerThreadHandler.post(() -> callback.onTypefaceRetrieved(cachedTypeface));
455            return;
456        }
457
458        handler.post(() -> {
459            FontFamilyResult result;
460            try {
461                result = fetchFonts(context, null /* cancellation signal */, request);
462            } catch (NameNotFoundException e) {
463                callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
464                        FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND));
465                return;
466            }
467
468            // Same request might be dispatched during fetchFonts. Check the cache again.
469            final Typeface anotherCachedTypeface = sTypefaceCache.get(request.getIdentifier());
470            if (anotherCachedTypeface != null) {
471                callerThreadHandler.post(() -> callback.onTypefaceRetrieved(anotherCachedTypeface));
472                return;
473            }
474
475            if (result.getStatusCode() != FontFamilyResult.STATUS_OK) {
476                switch (result.getStatusCode()) {
477                    case FontFamilyResult.STATUS_WRONG_CERTIFICATES:
478                        callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
479                                FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES));
480                        return;
481                    case FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED:
482                        callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
483                                FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
484                        return;
485                    default:
486                        // fetchFont returns unexpected status type. Fallback to load error.
487                        callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
488                                FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
489                        return;
490                }
491            }
492
493            final FontInfo[] fonts = result.getFonts();
494            if (fonts == null || fonts.length == 0) {
495                callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
496                        FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND));
497                return;
498            }
499            for (final FontInfo font : fonts) {
500                if (font.getResultCode() != Columns.RESULT_CODE_OK) {
501                    // We proceed if all font entry is ready to use. Otherwise report the first
502                    // error.
503                    final int resultCode = font.getResultCode();
504                    if (resultCode < 0) {
505                        // Negative values are reserved for internal errors. Fallback to load error.
506                        callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
507                                FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
508                    } else {
509                        callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
510                                resultCode));
511                    }
512                    return;
513                }
514            }
515
516            final Typeface typeface = buildTypeface(context, null /* cancellation signal */, fonts);
517            if (typeface == null) {
518                // Something went wrong during reading font files. This happens if the given font
519                // file is an unsupported font type.
520                callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
521                        FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
522                return;
523            }
524
525            sTypefaceCache.put(request.getIdentifier(), typeface);
526            callerThreadHandler.post(() -> callback.onTypefaceRetrieved(typeface));
527        });
528    }
529
530    /**
531     * Fetch fonts given a font request.
532     *
533     * @param context A {@link Context} to be used for fetching fonts.
534     * @param cancellationSignal A signal to cancel the operation in progress, or null if none. If
535     *                           the operation is canceled, then {@link
536     *                           android.os.OperationCanceledException} will be thrown when the
537     *                           query is executed.
538     * @param request A {@link FontRequest} object that identifies the provider and query for the
539     *                request.
540     *
541     * @return {@link FontFamilyResult}
542     *
543     * @throws NameNotFoundException If requested package or authority was not found in system.
544     */
545    public static @NonNull FontFamilyResult fetchFonts(
546            @NonNull Context context, @Nullable CancellationSignal cancellationSignal,
547            @NonNull FontRequest request) throws NameNotFoundException {
548        if (context.isRestricted()) {
549            // TODO: Should we allow if the peer process is system or myself?
550            return new FontFamilyResult(FontFamilyResult.STATUS_REJECTED, null);
551        }
552        ProviderInfo providerInfo = getProvider(context.getPackageManager(), request);
553        if (providerInfo == null) {
554            return new FontFamilyResult(FontFamilyResult.STATUS_WRONG_CERTIFICATES, null);
555
556        }
557        try {
558            FontInfo[] fonts = getFontFromProvider(
559                    context, request, providerInfo.authority, cancellationSignal);
560            return new FontFamilyResult(FontFamilyResult.STATUS_OK, fonts);
561        } catch (InvalidFormatException e) {
562            return new FontFamilyResult(FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED, null);
563        }
564    }
565
566    /**
567     * Build a Typeface from an array of {@link FontInfo}. Results that are marked as not ready
568     * will be skipped.
569     *
570     * @param context A {@link Context} that will be used to fetch the font contents.
571     * @param cancellationSignal A signal to cancel the operation in progress, or null if none. If
572     *                           the operation is canceled, then {@link
573     *                           android.os.OperationCanceledException} will be thrown.
574     * @param fonts An array of {@link FontInfo} to be used to create a Typeface.
575     * @param weight A weight value to be used for selecting a font from a font family.
576     * @param italic {@code true} if this font is of italic style. This will be used for font
577     *               selection from a font family.
578     * @param fallbackFontName A fallback font name used if this method fails to create the
579     *                         Typeface. By passing {@code null}, this method returns {@code null}
580     *                         if typeface creation fails.
581     * @return A Typeface object. May return {@code null} if that is the value passed to {@code
582     *         fallBackFontName}.
583     */
584    public static Typeface buildTypeface(@NonNull Context context,
585            @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts,
586            int weight, boolean italic, @Nullable String fallbackFontName) {
587        if (context.isRestricted()) {
588            // TODO: Should we allow if the peer process is system or myself?
589            return null;
590        }
591        final Map<Uri, ByteBuffer> uriBuffer =
592                prepareFontData(context, fonts, cancellationSignal);
593        return new Typeface.Builder(fonts, uriBuffer)
594            .setFallback(fallbackFontName)
595            .setWeight(weight)
596            .setItalic(italic)
597            .build();
598    }
599
600    /**
601     * Build a Typeface from an array of {@link FontInfo}
602     *
603     * Results that are marked as not ready will be skipped.
604     *
605     * @param context A {@link Context} that will be used to fetch the font contents.
606     * @param cancellationSignal A signal to cancel the operation in progress, or null if none. If
607     *                           the operation is canceled, then {@link
608     *                           android.os.OperationCanceledException} will be thrown.
609     * @param fonts An array of {@link FontInfo} to be used to create a Typeface.
610     * @return A Typeface object. Returns null if typeface creation fails.
611     */
612    public static Typeface buildTypeface(@NonNull Context context,
613            @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts) {
614        if (context.isRestricted()) {
615            // TODO: Should we allow if the peer process is system or myself?
616            return null;
617        }
618        final Map<Uri, ByteBuffer> uriBuffer =
619                prepareFontData(context, fonts, cancellationSignal);
620        return new Typeface.Builder(fonts, uriBuffer).build();
621    }
622
623    /**
624     * A helper function to create a mapping from {@link Uri} to {@link ByteBuffer}.
625     *
626     * Skip if the file contents is not ready to be read.
627     *
628     * @param context A {@link Context} to be used for resolving content URI in
629     *                {@link FontInfo}.
630     * @param fonts An array of {@link FontInfo}.
631     * @return A map from {@link Uri} to {@link ByteBuffer}.
632     */
633    private static Map<Uri, ByteBuffer> prepareFontData(Context context, FontInfo[] fonts,
634            CancellationSignal cancellationSignal) {
635        final HashMap<Uri, ByteBuffer> out = new HashMap<>();
636        final ContentResolver resolver = context.getContentResolver();
637
638        for (FontInfo font : fonts) {
639            if (font.getResultCode() != Columns.RESULT_CODE_OK) {
640                continue;
641            }
642
643            final Uri uri = font.getUri();
644            if (out.containsKey(uri)) {
645                continue;
646            }
647
648            ByteBuffer buffer = null;
649            try (final ParcelFileDescriptor pfd =
650                    resolver.openFileDescriptor(uri, "r", cancellationSignal);
651                    final FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {
652                final FileChannel fileChannel = fis.getChannel();
653                final long size = fileChannel.size();
654                buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);
655            } catch (IOException e) {
656                // ignore
657            }
658
659            // TODO: try other approach?, e.g. read all contents instead of mmap.
660
661            out.put(uri, buffer);
662        }
663        return Collections.unmodifiableMap(out);
664    }
665
666    /** @hide */
667    @VisibleForTesting
668    public static @Nullable ProviderInfo getProvider(
669            PackageManager packageManager, FontRequest request) throws NameNotFoundException {
670        String providerAuthority = request.getProviderAuthority();
671        ProviderInfo info = packageManager.resolveContentProvider(providerAuthority, 0);
672        if (info == null) {
673            throw new NameNotFoundException("No package found for authority: " + providerAuthority);
674        }
675
676        if (!info.packageName.equals(request.getProviderPackage())) {
677            throw new NameNotFoundException("Found content provider " + providerAuthority
678                    + ", but package was not " + request.getProviderPackage());
679        }
680        // Trust system apps without signature checks
681        if (info.applicationInfo.isSystemApp()) {
682            return info;
683        }
684
685        List<byte[]> signatures;
686        PackageInfo packageInfo = packageManager.getPackageInfo(info.packageName,
687                PackageManager.GET_SIGNATURES);
688        signatures = convertToByteArrayList(packageInfo.signatures);
689        Collections.sort(signatures, sByteArrayComparator);
690
691        List<List<byte[]>> requestCertificatesList = request.getCertificates();
692        for (int i = 0; i < requestCertificatesList.size(); ++i) {
693            // Make a copy so we can sort it without modifying the incoming data.
694            List<byte[]> requestSignatures = new ArrayList<>(requestCertificatesList.get(i));
695            Collections.sort(requestSignatures, sByteArrayComparator);
696            if (equalsByteArrayList(signatures, requestSignatures)) {
697                return info;
698            }
699        }
700        return null;
701    }
702
703    private static final Comparator<byte[]> sByteArrayComparator = (l, r) -> {
704        if (l.length != r.length) {
705            return l.length - r.length;
706        }
707        for (int i = 0; i < l.length; ++i) {
708            if (l[i] != r[i]) {
709                return l[i] - r[i];
710            }
711        }
712        return 0;
713    };
714
715    private static boolean equalsByteArrayList(
716            List<byte[]> signatures, List<byte[]> requestSignatures) {
717        if (signatures.size() != requestSignatures.size()) {
718            return false;
719        }
720        for (int i = 0; i < signatures.size(); ++i) {
721            if (!Arrays.equals(signatures.get(i), requestSignatures.get(i))) {
722                return false;
723            }
724        }
725        return true;
726    }
727
728    private static List<byte[]> convertToByteArrayList(Signature[] signatures) {
729        List<byte[]> shas = new ArrayList<>();
730        for (int i = 0; i < signatures.length; ++i) {
731            shas.add(signatures[i].toByteArray());
732        }
733        return shas;
734    }
735
736    /** @hide */
737    @VisibleForTesting
738    public static @NonNull FontInfo[] getFontFromProvider(
739            Context context, FontRequest request, String authority,
740            CancellationSignal cancellationSignal) throws InvalidFormatException {
741        ArrayList<FontInfo> result = new ArrayList<>();
742        final Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
743                .authority(authority)
744                .build();
745        final Uri fileBaseUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
746                .authority(authority)
747                .appendPath("file")
748                .build();
749        try (Cursor cursor = context.getContentResolver().query(uri, new String[] { Columns._ID,
750                        Columns.FILE_ID, Columns.TTC_INDEX, Columns.VARIATION_SETTINGS,
751                        Columns.WEIGHT, Columns.ITALIC, Columns.RESULT_CODE },
752                "query = ?", new String[] { request.getQuery() }, null, cancellationSignal);) {
753            // TODO: Should we restrict the amount of fonts that can be returned?
754            // TODO: Write documentation explaining that all results should be from the same family.
755            if (cursor != null && cursor.getCount() > 0) {
756                final int resultCodeColumnIndex = cursor.getColumnIndex(Columns.RESULT_CODE);
757                result = new ArrayList<>();
758                final int idColumnIndex = cursor.getColumnIndexOrThrow(Columns._ID);
759                final int fileIdColumnIndex = cursor.getColumnIndex(Columns.FILE_ID);
760                final int ttcIndexColumnIndex = cursor.getColumnIndex(Columns.TTC_INDEX);
761                final int vsColumnIndex = cursor.getColumnIndex(Columns.VARIATION_SETTINGS);
762                final int weightColumnIndex = cursor.getColumnIndex(Columns.WEIGHT);
763                final int italicColumnIndex = cursor.getColumnIndex(Columns.ITALIC);
764                while (cursor.moveToNext()) {
765                    int resultCode = resultCodeColumnIndex != -1
766                            ? cursor.getInt(resultCodeColumnIndex) : Columns.RESULT_CODE_OK;
767                    final int ttcIndex = ttcIndexColumnIndex != -1
768                            ? cursor.getInt(ttcIndexColumnIndex) : 0;
769                    final String variationSettings = vsColumnIndex != -1
770                            ? cursor.getString(vsColumnIndex) : null;
771
772                    Uri fileUri;
773                    if (fileIdColumnIndex == -1) {
774                        long id = cursor.getLong(idColumnIndex);
775                        fileUri = ContentUris.withAppendedId(uri, id);
776                    } else {
777                        long id = cursor.getLong(fileIdColumnIndex);
778                        fileUri = ContentUris.withAppendedId(fileBaseUri, id);
779                    }
780                    int weight;
781                    boolean italic;
782                    if (weightColumnIndex != -1 && italicColumnIndex != -1) {
783                        weight = cursor.getInt(weightColumnIndex);
784                        italic = cursor.getInt(italicColumnIndex) == 1;
785                    } else {
786                        weight = Typeface.Builder.NORMAL_WEIGHT;
787                        italic = false;
788                    }
789                    FontVariationAxis[] axes =
790                            FontVariationAxis.fromFontVariationSettings(variationSettings);
791                    result.add(new FontInfo(fileUri, ttcIndex, axes, weight, italic, resultCode));
792                }
793            }
794        }
795        return result.toArray(new FontInfo[0]);
796    }
797}
798