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({STATUS_OK, STATUS_WRONG_CERTIFICATES, STATUS_UNEXPECTED_DATA_PROVIDED}) 287 @Retention(RetentionPolicy.SOURCE) 288 @interface FontResultStatus {} 289 290 private final @FontResultStatus int mStatusCode; 291 private final FontInfo[] mFonts; 292 293 /** @hide */ FontFamilyResult(@ontResultStatus int statusCode, @Nullable FontInfo[] fonts)294 public FontFamilyResult(@FontResultStatus int statusCode, @Nullable FontInfo[] fonts) { 295 mStatusCode = statusCode; 296 mFonts = fonts; 297 } 298 getStatusCode()299 public @FontResultStatus int getStatusCode() { 300 return mStatusCode; 301 } 302 getFonts()303 public @NonNull FontInfo[] getFonts() { 304 return mFonts; 305 } 306 } 307 308 private static final int THREAD_RENEWAL_THRESHOLD_MS = 10000; 309 310 private static final long SYNC_FONT_FETCH_TIMEOUT_MS = 500; 311 312 // We use a background thread to post the content resolving work for all requests on. This 313 // thread should be quit/stopped after all requests are done. 314 // TODO: Factor out to other class. Consider to switch MessageQueue.IdleHandler. 315 private static final Runnable sReplaceDispatcherThreadRunnable = new Runnable() { 316 @Override 317 public void run() { 318 synchronized (sLock) { 319 if (sThread != null) { 320 sThread.quitSafely(); 321 sThread = null; 322 sHandler = null; 323 } 324 } 325 } 326 }; 327 328 /** @hide */ getFontSync(FontRequest request)329 public static Typeface getFontSync(FontRequest request) { 330 final String id = request.getIdentifier(); 331 Typeface cachedTypeface = sTypefaceCache.get(id); 332 if (cachedTypeface != null) { 333 return cachedTypeface; 334 } 335 336 // Unfortunately the typeface is not available at this time, but requesting from the font 337 // provider takes too much time. For now, request the font data to ensure it is in the cache 338 // next time and return. 339 synchronized (sLock) { 340 if (sHandler == null) { 341 sThread = new HandlerThread("fonts", Process.THREAD_PRIORITY_BACKGROUND); 342 sThread.start(); 343 sHandler = new Handler(sThread.getLooper()); 344 } 345 final Lock lock = new ReentrantLock(); 346 final Condition cond = lock.newCondition(); 347 final AtomicReference<Typeface> holder = new AtomicReference<>(); 348 final AtomicBoolean waiting = new AtomicBoolean(true); 349 final AtomicBoolean timeout = new AtomicBoolean(false); 350 351 sHandler.post(() -> { 352 try { 353 FontFamilyResult result = fetchFonts(sContext, null, request); 354 if (result.getStatusCode() == FontFamilyResult.STATUS_OK) { 355 Typeface typeface = buildTypeface(sContext, null, result.getFonts()); 356 if (typeface != null) { 357 sTypefaceCache.put(id, typeface); 358 } 359 holder.set(typeface); 360 } 361 } catch (NameNotFoundException e) { 362 // Ignore. 363 } 364 lock.lock(); 365 try { 366 if (!timeout.get()) { 367 waiting.set(false); 368 cond.signal(); 369 } 370 } finally { 371 lock.unlock(); 372 } 373 }); 374 sHandler.removeCallbacks(sReplaceDispatcherThreadRunnable); 375 sHandler.postDelayed(sReplaceDispatcherThreadRunnable, THREAD_RENEWAL_THRESHOLD_MS); 376 377 long remaining = TimeUnit.MILLISECONDS.toNanos(SYNC_FONT_FETCH_TIMEOUT_MS); 378 lock.lock(); 379 try { 380 if (!waiting.get()) { 381 return holder.get(); 382 } 383 for (;;) { 384 try { 385 remaining = cond.awaitNanos(remaining); 386 } catch (InterruptedException e) { 387 // do nothing. 388 } 389 if (!waiting.get()) { 390 return holder.get(); 391 } 392 if (remaining <= 0) { 393 timeout.set(true); 394 Log.w(TAG, "Remote font fetch timed out: " + 395 request.getProviderAuthority() + "/" + request.getQuery()); 396 return null; 397 } 398 } 399 } finally { 400 lock.unlock(); 401 } 402 } 403 } 404 405 /** 406 * Interface used to receive asynchronously fetched typefaces. 407 */ 408 public static class FontRequestCallback { 409 /** 410 * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given 411 * provider was not found on the device. 412 */ 413 public static final int FAIL_REASON_PROVIDER_NOT_FOUND = -1; 414 /** 415 * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given 416 * provider must be authenticated and the given certificates do not match its signature. 417 */ 418 public static final int FAIL_REASON_WRONG_CERTIFICATES = -2; 419 /** 420 * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font 421 * returned by the provider was not loaded properly. 422 */ 423 public static final int FAIL_REASON_FONT_LOAD_ERROR = -3; 424 /** 425 * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font 426 * provider did not return any results for the given query. 427 */ 428 public static final int FAIL_REASON_FONT_NOT_FOUND = Columns.RESULT_CODE_FONT_NOT_FOUND; 429 /** 430 * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font 431 * provider found the queried font, but it is currently unavailable. 432 */ 433 public static final int FAIL_REASON_FONT_UNAVAILABLE = Columns.RESULT_CODE_FONT_UNAVAILABLE; 434 /** 435 * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given 436 * query was not supported by the provider. 437 */ 438 public static final int FAIL_REASON_MALFORMED_QUERY = Columns.RESULT_CODE_MALFORMED_QUERY; 439 440 /** @hide */ 441 @IntDef({ FAIL_REASON_PROVIDER_NOT_FOUND, FAIL_REASON_FONT_LOAD_ERROR, 442 FAIL_REASON_FONT_NOT_FOUND, FAIL_REASON_FONT_UNAVAILABLE, 443 FAIL_REASON_MALFORMED_QUERY }) 444 @Retention(RetentionPolicy.SOURCE) 445 @interface FontRequestFailReason {} 446 FontRequestCallback()447 public FontRequestCallback() {} 448 449 /** 450 * Called then a Typeface request done via {@link #requestFonts} is complete. Note that this 451 * method will not be called if {@link #onTypefaceRequestFailed(int)} is called instead. 452 * @param typeface The Typeface object retrieved. 453 */ onTypefaceRetrieved(Typeface typeface)454 public void onTypefaceRetrieved(Typeface typeface) {} 455 456 /** 457 * Called when a Typeface request done via {@link #requestFonts}} fails. 458 * @param reason One of {@link #FAIL_REASON_PROVIDER_NOT_FOUND}, 459 * {@link #FAIL_REASON_FONT_NOT_FOUND}, 460 * {@link #FAIL_REASON_FONT_LOAD_ERROR}, 461 * {@link #FAIL_REASON_FONT_UNAVAILABLE} or 462 * {@link #FAIL_REASON_MALFORMED_QUERY} if returned by the system. May also be 463 * a positive value greater than 0 defined by the font provider as an 464 * additional error code. Refer to the provider's documentation for more 465 * information on possible returned error codes. 466 */ onTypefaceRequestFailed(@ontRequestFailReason int reason)467 public void onTypefaceRequestFailed(@FontRequestFailReason int reason) {} 468 } 469 470 /** 471 * Create a typeface object given a font request. The font will be asynchronously fetched, 472 * therefore the result is delivered to the given callback. See {@link FontRequest}. 473 * Only one of the methods in callback will be invoked, depending on whether the request 474 * succeeds or fails. These calls will happen on the caller thread. 475 * 476 * Note that the result Typeface may be cached internally and the same instance will be returned 477 * the next time you call this method with the same request. If you want to bypass this cache, 478 * use {@link #fetchFonts} and {@link #buildTypeface} instead. 479 * 480 * @param context A context to be used for fetching from font provider. 481 * @param request A {@link FontRequest} object that identifies the provider and query for the 482 * request. May not be null. 483 * @param handler A handler to be processed the font fetching. 484 * @param cancellationSignal A signal to cancel the operation in progress, or null if none. If 485 * the operation is canceled, then {@link 486 * android.os.OperationCanceledException} will be thrown. 487 * @param callback A callback that will be triggered when results are obtained. May not be null. 488 */ requestFonts(@onNull Context context, @NonNull FontRequest request, @NonNull Handler handler, @Nullable CancellationSignal cancellationSignal, @NonNull FontRequestCallback callback)489 public static void requestFonts(@NonNull Context context, @NonNull FontRequest request, 490 @NonNull Handler handler, @Nullable CancellationSignal cancellationSignal, 491 @NonNull FontRequestCallback callback) { 492 493 final Handler callerThreadHandler = new Handler(); 494 final Typeface cachedTypeface = sTypefaceCache.get(request.getIdentifier()); 495 if (cachedTypeface != null) { 496 callerThreadHandler.post(() -> callback.onTypefaceRetrieved(cachedTypeface)); 497 return; 498 } 499 500 handler.post(() -> { 501 FontFamilyResult result; 502 try { 503 result = fetchFonts(context, cancellationSignal, request); 504 } catch (NameNotFoundException e) { 505 callerThreadHandler.post(() -> callback.onTypefaceRequestFailed( 506 FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND)); 507 return; 508 } 509 510 // Same request might be dispatched during fetchFonts. Check the cache again. 511 final Typeface anotherCachedTypeface = sTypefaceCache.get(request.getIdentifier()); 512 if (anotherCachedTypeface != null) { 513 callerThreadHandler.post(() -> callback.onTypefaceRetrieved(anotherCachedTypeface)); 514 return; 515 } 516 517 if (result.getStatusCode() != FontFamilyResult.STATUS_OK) { 518 switch (result.getStatusCode()) { 519 case FontFamilyResult.STATUS_WRONG_CERTIFICATES: 520 callerThreadHandler.post(() -> callback.onTypefaceRequestFailed( 521 FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES)); 522 return; 523 case FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED: 524 callerThreadHandler.post(() -> callback.onTypefaceRequestFailed( 525 FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR)); 526 return; 527 default: 528 // fetchFont returns unexpected status type. Fallback to load error. 529 callerThreadHandler.post(() -> callback.onTypefaceRequestFailed( 530 FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR)); 531 return; 532 } 533 } 534 535 final FontInfo[] fonts = result.getFonts(); 536 if (fonts == null || fonts.length == 0) { 537 callerThreadHandler.post(() -> callback.onTypefaceRequestFailed( 538 FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND)); 539 return; 540 } 541 for (final FontInfo font : fonts) { 542 if (font.getResultCode() != Columns.RESULT_CODE_OK) { 543 // We proceed if all font entry is ready to use. Otherwise report the first 544 // error. 545 final int resultCode = font.getResultCode(); 546 if (resultCode < 0) { 547 // Negative values are reserved for internal errors. Fallback to load error. 548 callerThreadHandler.post(() -> callback.onTypefaceRequestFailed( 549 FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR)); 550 } else { 551 callerThreadHandler.post(() -> callback.onTypefaceRequestFailed( 552 resultCode)); 553 } 554 return; 555 } 556 } 557 558 final Typeface typeface = buildTypeface(context, cancellationSignal, fonts); 559 if (typeface == null) { 560 // Something went wrong during reading font files. This happens if the given font 561 // file is an unsupported font type. 562 callerThreadHandler.post(() -> callback.onTypefaceRequestFailed( 563 FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR)); 564 return; 565 } 566 567 sTypefaceCache.put(request.getIdentifier(), typeface); 568 callerThreadHandler.post(() -> callback.onTypefaceRetrieved(typeface)); 569 }); 570 } 571 572 /** 573 * Fetch fonts given a font request. 574 * 575 * @param context A {@link Context} to be used for fetching fonts. 576 * @param cancellationSignal A signal to cancel the operation in progress, or null if none. If 577 * the operation is canceled, then {@link 578 * android.os.OperationCanceledException} will be thrown when the 579 * query is executed. 580 * @param request A {@link FontRequest} object that identifies the provider and query for the 581 * request. 582 * 583 * @return {@link FontFamilyResult} 584 * 585 * @throws NameNotFoundException If requested package or authority was not found in system. 586 */ fetchFonts( @onNull Context context, @Nullable CancellationSignal cancellationSignal, @NonNull FontRequest request)587 public static @NonNull FontFamilyResult fetchFonts( 588 @NonNull Context context, @Nullable CancellationSignal cancellationSignal, 589 @NonNull FontRequest request) throws NameNotFoundException { 590 if (context.isRestricted()) { 591 // TODO: Should we allow if the peer process is system or myself? 592 return new FontFamilyResult(FontFamilyResult.STATUS_REJECTED, null); 593 } 594 ProviderInfo providerInfo = getProvider(context.getPackageManager(), request); 595 if (providerInfo == null) { 596 return new FontFamilyResult(FontFamilyResult.STATUS_WRONG_CERTIFICATES, null); 597 598 } 599 try { 600 FontInfo[] fonts = getFontFromProvider( 601 context, request, providerInfo.authority, cancellationSignal); 602 return new FontFamilyResult(FontFamilyResult.STATUS_OK, fonts); 603 } catch (IllegalArgumentException e) { 604 return new FontFamilyResult(FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED, null); 605 } 606 } 607 608 /** 609 * Build a Typeface from an array of {@link FontInfo} 610 * 611 * Results that are marked as not ready will be skipped. 612 * 613 * @param context A {@link Context} that will be used to fetch the font contents. 614 * @param cancellationSignal A signal to cancel the operation in progress, or null if none. If 615 * the operation is canceled, then {@link 616 * android.os.OperationCanceledException} will be thrown. 617 * @param fonts An array of {@link FontInfo} to be used to create a Typeface. 618 * @return A Typeface object. Returns null if typeface creation fails. 619 */ buildTypeface(@onNull Context context, @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts)620 public static Typeface buildTypeface(@NonNull Context context, 621 @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts) { 622 if (context.isRestricted()) { 623 // TODO: Should we allow if the peer process is system or myself? 624 return null; 625 } 626 final Map<Uri, ByteBuffer> uriBuffer = 627 prepareFontData(context, fonts, cancellationSignal); 628 if (uriBuffer.isEmpty()) { 629 return null; 630 } 631 return new Typeface.Builder(fonts, uriBuffer).build(); 632 } 633 634 /** 635 * A helper function to create a mapping from {@link Uri} to {@link ByteBuffer}. 636 * 637 * Skip if the file contents is not ready to be read. 638 * 639 * @param context A {@link Context} to be used for resolving content URI in 640 * {@link FontInfo}. 641 * @param fonts An array of {@link FontInfo}. 642 * @return A map from {@link Uri} to {@link ByteBuffer}. 643 */ prepareFontData(Context context, FontInfo[] fonts, CancellationSignal cancellationSignal)644 private static Map<Uri, ByteBuffer> prepareFontData(Context context, FontInfo[] fonts, 645 CancellationSignal cancellationSignal) { 646 final HashMap<Uri, ByteBuffer> out = new HashMap<>(); 647 final ContentResolver resolver = context.getContentResolver(); 648 649 for (FontInfo font : fonts) { 650 if (font.getResultCode() != Columns.RESULT_CODE_OK) { 651 continue; 652 } 653 654 final Uri uri = font.getUri(); 655 if (out.containsKey(uri)) { 656 continue; 657 } 658 659 ByteBuffer buffer = null; 660 try (final ParcelFileDescriptor pfd = 661 resolver.openFileDescriptor(uri, "r", cancellationSignal)) { 662 if (pfd != null) { 663 try (final FileInputStream fis = 664 new FileInputStream(pfd.getFileDescriptor())) { 665 final FileChannel fileChannel = fis.getChannel(); 666 final long size = fileChannel.size(); 667 buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size); 668 } catch (IOException e) { 669 // ignore 670 } 671 } 672 } catch (IOException e) { 673 // ignore 674 } 675 676 // TODO: try other approach?, e.g. read all contents instead of mmap. 677 678 out.put(uri, buffer); 679 } 680 return Collections.unmodifiableMap(out); 681 } 682 683 /** @hide */ 684 @VisibleForTesting getProvider( PackageManager packageManager, FontRequest request)685 public static @Nullable ProviderInfo getProvider( 686 PackageManager packageManager, FontRequest request) throws NameNotFoundException { 687 String providerAuthority = request.getProviderAuthority(); 688 ProviderInfo info = packageManager.resolveContentProvider(providerAuthority, 0); 689 if (info == null) { 690 throw new NameNotFoundException("No package found for authority: " + providerAuthority); 691 } 692 693 if (!info.packageName.equals(request.getProviderPackage())) { 694 throw new NameNotFoundException("Found content provider " + providerAuthority 695 + ", but package was not " + request.getProviderPackage()); 696 } 697 // Trust system apps without signature checks 698 if (info.applicationInfo.isSystemApp()) { 699 return info; 700 } 701 702 List<byte[]> signatures; 703 PackageInfo packageInfo = packageManager.getPackageInfo(info.packageName, 704 PackageManager.GET_SIGNATURES); 705 signatures = convertToByteArrayList(packageInfo.signatures); 706 Collections.sort(signatures, sByteArrayComparator); 707 708 List<List<byte[]>> requestCertificatesList = request.getCertificates(); 709 for (int i = 0; i < requestCertificatesList.size(); ++i) { 710 // Make a copy so we can sort it without modifying the incoming data. 711 List<byte[]> requestSignatures = new ArrayList<>(requestCertificatesList.get(i)); 712 Collections.sort(requestSignatures, sByteArrayComparator); 713 if (equalsByteArrayList(signatures, requestSignatures)) { 714 return info; 715 } 716 } 717 return null; 718 } 719 720 private static final Comparator<byte[]> sByteArrayComparator = (l, r) -> { 721 if (l.length != r.length) { 722 return l.length - r.length; 723 } 724 for (int i = 0; i < l.length; ++i) { 725 if (l[i] != r[i]) { 726 return l[i] - r[i]; 727 } 728 } 729 return 0; 730 }; 731 equalsByteArrayList( List<byte[]> signatures, List<byte[]> requestSignatures)732 private static boolean equalsByteArrayList( 733 List<byte[]> signatures, List<byte[]> requestSignatures) { 734 if (signatures.size() != requestSignatures.size()) { 735 return false; 736 } 737 for (int i = 0; i < signatures.size(); ++i) { 738 if (!Arrays.equals(signatures.get(i), requestSignatures.get(i))) { 739 return false; 740 } 741 } 742 return true; 743 } 744 convertToByteArrayList(Signature[] signatures)745 private static List<byte[]> convertToByteArrayList(Signature[] signatures) { 746 List<byte[]> shas = new ArrayList<>(); 747 for (int i = 0; i < signatures.length; ++i) { 748 shas.add(signatures[i].toByteArray()); 749 } 750 return shas; 751 } 752 753 /** @hide */ 754 @VisibleForTesting getFontFromProvider( Context context, FontRequest request, String authority, CancellationSignal cancellationSignal)755 public static @NonNull FontInfo[] getFontFromProvider( 756 Context context, FontRequest request, String authority, 757 CancellationSignal cancellationSignal) { 758 ArrayList<FontInfo> result = new ArrayList<>(); 759 final Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 760 .authority(authority) 761 .build(); 762 final Uri fileBaseUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT) 763 .authority(authority) 764 .appendPath("file") 765 .build(); 766 try (Cursor cursor = context.getContentResolver().query(uri, new String[] { Columns._ID, 767 Columns.FILE_ID, Columns.TTC_INDEX, Columns.VARIATION_SETTINGS, 768 Columns.WEIGHT, Columns.ITALIC, Columns.RESULT_CODE }, 769 "query = ?", new String[] { request.getQuery() }, null, cancellationSignal);) { 770 // TODO: Should we restrict the amount of fonts that can be returned? 771 // TODO: Write documentation explaining that all results should be from the same family. 772 if (cursor != null && cursor.getCount() > 0) { 773 final int resultCodeColumnIndex = cursor.getColumnIndex(Columns.RESULT_CODE); 774 result = new ArrayList<>(); 775 final int idColumnIndex = cursor.getColumnIndexOrThrow(Columns._ID); 776 final int fileIdColumnIndex = cursor.getColumnIndex(Columns.FILE_ID); 777 final int ttcIndexColumnIndex = cursor.getColumnIndex(Columns.TTC_INDEX); 778 final int vsColumnIndex = cursor.getColumnIndex(Columns.VARIATION_SETTINGS); 779 final int weightColumnIndex = cursor.getColumnIndex(Columns.WEIGHT); 780 final int italicColumnIndex = cursor.getColumnIndex(Columns.ITALIC); 781 while (cursor.moveToNext()) { 782 int resultCode = resultCodeColumnIndex != -1 783 ? cursor.getInt(resultCodeColumnIndex) : Columns.RESULT_CODE_OK; 784 final int ttcIndex = ttcIndexColumnIndex != -1 785 ? cursor.getInt(ttcIndexColumnIndex) : 0; 786 final String variationSettings = vsColumnIndex != -1 787 ? cursor.getString(vsColumnIndex) : null; 788 789 Uri fileUri; 790 if (fileIdColumnIndex == -1) { 791 long id = cursor.getLong(idColumnIndex); 792 fileUri = ContentUris.withAppendedId(uri, id); 793 } else { 794 long id = cursor.getLong(fileIdColumnIndex); 795 fileUri = ContentUris.withAppendedId(fileBaseUri, id); 796 } 797 int weight; 798 boolean italic; 799 if (weightColumnIndex != -1 && italicColumnIndex != -1) { 800 weight = cursor.getInt(weightColumnIndex); 801 italic = cursor.getInt(italicColumnIndex) == 1; 802 } else { 803 weight = Typeface.Builder.NORMAL_WEIGHT; 804 italic = false; 805 } 806 FontVariationAxis[] axes = 807 FontVariationAxis.fromFontVariationSettings(variationSettings); 808 result.add(new FontInfo(fileUri, ttcIndex, axes, weight, italic, resultCode)); 809 } 810 } 811 } 812 return result.toArray(new FontInfo[0]); 813 } 814 } 815