content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  package android.provider;
  
  import android.annotation.IntDef;
  import android.annotation.IntRange;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.content.ContentResolver;
  import android.content.ContentUris;
  import android.content.Context;
  import android.content.pm.PackageInfo;
  import android.content.pm.PackageManager;
  import android.content.pm.PackageManager.NameNotFoundException;
  import android.content.pm.ProviderInfo;
  import android.content.pm.Signature;
  import android.database.Cursor;
  import android.graphics.Typeface;
  import android.graphics.fonts.Font;
  import android.graphics.fonts.FontFamily;
  import android.graphics.fonts.FontStyle;
  import android.graphics.fonts.FontVariationAxis;
  import android.net.Uri;
  import android.os.CancellationSignal;
  import android.os.Handler;
  import android.os.HandlerThread;
  import android.os.ParcelFileDescriptor;
  import android.os.Process;
  import android.util.Log;
  import android.util.LruCache;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.Preconditions;
  
  import java.io.FileInputStream;
  import java.io.IOException;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.nio.ByteBuffer;
  import java.nio.channels.FileChannel;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.Comparator;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.atomic.AtomicBoolean;
  import java.util.concurrent.atomic.AtomicReference;
  import java.util.concurrent.locks.Condition;
  import java.util.concurrent.locks.Lock;
  import java.util.concurrent.locks.ReentrantLock;
  
  /**
   * Utility class to deal with Font ContentProviders.
   */
  public class FontsContract {
      private static final String TAG = "FontsContract";
  
      /**
       * Defines the constants used in a response from a Font Provider. The cursor returned from the
       * query should have the ID column populated with the content uri ID for the resulting font.
       * This should point to a real file or shared memory, as the client will mmap the given file
       * descriptor. Pipes, sockets and other non-mmap-able file descriptors will fail to load in the
       * client application.
       */
      public static final class Columns implements BaseColumns {
  
          // Do not instantiate.
          private Columns() {}
  
          /**
           * Constant used to request data from a font provider. The cursor returned from the query
           * may populate this column with a long for the font file ID. The client will request a file
           * descriptor to "file/FILE_ID" with this ID immediately under the top-level content URI. If
           * not present, the client will request a file descriptor to the top-level URI with the
           * given base font ID. Note that several results may return the same file ID, e.g. for TTC
           * files with different indices.
           */
          public static final String FILE_ID = "file_id";
          /**
           * Constant used to request data from a font provider. The cursor returned from the query
           * should have this column populated with an int for the ttc index for the resulting font.
           */
          public static final String TTC_INDEX = "font_ttc_index";
          /**
           * Constant used to request data from a font provider. The cursor returned from the query
           * may populate this column with the font variation settings String information for the
           * font.
           */
          public static final String VARIATION_SETTINGS = "font_variation_settings";
          /**
           * Constant used to request data from a font provider. The cursor returned from the query
           * should have this column populated with the int weight for the resulting font. This value
           * should be between 100 and 900. The most common values are 400 for regular weight and 700
           * for bold weight.
           */
          public static final String WEIGHT = "font_weight";
          /**
           * Constant used to request data from a font provider. The cursor returned from the query
           * should have this column populated with the int italic for the resulting font. This should
           * be 0 for regular style and 1 for italic.
           */
          public static final String ITALIC = "font_italic";
          /**
           * Constant used to request data from a font provider. The cursor returned from the query
           * should have this column populated to indicate the result status of the
           * query. This will be checked before any other data in the cursor. Possible values are
           * {@link #RESULT_CODE_OK}, {@link #RESULT_CODE_FONT_NOT_FOUND},
           * {@link #RESULT_CODE_MALFORMED_QUERY} and {@link #RESULT_CODE_FONT_UNAVAILABLE} for system
           * defined values. You may also define your own values in the 0x000010000..0xFFFF0000 range.
           * If not present, {@link #RESULT_CODE_OK} will be assumed.
           */
          public static final String RESULT_CODE = "result_code";
  
          /**
           * Constant used to represent a result was retrieved successfully. The given fonts will be
           * attempted to retrieve immediately via
           * {@link android.content.ContentProvider#openFile(Uri, String)}. See {@link #RESULT_CODE}.
           */
          public static final int RESULT_CODE_OK = 0;
          /**
           * Constant used to represent a result was not found. See {@link #RESULT_CODE}.
           */
          public static final int RESULT_CODE_FONT_NOT_FOUND = 1;
          /**
           * Constant used to represent a result was found, but cannot be provided at this moment. Use
           * this to indicate, for example, that a font needs to be fetched from the network. See
           * {@link #RESULT_CODE}.
           */
          public static final int RESULT_CODE_FONT_UNAVAILABLE = 2;
          /**
           * Constant used to represent that the query was not in a supported format by the provider.
           * See {@link #RESULT_CODE}.
           */
          public static final int RESULT_CODE_MALFORMED_QUERY = 3;
      }
  
      private static final Object sLock = new Object();
      @GuardedBy("sLock")
      private static Handler sHandler;
      @GuardedBy("sLock")
      private static HandlerThread sThread;
      @GuardedBy("sLock")
      private static Set<String> sInQueueSet;
  
      private volatile static Context sContext;  // set once in setApplicationContextForResources
  
      private static final LruCache<String, Typeface> sTypefaceCache = new LruCache<>(16);
  
      private FontsContract() {
      }
  
      /** @hide */
      public static void setApplicationContextForResources(Context context) {
          sContext = context.getApplicationContext();
      }
  
      /**
       * Object represent a font entry in the family returned from {@link #fetchFonts}.
       */
      public static class FontInfo {
          private final Uri mUri;
          private final int mTtcIndex;
          private final FontVariationAxis[] mAxes;
          private final int mWeight;
          private final boolean mItalic;
          private final int mResultCode;
  
          /**
           * Creates a Font with all the information needed about a provided font.
           * @param uri A URI associated to the font file.
           * @param ttcIndex If providing a TTC_INDEX file, the index to point to. Otherwise, 0.
           * @param axes If providing a variation font, the settings for it. May be null.
           * @param weight An integer that indicates the font weight.
           * @param italic A boolean that indicates the font is italic style or not.
           * @param resultCode A boolean that indicates the font contents is ready.
           */
          /** @hide */
          public FontInfo(@NonNull Uri uri, @IntRange(from = 0) int ttcIndex,
                  @Nullable FontVariationAxis[] axes, @IntRange(from = 1, to = 1000) int weight,
                  boolean italic, int resultCode) {
              mUri = Preconditions.checkNotNull(uri);
              mTtcIndex = ttcIndex;
              mAxes = axes;
              mWeight = weight;
              mItalic = italic;
              mResultCode = resultCode;
          }
  
          /**
           * Returns a URI associated to this record.
           */
          public @NonNull Uri getUri() {
              return mUri;
          }
  
          /**
           * Returns the index to be used to access this font when accessing a TTC file.
           */
          public @IntRange(from = 0) int getTtcIndex() {
              return mTtcIndex;
          }
  
          /**
           * Returns the list of axes associated to this font.
           */
          public @Nullable FontVariationAxis[] getAxes() {
              return mAxes;
          }
  
          /**
           * Returns the weight value for this font.
           */
          public @IntRange(from = 1, to = 1000) int getWeight() {
              return mWeight;
          }
  
          /**
           * Returns whether this font is italic.
           */
          public boolean isItalic() {
              return mItalic;
          }
  
          /**
           * Returns result code.
           *
           * {@link FontsContract.Columns#RESULT_CODE}
           */
          public int getResultCode() {
              return mResultCode;
          }
      }
  
      /**
       * Object returned from {@link #fetchFonts}.
       */
      public static class FontFamilyResult {
          /**
           * Constant represents that the font was successfully retrieved. Note that when this value
           * is set and {@link #getFonts} returns an empty array, it means there were no fonts
           * matching the given query.
           */
          public static final int STATUS_OK = 0;
  
          /**
           * Constant represents that the given certificate was not matched with the provider's
           * signature. {@link #getFonts} returns null if this status was set.
           */
          public static final int STATUS_WRONG_CERTIFICATES = 1;
  
          /**
           * Constant represents that the provider returns unexpected data. {@link #getFonts} returns
           * null if this status was set. For example, this value is set when the font provider
           * gives invalid format of variation settings.
           */
          public static final int STATUS_UNEXPECTED_DATA_PROVIDED = 2;
  
          /**
           * Constant represents that the fetching font data was rejected by system. This happens if
           * the passed context is restricted.
           */
          public static final int STATUS_REJECTED = 3;
  
          /** @hide */
          @IntDef(prefix = { "STATUS_" }, value = {
                  STATUS_OK,
                  STATUS_WRONG_CERTIFICATES,
                  STATUS_UNEXPECTED_DATA_PROVIDED
          })
          @Retention(RetentionPolicy.SOURCE)
          @interface FontResultStatus {}
  
          private final @FontResultStatus int mStatusCode;
          private final FontInfo[] mFonts;
  
          /** @hide */
          public FontFamilyResult(@FontResultStatus int statusCode, @Nullable FontInfo[] fonts) {
              mStatusCode = statusCode;
              mFonts = fonts;
          }
  
          public @FontResultStatus int getStatusCode() {
              return mStatusCode;
          }
  
          public @NonNull FontInfo[] getFonts() {
              return mFonts;
          }
      }
  
      private static final int THREAD_RENEWAL_THRESHOLD_MS = 10000;
  
      private static final long SYNC_FONT_FETCH_TIMEOUT_MS = 500;
  
      // We use a background thread to post the content resolving work for all requests on. This
      // thread should be quit/stopped after all requests are done.
      // TODO: Factor out to other class. Consider to switch MessageQueue.IdleHandler.
      private static final Runnable sReplaceDispatcherThreadRunnable = new Runnable() {
          @Override
          public void run() {
              synchronized (sLock) {
                  if (sThread != null) {
                      sThread.quitSafely();
                      sThread = null;
                      sHandler = null;
                  }
              }
          }
      };
  
      /** @hide */
      public static Typeface getFontSync(FontRequest request) {
          final String id = request.getIdentifier();
          Typeface cachedTypeface = sTypefaceCache.get(id);
          if (cachedTypeface != null) {
              return cachedTypeface;
          }
  
          // Unfortunately the typeface is not available at this time, but requesting from the font
          // provider takes too much time. For now, request the font data to ensure it is in the cache
          // next time and return.
          synchronized (sLock) {
              if (sHandler == null) {
                  sThread = new HandlerThread("fonts", Process.THREAD_PRIORITY_BACKGROUND);
                  sThread.start();
                  sHandler = new Handler(sThread.getLooper());
              }
              final Lock lock = new ReentrantLock();
              final Condition cond = lock.newCondition();
              final AtomicReference<Typeface> holder = new AtomicReference<>();
              final AtomicBoolean waiting = new AtomicBoolean(true);
              final AtomicBoolean timeout = new AtomicBoolean(false);
  
              sHandler.post(() -> {
                  try {
                      FontFamilyResult result = fetchFonts(sContext, null, request);
                      if (result.getStatusCode() == FontFamilyResult.STATUS_OK) {
                          Typeface typeface = buildTypeface(sContext, null, result.getFonts());
                          if (typeface != null) {
                              sTypefaceCache.put(id, typeface);
                          }
                          holder.set(typeface);
                      }
                  } catch (NameNotFoundException e) {
                      // Ignore.
                  }
                  lock.lock();
                  try {
                      if (!timeout.get()) {
                        waiting.set(false);
                        cond.signal();
                      }
                  } finally {
                      lock.unlock();
                  }
              });
              sHandler.removeCallbacks(sReplaceDispatcherThreadRunnable);
              sHandler.postDelayed(sReplaceDispatcherThreadRunnable, THREAD_RENEWAL_THRESHOLD_MS);
  
              long remaining = TimeUnit.MILLISECONDS.toNanos(SYNC_FONT_FETCH_TIMEOUT_MS);
              lock.lock();
              try {
                  if (!waiting.get()) {
                      return holder.get();
                  }
                  for (;;) {
                      try {
                          remaining = cond.awaitNanos(remaining);
                      } catch (InterruptedException e) {
                          // do nothing.
                      }
                      if (!waiting.get()) {
                          return holder.get();
                      }
                      if (remaining <= 0) {
                          timeout.set(true);
                          Log.w(TAG, "Remote font fetch timed out: " +
                                  request.getProviderAuthority() + "/" + request.getQuery());
                          return null;
                      }
                  }
              } finally {
                  lock.unlock();
              }
          }
      }
  
      /**
       * Interface used to receive asynchronously fetched typefaces.
       */
      public static class FontRequestCallback {
          /**
           * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
           * provider was not found on the device.
           */
          public static final int FAIL_REASON_PROVIDER_NOT_FOUND = -1;
          /**
           * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
           * provider must be authenticated and the given certificates do not match its signature.
           */
          public static final int FAIL_REASON_WRONG_CERTIFICATES = -2;
          /**
           * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
           * returned by the provider was not loaded properly.
           */
          public static final int FAIL_REASON_FONT_LOAD_ERROR = -3;
          /**
           * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
           * provider did not return any results for the given query.
           */
          public static final int FAIL_REASON_FONT_NOT_FOUND = Columns.RESULT_CODE_FONT_NOT_FOUND;
          /**
           * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
           * provider found the queried font, but it is currently unavailable.
           */
          public static final int FAIL_REASON_FONT_UNAVAILABLE = Columns.RESULT_CODE_FONT_UNAVAILABLE;
          /**
           * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
           * query was not supported by the provider.
           */
          public static final int FAIL_REASON_MALFORMED_QUERY = Columns.RESULT_CODE_MALFORMED_QUERY;
  
          /** @hide */
          @IntDef(prefix = { "FAIL_" }, value = {
                  FAIL_REASON_PROVIDER_NOT_FOUND,
                  FAIL_REASON_FONT_LOAD_ERROR,
                  FAIL_REASON_FONT_NOT_FOUND,
                  FAIL_REASON_FONT_UNAVAILABLE,
                  FAIL_REASON_MALFORMED_QUERY
          })
          @Retention(RetentionPolicy.SOURCE)
          @interface FontRequestFailReason {}
  
          public FontRequestCallback() {}
  
          /**
           * Called then a Typeface request done via {@link #requestFonts} is complete. Note that this
           * method will not be called if {@link #onTypefaceRequestFailed(int)} is called instead.
           * @param typeface  The Typeface object retrieved.
           */
          public void onTypefaceRetrieved(Typeface typeface) {}
  
          /**
           * Called when a Typeface request done via {@link #requestFonts}} fails.
           * @param reason One of {@link #FAIL_REASON_PROVIDER_NOT_FOUND},
           *               {@link #FAIL_REASON_FONT_NOT_FOUND},
           *               {@link #FAIL_REASON_FONT_LOAD_ERROR},
           *               {@link #FAIL_REASON_FONT_UNAVAILABLE} or
           *               {@link #FAIL_REASON_MALFORMED_QUERY} if returned by the system. May also be
           *               a positive value greater than 0 defined by the font provider as an
           *               additional error code. Refer to the provider's documentation for more
           *               information on possible returned error codes.
           */
          public void onTypefaceRequestFailed(@FontRequestFailReason int reason) {}
      }
  
      /**
       * Create a typeface object given a font request. The font will be asynchronously fetched,
       * therefore the result is delivered to the given callback. See {@link FontRequest}.
       * Only one of the methods in callback will be invoked, depending on whether the request
       * succeeds or fails. These calls will happen on the caller thread.
       *
       * Note that the result Typeface may be cached internally and the same instance will be returned
       * the next time you call this method with the same request. If you want to bypass this cache,
       * use {@link #fetchFonts} and {@link #buildTypeface} instead.
       *
       * @param context A context to be used for fetching from font provider.
       * @param request A {@link FontRequest} object that identifies the provider and query for the
       *                request. May not be null.
       * @param handler A handler to be processed the font fetching.
       * @param cancellationSignal A signal to cancel the operation in progress, or null if none. If
       *                           the operation is canceled, then {@link
       *                           android.os.OperationCanceledException} will be thrown.
       * @param callback A callback that will be triggered when results are obtained. May not be null.
       */
      public static void requestFonts(@NonNull Context context, @NonNull FontRequest request,
              @NonNull Handler handler, @Nullable CancellationSignal cancellationSignal,
              @NonNull FontRequestCallback callback) {
  
          final Handler callerThreadHandler = new Handler();
          final Typeface cachedTypeface = sTypefaceCache.get(request.getIdentifier());
          if (cachedTypeface != null) {
              callerThreadHandler.post(() -> callback.onTypefaceRetrieved(cachedTypeface));
              return;
          }
  
          handler.post(() -> {
              FontFamilyResult result;
              try {
                  result = fetchFonts(context, cancellationSignal, request);
              } catch (NameNotFoundException e) {
                  callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
                          FontRequestCallback.FAIL_REASON_PROVIDER_NOT_FOUND));
                  return;
              }
  
              // Same request might be dispatched during fetchFonts. Check the cache again.
              final Typeface anotherCachedTypeface = sTypefaceCache.get(request.getIdentifier());
              if (anotherCachedTypeface != null) {
                  callerThreadHandler.post(() -> callback.onTypefaceRetrieved(anotherCachedTypeface));
                  return;
              }
  
              if (result.getStatusCode() != FontFamilyResult.STATUS_OK) {
                  switch (result.getStatusCode()) {
                      case FontFamilyResult.STATUS_WRONG_CERTIFICATES:
                          callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
                                  FontRequestCallback.FAIL_REASON_WRONG_CERTIFICATES));
                          return;
                      case FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED:
                          callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
                                  FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
                          return;
                      default:
                          // fetchFont returns unexpected status type. Fallback to load error.
                          callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
                                  FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
                          return;
                  }
              }
  
              final FontInfo[] fonts = result.getFonts();
              if (fonts == null || fonts.length == 0) {
                  callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
                          FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND));
                  return;
              }
              for (final FontInfo font : fonts) {
                  if (font.getResultCode() != Columns.RESULT_CODE_OK) {
                      // We proceed if all font entry is ready to use. Otherwise report the first
                      // error.
                      final int resultCode = font.getResultCode();
                      if (resultCode < 0) {
                          // Negative values are reserved for internal errors. Fallback to load error.
                          callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
                                  FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
                      } else {
                          callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
                                  resultCode));
                      }
                      return;
                  }
              }
  
              final Typeface typeface = buildTypeface(context, cancellationSignal, fonts);
              if (typeface == null) {
                  // Something went wrong during reading font files. This happens if the given font
                  // file is an unsupported font type.
                  callerThreadHandler.post(() -> callback.onTypefaceRequestFailed(
                          FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR));
                  return;
              }
  
              sTypefaceCache.put(request.getIdentifier(), typeface);
              callerThreadHandler.post(() -> callback.onTypefaceRetrieved(typeface));
          });
      }
  
      /**
       * Fetch fonts given a font request.
       *
       * @param context A {@link Context} to be used for fetching fonts.
       * @param cancellationSignal A signal to cancel the operation in progress, or null if none. If
       *                           the operation is canceled, then {@link
       *                           android.os.OperationCanceledException} will be thrown when the
       *                           query is executed.
       * @param request A {@link FontRequest} object that identifies the provider and query for the
       *                request.
       *
       * @return {@link FontFamilyResult}
       *
       * @throws NameNotFoundException If requested package or authority was not found in system.
       */
      public static @NonNull FontFamilyResult fetchFonts(
              @NonNull Context context, @Nullable CancellationSignal cancellationSignal,
              @NonNull FontRequest request) throws NameNotFoundException {
          if (context.isRestricted()) {
              // TODO: Should we allow if the peer process is system or myself?
              return new FontFamilyResult(FontFamilyResult.STATUS_REJECTED, null);
          }
          ProviderInfo providerInfo = getProvider(context.getPackageManager(), request);
          if (providerInfo == null) {
              return new FontFamilyResult(FontFamilyResult.STATUS_WRONG_CERTIFICATES, null);
  
          }
          try {
              FontInfo[] fonts = getFontFromProvider(
                      context, request, providerInfo.authority, cancellationSignal);
              return new FontFamilyResult(FontFamilyResult.STATUS_OK, fonts);
          } catch (IllegalArgumentException e) {
              return new FontFamilyResult(FontFamilyResult.STATUS_UNEXPECTED_DATA_PROVIDED, null);
          }
      }
  
      /**
       * Build a Typeface from an array of {@link FontInfo}
       *
       * Results that are marked as not ready will be skipped.
       *
       * @param context A {@link Context} that will be used to fetch the font contents.
       * @param cancellationSignal A signal to cancel the operation in progress, or null if none. If
       *                           the operation is canceled, then {@link
       *                           android.os.OperationCanceledException} will be thrown.
       * @param fonts An array of {@link FontInfo} to be used to create a Typeface.
       * @return A Typeface object. Returns null if typeface creation fails.
       */
      public static Typeface buildTypeface(@NonNull Context context,
              @Nullable CancellationSignal cancellationSignal, @NonNull FontInfo[] fonts) {
          if (context.isRestricted()) {
              // TODO: Should we allow if the peer process is system or myself?
              return null;
          }
          final Map<Uri, ByteBuffer> uriBuffer =
                  prepareFontData(context, fonts, cancellationSignal);
          if (uriBuffer.isEmpty()) {
              return null;
          }
  
          FontFamily.Builder familyBuilder = null;
          for (FontInfo fontInfo : fonts) {
              final ByteBuffer buffer = uriBuffer.get(fontInfo.getUri());
              if (buffer == null) {
                  continue;
              }
              try {
                  final Font font = new Font.Builder(buffer)
                          .setWeight(fontInfo.getWeight())
                          .setSlant(fontInfo.isItalic()
                                  ? FontStyle.FONT_SLANT_ITALIC : FontStyle.FONT_SLANT_UPRIGHT)
                          .setTtcIndex(fontInfo.getTtcIndex())
                          .setFontVariationSettings(fontInfo.getAxes())
                          .build();
                  if (familyBuilder == null) {
                      familyBuilder = new FontFamily.Builder(font);
                  } else {
                      familyBuilder.addFont(font);
                  }
              } catch (IllegalArgumentException e) {
                  // To be a compatible behavior with API28 or before, catch IllegalArgumentExcetpion
                  // thrown by native code and returns null.
                  return null;
              } catch (IOException e) {
                  continue;
              }
          }
          if (familyBuilder == null) {
              return null;
          }
  
          final FontFamily family = familyBuilder.build();
  
          final FontStyle normal = new FontStyle(FontStyle.FONT_WEIGHT_NORMAL,
                  FontStyle.FONT_SLANT_UPRIGHT);
          Font bestFont = family.getFont(0);
          int bestScore = normal.getMatchScore(bestFont.getStyle());
          for (int i = 1; i < family.getSize(); ++i) {
              final Font candidate = family.getFont(i);
              final int score = normal.getMatchScore(candidate.getStyle());
              if (score < bestScore) {
                  bestFont = candidate;
                  bestScore = score;
              }
          }
          return new Typeface.CustomFallbackBuilder(family).setStyle(bestFont.getStyle()).build();
      }
  
      /**
       * A helper function to create a mapping from {@link Uri} to {@link ByteBuffer}.
       *
       * Skip if the file contents is not ready to be read.
       *
       * @param context A {@link Context} to be used for resolving content URI in
       *                {@link FontInfo}.
       * @param fonts An array of {@link FontInfo}.
       * @return A map from {@link Uri} to {@link ByteBuffer}.
       */
      private static Map<Uri, ByteBuffer> prepareFontData(Context context, FontInfo[] fonts,
              CancellationSignal cancellationSignal) {
          final HashMap<Uri, ByteBuffer> out = new HashMap<>();
          final ContentResolver resolver = context.getContentResolver();
  
          for (FontInfo font : fonts) {
              if (font.getResultCode() != Columns.RESULT_CODE_OK) {
                  continue;
              }
  
              final Uri uri = font.getUri();
              if (out.containsKey(uri)) {
                  continue;
              }
  
              ByteBuffer buffer = null;
              try (final ParcelFileDescriptor pfd =
                      resolver.openFileDescriptor(uri, "r", cancellationSignal)) {
                  if (pfd != null) {
                      try (final FileInputStream fis =
                              new FileInputStream(pfd.getFileDescriptor())) {
                          final FileChannel fileChannel = fis.getChannel();
                          final long size = fileChannel.size();
                          buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, size);
                      } catch (IOException e) {
                          // ignore
                      }
                  }
              } catch (IOException e) {
                  // ignore
              }
  
              // TODO: try other approach?, e.g. read all contents instead of mmap.
  
              out.put(uri, buffer);
          }
          return Collections.unmodifiableMap(out);
      }
  
      /** @hide */
      @VisibleForTesting
      public static @Nullable ProviderInfo getProvider(
              PackageManager packageManager, FontRequest request) throws NameNotFoundException {
          String providerAuthority = request.getProviderAuthority();
          ProviderInfo info = packageManager.resolveContentProvider(providerAuthority, 0);
          if (info == null) {
              throw new NameNotFoundException("No package found for authority: " + providerAuthority);
          }
  
          if (!info.packageName.equals(request.getProviderPackage())) {
              throw new NameNotFoundException("Found content provider " + providerAuthority
                      + ", but package was not " + request.getProviderPackage());
          }
          // Trust system apps without signature checks
          if (info.applicationInfo.isSystemApp()) {
              return info;
          }
  
          List<byte[]> signatures;
          PackageInfo packageInfo = packageManager.getPackageInfo(info.packageName,
                  PackageManager.GET_SIGNATURES);
          signatures = convertToByteArrayList(packageInfo.signatures);
          Collections.sort(signatures, sByteArrayComparator);
  
          List<List<byte[]>> requestCertificatesList = request.getCertificates();
          for (int i = 0; i < requestCertificatesList.size(); ++i) {
              // Make a copy so we can sort it without modifying the incoming data.
              List<byte[]> requestSignatures = new ArrayList<>(requestCertificatesList.get(i));
              Collections.sort(requestSignatures, sByteArrayComparator);
              if (equalsByteArrayList(signatures, requestSignatures)) {
                  return info;
              }
          }
          return null;
      }
  
      private static final Comparator<byte[]> sByteArrayComparator = (l, r) -> {
          if (l.length != r.length) {
              return l.length - r.length;
          }
          for (int i = 0; i < l.length; ++i) {
              if (l[i] != r[i]) {
                  return l[i] - r[i];
              }
          }
          return 0;
      };
  
      private static boolean equalsByteArrayList(
              List<byte[]> signatures, List<byte[]> requestSignatures) {
          if (signatures.size() != requestSignatures.size()) {
              return false;
          }
          for (int i = 0; i < signatures.size(); ++i) {
              if (!Arrays.equals(signatures.get(i), requestSignatures.get(i))) {
                  return false;
              }
          }
          return true;
      }
  
      private static List<byte[]> convertToByteArrayList(Signature[] signatures) {
          List<byte[]> shas = new ArrayList<>();
          for (int i = 0; i < signatures.length; ++i) {
              shas.add(signatures[i].toByteArray());
          }
          return shas;
      }
  
      /** @hide */
      @VisibleForTesting
      public static @NonNull FontInfo[] getFontFromProvider(
              Context context, FontRequest request, String authority,
              CancellationSignal cancellationSignal) {
          ArrayList<FontInfo> result = new ArrayList<>();
          final Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                  .authority(authority)
                  .build();
          final Uri fileBaseUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
                  .authority(authority)
                  .appendPath("file")
                  .build();
          try (Cursor cursor = context.getContentResolver().query(uri, new String[] { Columns._ID,
                          Columns.FILE_ID, Columns.TTC_INDEX, Columns.VARIATION_SETTINGS,
                          Columns.WEIGHT, Columns.ITALIC, Columns.RESULT_CODE },
                  "query = ?", new String[] { request.getQuery() }, null, cancellationSignal);) {
              // TODO: Should we restrict the amount of fonts that can be returned?
              // TODO: Write documentation explaining that all results should be from the same family.
              if (cursor != null && cursor.getCount() > 0) {
                  final int resultCodeColumnIndex = cursor.getColumnIndex(Columns.RESULT_CODE);
                  result = new ArrayList<>();
                  final int idColumnIndex = cursor.getColumnIndexOrThrow(Columns._ID);
                  final int fileIdColumnIndex = cursor.getColumnIndex(Columns.FILE_ID);
                  final int ttcIndexColumnIndex = cursor.getColumnIndex(Columns.TTC_INDEX);
                  final int vsColumnIndex = cursor.getColumnIndex(Columns.VARIATION_SETTINGS);
                  final int weightColumnIndex = cursor.getColumnIndex(Columns.WEIGHT);
                  final int italicColumnIndex = cursor.getColumnIndex(Columns.ITALIC);
                  while (cursor.moveToNext()) {
                      int resultCode = resultCodeColumnIndex != -1
                              ? cursor.getInt(resultCodeColumnIndex) : Columns.RESULT_CODE_OK;
                      final int ttcIndex = ttcIndexColumnIndex != -1
                              ? cursor.getInt(ttcIndexColumnIndex) : 0;
                      final String variationSettings = vsColumnIndex != -1
                              ? cursor.getString(vsColumnIndex) : null;
  
                      Uri fileUri;
                      if (fileIdColumnIndex == -1) {
                          long id = cursor.getLong(idColumnIndex);
                          fileUri = ContentUris.withAppendedId(uri, id);
                      } else {
                          long id = cursor.getLong(fileIdColumnIndex);
                          fileUri = ContentUris.withAppendedId(fileBaseUri, id);
                      }
                      int weight;
                      boolean italic;
                      if (weightColumnIndex != -1 && italicColumnIndex != -1) {
                          weight = cursor.getInt(weightColumnIndex);
                          italic = cursor.getInt(italicColumnIndex) == 1;
                      } else {
                          weight = Typeface.Builder.NORMAL_WEIGHT;
                          italic = false;
                      }
                      FontVariationAxis[] axes =
                              FontVariationAxis.fromFontVariationSettings(variationSettings);
                      result.add(new FontInfo(fileUri, ttcIndex, axes, weight, italic, resultCode));
                  }
              }
          }
          return result.toArray(new FontInfo[0]);
      }
  }
  