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 Log.e(TAG, "Error mapping font file " + fullPath); 109 return null; 110 } 111 } 112 pushFamilyToFallback(@onNull FontConfig.Family xmlFamily, @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap, @NonNull Map<String, ByteBuffer> cache, @NonNull ArrayList<Font> availableFonts)113 private static void pushFamilyToFallback(@NonNull FontConfig.Family xmlFamily, 114 @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap, 115 @NonNull Map<String, ByteBuffer> cache, 116 @NonNull ArrayList<Font> availableFonts) { 117 118 final String languageTags = xmlFamily.getLanguages(); 119 final int variant = xmlFamily.getVariant(); 120 121 final ArrayList<FontConfig.Font> defaultFonts = new ArrayList<>(); 122 final ArrayMap<String, ArrayList<FontConfig.Font>> specificFallbackFonts = new ArrayMap<>(); 123 124 // Collect default fallback and specific fallback fonts. 125 for (final FontConfig.Font font : xmlFamily.getFonts()) { 126 final String fallbackName = font.getFallbackFor(); 127 if (fallbackName == null) { 128 defaultFonts.add(font); 129 } else { 130 ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(fallbackName); 131 if (fallback == null) { 132 fallback = new ArrayList<>(); 133 specificFallbackFonts.put(fallbackName, fallback); 134 } 135 fallback.add(font); 136 } 137 } 138 139 final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily( 140 xmlFamily.getName(), defaultFonts, languageTags, variant, cache, availableFonts); 141 142 // Insert family into fallback map. 143 for (int i = 0; i < fallbackMap.size(); i++) { 144 final ArrayList<FontConfig.Font> fallback = 145 specificFallbackFonts.get(fallbackMap.keyAt(i)); 146 if (fallback == null) { 147 if (defaultFamily != null) { 148 fallbackMap.valueAt(i).add(defaultFamily); 149 } 150 } else { 151 final FontFamily family = createFontFamily( 152 xmlFamily.getName(), fallback, languageTags, variant, cache, 153 availableFonts); 154 if (family != null) { 155 fallbackMap.valueAt(i).add(family); 156 } else if (defaultFamily != null) { 157 fallbackMap.valueAt(i).add(defaultFamily); 158 } else { 159 // There is no valid for for default fallback. Ignore. 160 } 161 } 162 } 163 } 164 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)165 private static @Nullable FontFamily createFontFamily(@NonNull String familyName, 166 @NonNull List<FontConfig.Font> fonts, 167 @NonNull String languageTags, 168 @FontConfig.Family.Variant int variant, 169 @NonNull Map<String, ByteBuffer> cache, 170 @NonNull ArrayList<Font> availableFonts) { 171 if (fonts.size() == 0) { 172 return null; 173 } 174 175 FontFamily.Builder b = null; 176 for (int i = 0; i < fonts.size(); i++) { 177 final FontConfig.Font fontConfig = fonts.get(i); 178 final String fullPath = fontConfig.getFontName(); 179 ByteBuffer buffer = cache.get(fullPath); 180 if (buffer == null) { 181 if (cache.containsKey(fullPath)) { 182 continue; // Already failed to mmap. Skip it. 183 } 184 buffer = mmap(fullPath); 185 cache.put(fullPath, buffer); 186 if (buffer == null) { 187 continue; 188 } 189 } 190 191 final Font font; 192 try { 193 font = new Font.Builder(buffer, new File(fullPath), languageTags) 194 .setWeight(fontConfig.getWeight()) 195 .setSlant(fontConfig.isItalic() ? FontStyle.FONT_SLANT_ITALIC 196 : FontStyle.FONT_SLANT_UPRIGHT) 197 .setTtcIndex(fontConfig.getTtcIndex()) 198 .setFontVariationSettings(fontConfig.getAxes()) 199 .build(); 200 } catch (IOException e) { 201 throw new RuntimeException(e); // Never reaches here 202 } 203 204 availableFonts.add(font); 205 if (b == null) { 206 b = new FontFamily.Builder(font); 207 } else { 208 b.addFont(font); 209 } 210 } 211 return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */); 212 } 213 appendNamedFamily(@onNull FontConfig.Family xmlFamily, @NonNull HashMap<String, ByteBuffer> bufferCache, @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap, @NonNull ArrayList<Font> availableFonts)214 private static void appendNamedFamily(@NonNull FontConfig.Family xmlFamily, 215 @NonNull HashMap<String, ByteBuffer> bufferCache, 216 @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap, 217 @NonNull ArrayList<Font> availableFonts) { 218 final String familyName = xmlFamily.getName(); 219 final FontFamily family = createFontFamily( 220 familyName, Arrays.asList(xmlFamily.getFonts()), 221 xmlFamily.getLanguages(), xmlFamily.getVariant(), bufferCache, availableFonts); 222 if (family == null) { 223 return; 224 } 225 final ArrayList<FontFamily> fallback = new ArrayList<>(); 226 fallback.add(family); 227 fallbackListMap.put(familyName, fallback); 228 } 229 230 /** 231 * Build the system fallback from xml file. 232 * 233 * @param xmlPath A full path string to the fonts.xml file. 234 * @param fontDir A full path string to the system font directory. This must end with 235 * slash('/'). 236 * @param fallbackMap An output system fallback map. Caller must pass empty map. 237 * @return a list of aliases 238 * @hide 239 */ 240 @VisibleForTesting buildSystemFallback(@onNull String xmlPath, @NonNull String fontDir, @NonNull FontCustomizationParser.Result oemCustomization, @NonNull ArrayMap<String, FontFamily[]> fallbackMap, @NonNull ArrayList<Font> availableFonts)241 public static FontConfig.Alias[] buildSystemFallback(@NonNull String xmlPath, 242 @NonNull String fontDir, 243 @NonNull FontCustomizationParser.Result oemCustomization, 244 @NonNull ArrayMap<String, FontFamily[]> fallbackMap, 245 @NonNull ArrayList<Font> availableFonts) { 246 try { 247 final FileInputStream fontsIn = new FileInputStream(xmlPath); 248 final FontConfig fontConfig = FontListParser.parse(fontsIn, fontDir); 249 250 final HashMap<String, ByteBuffer> bufferCache = new HashMap<String, ByteBuffer>(); 251 final FontConfig.Family[] xmlFamilies = fontConfig.getFamilies(); 252 253 final ArrayMap<String, ArrayList<FontFamily>> fallbackListMap = new ArrayMap<>(); 254 // First traverse families which have a 'name' attribute to create fallback map. 255 for (final FontConfig.Family xmlFamily : xmlFamilies) { 256 final String familyName = xmlFamily.getName(); 257 if (familyName == null) { 258 continue; 259 } 260 appendNamedFamily(xmlFamily, bufferCache, fallbackListMap, availableFonts); 261 } 262 263 for (int i = 0; i < oemCustomization.mAdditionalNamedFamilies.size(); ++i) { 264 appendNamedFamily(oemCustomization.mAdditionalNamedFamilies.get(i), 265 bufferCache, fallbackListMap, availableFonts); 266 } 267 268 // Then, add fallback fonts to the each fallback map. 269 for (int i = 0; i < xmlFamilies.length; i++) { 270 final FontConfig.Family xmlFamily = xmlFamilies[i]; 271 // The first family (usually the sans-serif family) is always placed immediately 272 // after the primary family in the fallback. 273 if (i == 0 || xmlFamily.getName() == null) { 274 pushFamilyToFallback(xmlFamily, fallbackListMap, bufferCache, availableFonts); 275 } 276 } 277 278 // Build the font map and fallback map. 279 for (int i = 0; i < fallbackListMap.size(); i++) { 280 final String fallbackName = fallbackListMap.keyAt(i); 281 final List<FontFamily> familyList = fallbackListMap.valueAt(i); 282 final FontFamily[] families = familyList.toArray(new FontFamily[familyList.size()]); 283 284 fallbackMap.put(fallbackName, families); 285 } 286 287 final ArrayList<FontConfig.Alias> list = new ArrayList<>(); 288 list.addAll(Arrays.asList(fontConfig.getAliases())); 289 list.addAll(oemCustomization.mAdditionalAliases); 290 return list.toArray(new FontConfig.Alias[list.size()]); 291 } catch (IOException | XmlPullParserException e) { 292 Log.e(TAG, "Failed initialize system fallbacks.", e); 293 return ArrayUtils.emptyArray(FontConfig.Alias.class); 294 } 295 } 296 readFontCustomization( @onNull String customizeXml, @NonNull String customFontsDir)297 private static FontCustomizationParser.Result readFontCustomization( 298 @NonNull String customizeXml, @NonNull String customFontsDir) { 299 try (FileInputStream f = new FileInputStream(customizeXml)) { 300 return FontCustomizationParser.parse(f, customFontsDir); 301 } catch (IOException e) { 302 return new FontCustomizationParser.Result(); 303 } catch (XmlPullParserException e) { 304 Log.e(TAG, "Failed to parse font customization XML", e); 305 return new FontCustomizationParser.Result(); 306 } 307 } 308 309 static { 310 final ArrayMap<String, FontFamily[]> systemFallbackMap = new ArrayMap<>(); 311 final ArrayList<Font> availableFonts = new ArrayList<>(); 312 final FontCustomizationParser.Result oemCustomization = 313 readFontCustomization("/product/etc/fonts_customization.xml", "/product/fonts/"); 314 sAliases = buildSystemFallback("/system/etc/fonts.xml", "/system/fonts/", 315 oemCustomization, systemFallbackMap, availableFonts); 316 sSystemFallbackMap = Collections.unmodifiableMap(systemFallbackMap); 317 sAvailableFonts = Collections.unmodifiableList(availableFonts); 318 } 319 } 320