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