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