• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package android.provider;
17 
18 import static java.lang.annotation.RetentionPolicy.SOURCE;
19 
20 import android.annotation.IntDef;
21 import android.annotation.IntRange;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.ContentResolver;
25 import android.content.ContentUris;
26 import android.content.Context;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.PackageManager.NameNotFoundException;
29 import android.content.pm.PackageManager;
30 import android.content.pm.ProviderInfo;
31 import android.content.pm.Signature;
32 import android.database.Cursor;
33 import android.graphics.Typeface;
34 import android.graphics.fonts.FontVariationAxis;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.CancellationSignal;
38 import android.os.Handler;
39 import android.os.HandlerThread;
40 import android.os.ParcelFileDescriptor;
41 import android.os.Process;
42 import android.os.ResultReceiver;
43 import android.util.ArraySet;
44 import android.util.Log;
45 import android.util.LruCache;
46 
47 import com.android.internal.annotations.GuardedBy;
48 import com.android.internal.annotations.VisibleForTesting;
49 import com.android.internal.util.Preconditions;
50 
51 import java.io.FileInputStream;
52 import java.io.FileNotFoundException;
53 import java.io.IOException;
54 import java.lang.annotation.Retention;
55 import java.lang.annotation.RetentionPolicy;
56 import java.nio.ByteBuffer;
57 import java.nio.channels.FileChannel;
58 import java.util.ArrayList;
59 import java.util.Arrays;
60 import java.util.Collections;
61 import java.util.Comparator;
62 import java.util.HashMap;
63 import java.util.List;
64 import java.util.Map;
65 import java.util.Set;
66 import java.util.concurrent.TimeUnit;
67 import java.util.concurrent.locks.Condition;
68 import java.util.concurrent.locks.Lock;
69 import java.util.concurrent.locks.ReentrantLock;
70 import java.util.concurrent.atomic.AtomicBoolean;
71 import java.util.concurrent.atomic.AtomicReference;
72 
73 /**
74  * Utility class to deal with Font ContentProviders.
75  */
76 public 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.
Columns()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 
FontsContract()170     private FontsContract() {
171     }
172 
173     /** @hide */
setApplicationContextForResources(Context context)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 */
FontInfo(@onNull Uri uri, @IntRange(from = 0) int ttcIndex, @Nullable FontVariationAxis[] axes, @IntRange(from = 1, to = 1000) int weight, boolean italic, int resultCode)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          */
getUri()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          */
getTtcIndex()220         public @IntRange(from = 0) int getTtcIndex() {
221             return mTtcIndex;
222         }
223 
224         /**
225          * Returns the list of axes associated to this font.
226          */
getAxes()227         public @Nullable FontVariationAxis[] getAxes() {
228             return mAxes;
229         }
230 
231         /**
232          * Returns the weight value for this font.
233          */
getWeight()234         public @IntRange(from = 1, to = 1000) int getWeight() {
235             return mWeight;
236         }
237 
238         /**
239          * Returns whether this font is italic.
240          */
isItalic()241         public boolean isItalic() {
242             return mItalic;
243         }
244 
245         /**
246          * Returns result code.
247          *
248          * {@link FontsContract.Columns#RESULT_CODE}
249          */
getResultCode()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 */
FontFamilyResult(@ontResultStatus int statusCode, @Nullable FontInfo[] fonts)298         public FontFamilyResult(@FontResultStatus int statusCode, @Nullable FontInfo[] fonts) {
299             mStatusCode = statusCode;
300             mFonts = fonts;
301         }
302 
getStatusCode()303         public @FontResultStatus int getStatusCode() {
304             return mStatusCode;
305         }
306 
getFonts()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 */
getFontSync(FontRequest request)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 
FontRequestCallback()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          */
onTypefaceRetrieved(Typeface typeface)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          */
onTypefaceRequestFailed(@ontRequestFailReason int reason)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      */
requestFonts(@onNull Context context, @NonNull FontRequest request, @NonNull Handler handler, @Nullable CancellationSignal cancellationSignal, @NonNull FontRequestCallback callback)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      */
fetchFonts( @onNull Context context, @Nullable CancellationSignal cancellationSignal, @NonNull FontRequest request)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      */
buildTypeface(@onNull Context context, @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts)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      */
prepareFontData(Context context, FontInfo[] fonts, CancellationSignal cancellationSignal)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
getProvider( PackageManager packageManager, FontRequest request)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 
equalsByteArrayList( List<byte[]> signatures, List<byte[]> requestSignatures)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 
convertToByteArrayList(Signature[] signatures)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
getFontFromProvider( Context context, FontRequest request, String authority, CancellationSignal cancellationSignal)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