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