content: /*
   * Copyright 2018 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.graphics.fonts;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.graphics.FontListParser;
  import android.text.FontConfig;
  import android.util.ArrayMap;
  import android.util.Log;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.ArrayUtils;
  
  import org.xmlpull.v1.XmlPullParserException;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.IOException;
  import java.nio.ByteBuffer;
  import java.nio.channels.FileChannel;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  /**
   * Provides the system font configurations.
   */
  public final class SystemFonts {
      private static final String TAG = "SystemFonts";
      private static final String DEFAULT_FAMILY = "sans-serif";
  
      private SystemFonts() {}  // Do not instansiate.
  
      private static final Map<String, FontFamily[]> sSystemFallbackMap;
      private static final FontConfig.Alias[] sAliases;
      private static final List<Font> sAvailableFonts;
  
      /**
       * Returns all available font files in the system.
       *
       * @return a set of system fonts
       */
      public static @NonNull Set<Font> getAvailableFonts() {
          HashSet<Font> set = new HashSet<>();
          set.addAll(sAvailableFonts);
          return set;
      }
  
      /**
       * Returns fallback list for the given family name.
       *
       * If no fallback found for the given family name, returns fallback for the default family.
       *
       * @param familyName family name, e.g. "serif"
       * @hide
       */
      public static @NonNull FontFamily[] getSystemFallback(@Nullable String familyName) {
          final FontFamily[] families = sSystemFallbackMap.get(familyName);
          return families == null ? sSystemFallbackMap.get(DEFAULT_FAMILY) : families;
      }
  
      /**
       * Returns raw system fallback map.
       *
       * This method is intended to be used only by Typeface static initializer.
       * @hide
       */
      public static @NonNull Map<String, FontFamily[]> getRawSystemFallbackMap() {
          return sSystemFallbackMap;
      }
  
      /**
       * Returns a list of aliases.
       *
       * This method is intended to be used only by Typeface static initializer.
       * @hide
       */
      public static @NonNull FontConfig.Alias[] getAliases() {
          return sAliases;
      }
  
      private static @Nullable ByteBuffer mmap(@NonNull String fullPath) {
          try (FileInputStream file = new FileInputStream(fullPath)) {
              final FileChannel fileChannel = file.getChannel();
              final long fontSize = fileChannel.size();
              return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
          } catch (IOException e) {
              Log.e(TAG, "Error mapping font file " + fullPath);
              return null;
          }
      }
  
      private static void pushFamilyToFallback(@NonNull FontConfig.Family xmlFamily,
              @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap,
              @NonNull Map<String, ByteBuffer> cache,
              @NonNull ArrayList<Font> availableFonts) {
  
          final String languageTags = xmlFamily.getLanguages();
          final int variant = xmlFamily.getVariant();
  
          final ArrayList<FontConfig.Font> defaultFonts = new ArrayList<>();
          final ArrayMap<String, ArrayList<FontConfig.Font>> specificFallbackFonts = new ArrayMap<>();
  
          // Collect default fallback and specific fallback fonts.
          for (final FontConfig.Font font : xmlFamily.getFonts()) {
              final String fallbackName = font.getFallbackFor();
              if (fallbackName == null) {
                  defaultFonts.add(font);
              } else {
                  ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(fallbackName);
                  if (fallback == null) {
                      fallback = new ArrayList<>();
                      specificFallbackFonts.put(fallbackName, fallback);
                  }
                  fallback.add(font);
              }
          }
  
          final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily(
                  xmlFamily.getName(), defaultFonts, languageTags, variant, cache, availableFonts);
  
          // Insert family into fallback map.
          for (int i = 0; i < fallbackMap.size(); i++) {
              final ArrayList<FontConfig.Font> fallback =
                      specificFallbackFonts.get(fallbackMap.keyAt(i));
              if (fallback == null) {
                  if (defaultFamily != null) {
                      fallbackMap.valueAt(i).add(defaultFamily);
                  }
              } else {
                  final FontFamily family = createFontFamily(
                          xmlFamily.getName(), fallback, languageTags, variant, cache,
                          availableFonts);
                  if (family != null) {
                      fallbackMap.valueAt(i).add(family);
                  } else if (defaultFamily != null) {
                      fallbackMap.valueAt(i).add(defaultFamily);
                  } else {
                      // There is no valid for for default fallback. Ignore.
                  }
              }
          }
      }
  
      private static @Nullable FontFamily createFontFamily(@NonNull String familyName,
              @NonNull List<FontConfig.Font> fonts,
              @NonNull String languageTags,
              @FontConfig.Family.Variant int variant,
              @NonNull Map<String, ByteBuffer> cache,
              @NonNull ArrayList<Font> availableFonts) {
          if (fonts.size() == 0) {
              return null;
          }
  
          FontFamily.Builder b = null;
          for (int i = 0; i < fonts.size(); i++) {
              final FontConfig.Font fontConfig = fonts.get(i);
              final String fullPath = fontConfig.getFontName();
              ByteBuffer buffer = cache.get(fullPath);
              if (buffer == null) {
                  if (cache.containsKey(fullPath)) {
                      continue;  // Already failed to mmap. Skip it.
                  }
                  buffer = mmap(fullPath);
                  cache.put(fullPath, buffer);
                  if (buffer == null) {
                      continue;
                  }
              }
  
              final Font font;
              try {
                  font = new Font.Builder(buffer, new File(fullPath), languageTags)
                          .setWeight(fontConfig.getWeight())
                          .setSlant(fontConfig.isItalic() ? FontStyle.FONT_SLANT_ITALIC
                                  : FontStyle.FONT_SLANT_UPRIGHT)
                          .setTtcIndex(fontConfig.getTtcIndex())
                          .setFontVariationSettings(fontConfig.getAxes())
                          .build();
              } catch (IOException e) {
                  throw new RuntimeException(e);  // Never reaches here
              }
  
              availableFonts.add(font);
              if (b == null) {
                  b = new FontFamily.Builder(font);
              } else {
                  b.addFont(font);
              }
          }
          return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */);
      }
  
      private static void appendNamedFamily(@NonNull FontConfig.Family xmlFamily,
              @NonNull HashMap<String, ByteBuffer> bufferCache,
              @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap,
              @NonNull ArrayList<Font> availableFonts) {
          final String familyName = xmlFamily.getName();
          final FontFamily family = createFontFamily(
                  familyName, Arrays.asList(xmlFamily.getFonts()),
                  xmlFamily.getLanguages(), xmlFamily.getVariant(), bufferCache, availableFonts);
          if (family == null) {
              return;
          }
          final ArrayList<FontFamily> fallback = new ArrayList<>();
          fallback.add(family);
          fallbackListMap.put(familyName, fallback);
      }
  
      /**
       * Build the system fallback from xml file.
       *
       * @param xmlPath A full path string to the fonts.xml file.
       * @param fontDir A full path string to the system font directory. This must end with
       *                slash('/').
       * @param fallbackMap An output system fallback map. Caller must pass empty map.
       * @return a list of aliases
       * @hide
       */
      @VisibleForTesting
      public static FontConfig.Alias[] buildSystemFallback(@NonNull String xmlPath,
              @NonNull String fontDir,
              @NonNull FontCustomizationParser.Result oemCustomization,
              @NonNull ArrayMap<String, FontFamily[]> fallbackMap,
              @NonNull ArrayList<Font> availableFonts) {
          try {
              final FileInputStream fontsIn = new FileInputStream(xmlPath);
              final FontConfig fontConfig = FontListParser.parse(fontsIn, fontDir);
  
              final HashMap<String, ByteBuffer> bufferCache = new HashMap<String, ByteBuffer>();
              final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies();
  
              final ArrayMap<String, ArrayList<FontFamily>> fallbackListMap = new ArrayMap<>();
              // First traverse families which have a 'name' attribute to create fallback map.
              for (final FontConfig.Family xmlFamily : xmlFamilies) {
                  final String familyName = xmlFamily.getName();
                  if (familyName == null) {
                      continue;
                  }
                  appendNamedFamily(xmlFamily, bufferCache, fallbackListMap, availableFonts);
              }
  
              for (int i = 0; i < oemCustomization.mAdditionalNamedFamilies.size(); ++i) {
                  appendNamedFamily(oemCustomization.mAdditionalNamedFamilies.get(i),
                          bufferCache, fallbackListMap, availableFonts);
              }
  
              // Then, add fallback fonts to the each fallback map.
              for (int i = 0; i < xmlFamilies.length; i++) {
                  final FontConfig.Family xmlFamily = xmlFamilies[i];
                  // The first family (usually the sans-serif family) is always placed immediately
                  // after the primary family in the fallback.
                  if (i == 0 || xmlFamily.getName() == null) {
                      pushFamilyToFallback(xmlFamily, fallbackListMap, bufferCache, availableFonts);
                  }
              }
  
              // Build the font map and fallback map.
              for (int i = 0; i < fallbackListMap.size(); i++) {
                  final String fallbackName = fallbackListMap.keyAt(i);
                  final List<FontFamily> familyList = fallbackListMap.valueAt(i);
                  final FontFamily[] families = familyList.toArray(new FontFamily[familyList.size()]);
  
                  fallbackMap.put(fallbackName, families);
              }
  
              final ArrayList<FontConfig.Alias> list = new ArrayList<>();
              list.addAll(Arrays.asList(fontConfig.getAliases()));
              list.addAll(oemCustomization.mAdditionalAliases);
              return list.toArray(new FontConfig.Alias[list.size()]);
          } catch (IOException | XmlPullParserException e) {
              Log.e(TAG, "Failed initialize system fallbacks.", e);
              return ArrayUtils.emptyArray(FontConfig.Alias.class);
          }
      }
  
      private static FontCustomizationParser.Result readFontCustomization(
              @NonNull String customizeXml, @NonNull String customFontsDir) {
          try (FileInputStream f = new FileInputStream(customizeXml)) {
              return FontCustomizationParser.parse(f, customFontsDir);
          } catch (IOException e) {
              return new FontCustomizationParser.Result();
          } catch (XmlPullParserException e) {
              Log.e(TAG, "Failed to parse font customization XML", e);
              return new FontCustomizationParser.Result();
          }
      }
  
      static {
          final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>();
          final ArrayList<Font> availableFonts = new ArrayList<>();
          final FontCustomizationParser.Result oemCustomization =
                  readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/");
          sAliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/",
                  oemCustomization, systemFallbackMap, availableFonts);
          sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap);
          sAvailableFonts = Collections.unmodifiableList(availableFonts);
      }
  }
  