1 /* 2 * Copyright 2018 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 17 package android.graphics.fonts; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.graphics.FontListParser; 22 import android.text.FontConfig; 23 import android.util.ArrayMap; 24 import android.util.Log; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 import com.android.internal.util.ArrayUtils; 28 29 import org.xmlpull.v1.XmlPullParserException; 30 31 import java.io.File; 32 import java.io.FileInputStream; 33 import java.io.IOException; 34 import java.nio.ByteBuffer; 35 import java.nio.channels.FileChannel; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.Collections; 39 import java.util.HashMap; 40 import java.util.HashSet; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.Set; 44 45 /** 46 * Provides the system font configurations. 47 */ 48 public final class SystemFonts { 49 private static final String TAG = "SystemFonts"; 50 private static final String DEFAULT_FAMILY = "sans-serif"; 51 SystemFonts()52 private SystemFonts() {} // Do not instansiate. 53 54 private static final Map<String, FontFamily[]> sSystemFallbackMap; 55 private static final FontConfig.Alias[] sAliases; 56 private static final List<Font> sAvailableFonts; 57 58 /** 59 * Returns all available font files in the system. 60 * 61 * @return a set of system fonts 62 */ getAvailableFonts()63 public static @NonNull Set<Font> getAvailableFonts() { 64 HashSet<Font> set = new HashSet<>(); 65 set.addAll(sAvailableFonts); 66 return set; 67 } 68 69 /** 70 * Returns fallback list for the given family name. 71 * 72 * If no fallback found for the given family name, returns fallback for the default family. 73 * 74 * @param familyName family name, e.g. "serif" 75 * @hide 76 */ getSystemFallback(@ullable String familyName)77 public static @NonNull FontFamily[] getSystemFallback(@Nullable String familyName) { 78 final FontFamily[] families = sSystemFallbackMap.get(familyName); 79 return families == null ? sSystemFallbackMap.get(DEFAULT_FAMILY) : families; 80 } 81 82 /** 83 * Returns raw system fallback map. 84 * 85 * This method is intended to be used only by Typeface static initializer. 86 * @hide 87 */ getRawSystemFallbackMap()88 public static @NonNull Map<String, FontFamily[]> getRawSystemFallbackMap() { 89 return sSystemFallbackMap; 90 } 91 92 /** 93 * Returns a list of aliases. 94 * 95 * This method is intended to be used only by Typeface static initializer. 96 * @hide 97 */ getAliases()98 public static @NonNull FontConfig.Alias[] getAliases() { 99 return sAliases; 100 } 101 mmap(@onNull String fullPath)102 private static @Nullable ByteBuffer mmap(@NonNull String fullPath) { 103 try (FileInputStream file = new FileInputStream(fullPath)) { 104 final FileChannel fileChannel = file.getChannel(); 105 final long fontSize = fileChannel.size(); 106 return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize); 107 } catch (IOException e) { 108 return null; 109 } 110 } 111 pushFamilyToFallback(@onNull FontConfig.Family xmlFamily, @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap, @NonNull Map<String, ByteBuffer> cache, @NonNull ArrayList<Font> availableFonts)112 private static void pushFamilyToFallback(@NonNull FontConfig.Family xmlFamily, 113 @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap, 114 @NonNull Map<String, ByteBuffer> cache, 115 @NonNull ArrayList<Font> availableFonts) { 116 117 final String languageTags = xmlFamily.getLanguages(); 118 final int variant = xmlFamily.getVariant(); 119 120 final ArrayList<FontConfig.Font> defaultFonts = new ArrayList<>(); 121 final ArrayMap<String, ArrayList<FontConfig.Font>> specificFallbackFonts = new ArrayMap<>(); 122 123 // Collect default fallback and specific fallback fonts. 124 for (final FontConfig.Font font : xmlFamily.getFonts()) { 125 final String fallbackName = font.getFallbackFor(); 126 if (fallbackName == null) { 127 defaultFonts.add(font); 128 } else { 129 ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(fallbackName); 130 if (fallback == null) { 131 fallback = new ArrayList<>(); 132 specificFallbackFonts.put(fallbackName, fallback); 133 } 134 fallback.add(font); 135 } 136 } 137 138 final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily( 139 xmlFamily.getName(), defaultFonts, languageTags, variant, cache, availableFonts); 140 141 // Insert family into fallback map. 142 for (int i = 0; i < fallbackMap.size(); i++) { 143 final ArrayList<FontConfig.Font> fallback = 144 specificFallbackFonts.get(fallbackMap.keyAt(i)); 145 if (fallback == null) { 146 if (defaultFamily != null) { 147 fallbackMap.valueAt(i).add(defaultFamily); 148 } 149 } else { 150 final FontFamily family = createFontFamily( 151 xmlFamily.getName(), fallback, languageTags, variant, cache, 152 availableFonts); 153 if (family != null) { 154 fallbackMap.valueAt(i).add(family); 155 } else if (defaultFamily != null) { 156 fallbackMap.valueAt(i).add(defaultFamily); 157 } else { 158 // There is no valid for for default fallback. Ignore. 159 } 160 } 161 } 162 } 163 createFontFamily(@onNull String familyName, @NonNull List<FontConfig.Font> fonts, @NonNull String languageTags, @FontConfig.Family.Variant int variant, @NonNull Map<String, ByteBuffer> cache, @NonNull ArrayList<Font> availableFonts)164 private static @Nullable FontFamily createFontFamily(@NonNull String familyName, 165 @NonNull List<FontConfig.Font> fonts, 166 @NonNull String languageTags, 167 @FontConfig.Family.Variant int variant, 168 @NonNull Map<String, ByteBuffer> cache, 169 @NonNull ArrayList<Font> availableFonts) { 170 if (fonts.size() == 0) { 171 return null; 172 } 173 174 FontFamily.Builder b = null; 175 for (int i = 0; i < fonts.size(); i++) { 176 final FontConfig.Font fontConfig = fonts.get(i); 177 final String fullPath = fontConfig.getFontName(); 178 ByteBuffer buffer = cache.get(fullPath); 179 if (buffer == null) { 180 if (cache.containsKey(fullPath)) { 181 continue; // Already failed to mmap. Skip it. 182 } 183 buffer = mmap(fullPath); 184 cache.put(fullPath, buffer); 185 if (buffer == null) { 186 continue; 187 } 188 } 189 190 final Font font; 191 try { 192 font = new Font.Builder(buffer, new File(fullPath), languageTags) 193 .setWeight(fontConfig.getWeight()) 194 .setSlant(fontConfig.isItalic() ? FontStyle.FONT_SLANT_ITALIC 195 : FontStyle.FONT_SLANT_UPRIGHT) 196 .setTtcIndex(fontConfig.getTtcIndex()) 197 .setFontVariationSettings(fontConfig.getAxes()) 198 .build(); 199 } catch (IOException e) { 200 throw new RuntimeException(e); // Never reaches here 201 } 202 203 availableFonts.add(font); 204 if (b == null) { 205 b = new FontFamily.Builder(font); 206 } else { 207 b.addFont(font); 208 } 209 } 210 return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */); 211 } 212 appendNamedFamily(@onNull FontConfig.Family xmlFamily, @NonNull HashMap<String, ByteBuffer> bufferCache, @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap, @NonNull ArrayList<Font> availableFonts)213 private static void appendNamedFamily(@NonNull FontConfig.Family xmlFamily, 214 @NonNull HashMap<String, ByteBuffer> bufferCache, 215 @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap, 216 @NonNull ArrayList<Font> availableFonts) { 217 final String familyName = xmlFamily.getName(); 218 final FontFamily family = createFontFamily( 219 familyName, Arrays.asList(xmlFamily.getFonts()), 220 xmlFamily.getLanguages(), xmlFamily.getVariant(), bufferCache, availableFonts); 221 if (family == null) { 222 return; 223 } 224 final ArrayList<FontFamily> fallback = new ArrayList<>(); 225 fallback.add(family); 226 fallbackListMap.put(familyName, fallback); 227 } 228 229 /** 230 * Build the system fallback from xml file. 231 * 232 * @param xmlPath A full path string to the fonts.xml file. 233 * @param fontDir A full path string to the system font directory. This must end with 234 * slash('/'). 235 * @param fallbackMap An output system fallback map. Caller must pass empty map. 236 * @return a list of aliases 237 * @hide 238 */ 239 @VisibleForTesting buildSystemFallback(@onNull String xmlPath, @NonNull String fontDir, @NonNull FontCustomizationParser.Result oemCustomization, @NonNull ArrayMap<String, FontFamily[]> fallbackMap, @NonNull ArrayList<Font> availableFonts)240 public static FontConfig.Alias[] buildSystemFallback(@NonNull String xmlPath, 241 @NonNull String fontDir, 242 @NonNull FontCustomizationParser.Result oemCustomization, 243 @NonNull ArrayMap<String, FontFamily[]> fallbackMap, 244 @NonNull ArrayList<Font> availableFonts) { 245 try { 246 final FileInputStream fontsIn = new FileInputStream(xmlPath); 247 final FontConfig fontConfig = FontListParser.parse(fontsIn, fontDir); 248 249 final HashMap<String, ByteBuffer> bufferCache = new HashMap<String, ByteBuffer>(); 250 final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies(); 251 252 final ArrayMap<String, ArrayList<FontFamily>> fallbackListMap = new ArrayMap<>(); 253 // First traverse families which have a 'name' attribute to create fallback map. 254 for (final FontConfig.Family xmlFamily : xmlFamilies) { 255 final String familyName = xmlFamily.getName(); 256 if (familyName == null) { 257 continue; 258 } 259 appendNamedFamily(xmlFamily, bufferCache, fallbackListMap, availableFonts); 260 } 261 262 for (int i = 0; i < oemCustomization.mAdditionalNamedFamilies.size(); ++i) { 263 appendNamedFamily(oemCustomization.mAdditionalNamedFamilies.get(i), 264 bufferCache, fallbackListMap, availableFonts); 265 } 266 267 // Then, add fallback fonts to the each fallback map. 268 for (int i = 0; i < xmlFamilies.length; i++) { 269 final FontConfig.Family xmlFamily = xmlFamilies[i]; 270 // The first family (usually the sans-serif family) is always placed immediately 271 // after the primary family in the fallback. 272 if (i == 0 || xmlFamily.getName() == null) { 273 pushFamilyToFallback(xmlFamily, fallbackListMap, bufferCache, availableFonts); 274 } 275 } 276 277 // Build the font map and fallback map. 278 for (int i = 0; i < fallbackListMap.size(); i++) { 279 final String fallbackName = fallbackListMap.keyAt(i); 280 final List<FontFamily> familyList = fallbackListMap.valueAt(i); 281 final FontFamily[] families = familyList.toArray(new FontFamily[familyList.size()]); 282 283 fallbackMap.put(fallbackName, families); 284 } 285 286 final ArrayList<FontConfig.Alias> list = new ArrayList<>(); 287 list.addAll(Arrays.asList(fontConfig.getAliases())); 288 list.addAll(oemCustomization.mAdditionalAliases); 289 return list.toArray(new FontConfig.Alias[list.size()]); 290 } catch (IOException | XmlPullParserException e) { 291 Log.e(TAG, "Failed initialize system fallbacks.", e); 292 return ArrayUtils.emptyArray(FontConfig.Alias.class); 293 } 294 } 295 readFontCustomization( @onNull String customizeXml, @NonNull String customFontsDir)296 private static FontCustomizationParser.Result readFontCustomization( 297 @NonNull String customizeXml, @NonNull String customFontsDir) { 298 try (FileInputStream f = new FileInputStream(customizeXml)) { 299 return FontCustomizationParser.parse(f, customFontsDir); 300 } catch (IOException e) { 301 return new FontCustomizationParser.Result(); 302 } catch (XmlPullParserException e) { 303 Log.e(TAG, "Failed to parse font customization XML", e); 304 return new FontCustomizationParser.Result(); 305 } 306 } 307 308 static { 309 final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>(); 310 final ArrayList<Font> availableFonts = new ArrayList<>(); 311 final FontCustomizationParser.Result oemCustomization = 312 readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/"); 313 sAliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", 314 oemCustomization, systemFallbackMap, availableFonts); 315 sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap); 316 sAvailableFonts = Collections.unmodifiableList(availableFonts); 317 } 318 } 319