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