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