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