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