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.graphics.Typeface; 23 import android.text.FontConfig; 24 import android.util.ArrayMap; 25 import android.util.Log; 26 27 import com.android.internal.annotations.GuardedBy; 28 import com.android.internal.annotations.VisibleForTesting; 29 30 import org.xmlpull.v1.XmlPullParserException; 31 32 import java.io.File; 33 import java.io.FileInputStream; 34 import java.io.IOException; 35 import java.nio.ByteBuffer; 36 import java.nio.channels.FileChannel; 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Set; 42 43 /** 44 * Provides the system font configurations. 45 */ 46 public final class SystemFonts { 47 private static final String TAG = "SystemFonts"; 48 49 private static final String FONTS_XML = "/system/etc/fonts.xml"; 50 /** @hide */ 51 public static final String SYSTEM_FONT_DIR = "/system/fonts/"; 52 private static final String OEM_XML = "/product/etc/fonts_customization.xml"; 53 /** @hide */ 54 public static final String OEM_FONT_DIR = "/product/fonts/"; 55 SystemFonts()56 private SystemFonts() {} // Do not instansiate. 57 58 private static final Object LOCK = new Object(); 59 private static @GuardedBy("sLock") Set<Font> sAvailableFonts; 60 61 /** 62 * Returns all available font files in the system. 63 * 64 * @return a set of system fonts 65 */ getAvailableFonts()66 public static @NonNull Set<Font> getAvailableFonts() { 67 synchronized (LOCK) { 68 if (sAvailableFonts == null) { 69 sAvailableFonts = Font.getAvailableFonts(); 70 } 71 return sAvailableFonts; 72 } 73 } 74 75 /** 76 * @hide 77 */ resetAvailableFonts()78 public static void resetAvailableFonts() { 79 synchronized (LOCK) { 80 sAvailableFonts = null; 81 } 82 } 83 mmap(@onNull String fullPath)84 private static @Nullable ByteBuffer mmap(@NonNull String fullPath) { 85 try (FileInputStream file = new FileInputStream(fullPath)) { 86 final FileChannel fileChannel = file.getChannel(); 87 final long fontSize = fileChannel.size(); 88 return fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize); 89 } catch (IOException e) { 90 return null; 91 } 92 } 93 pushFamilyToFallback(@onNull FontConfig.FontFamily xmlFamily, @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap, @NonNull Map<String, ByteBuffer> cache)94 private static void pushFamilyToFallback(@NonNull FontConfig.FontFamily xmlFamily, 95 @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackMap, 96 @NonNull Map<String, ByteBuffer> cache) { 97 98 final String languageTags = xmlFamily.getLocaleList().toLanguageTags(); 99 final int variant = xmlFamily.getVariant(); 100 101 final ArrayList<FontConfig.Font> defaultFonts = new ArrayList<>(); 102 final ArrayMap<String, ArrayList<FontConfig.Font>> specificFallbackFonts = 103 new ArrayMap<>(); 104 105 // Collect default fallback and specific fallback fonts. 106 for (final FontConfig.Font font : xmlFamily.getFonts()) { 107 final String fallbackName = font.getFontFamilyName(); 108 if (fallbackName == null) { 109 defaultFonts.add(font); 110 } else { 111 ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(fallbackName); 112 if (fallback == null) { 113 fallback = new ArrayList<>(); 114 specificFallbackFonts.put(fallbackName, fallback); 115 } 116 fallback.add(font); 117 } 118 } 119 120 final FontFamily defaultFamily = defaultFonts.isEmpty() ? null : createFontFamily( 121 xmlFamily.getName(), defaultFonts, languageTags, variant, cache); 122 123 // Insert family into fallback map. 124 for (int i = 0; i < fallbackMap.size(); i++) { 125 String name = fallbackMap.keyAt(i); 126 final ArrayList<FontConfig.Font> fallback = specificFallbackFonts.get(name); 127 if (fallback == null) { 128 String familyName = xmlFamily.getName(); 129 if (defaultFamily != null 130 // do not add myself to the fallback chain. 131 && (familyName == null || !familyName.equals(name))) { 132 fallbackMap.valueAt(i).add(defaultFamily); 133 } 134 } else { 135 final FontFamily family = createFontFamily( 136 xmlFamily.getName(), fallback, languageTags, variant, cache); 137 if (family != null) { 138 fallbackMap.valueAt(i).add(family); 139 } else if (defaultFamily != null) { 140 fallbackMap.valueAt(i).add(defaultFamily); 141 } else { 142 // There is no valid for for default fallback. Ignore. 143 } 144 } 145 } 146 } 147 createFontFamily(@onNull String familyName, @NonNull List<FontConfig.Font> fonts, @NonNull String languageTags, @FontConfig.FontFamily.Variant int variant, @NonNull Map<String, ByteBuffer> cache)148 private static @Nullable FontFamily createFontFamily(@NonNull String familyName, 149 @NonNull List<FontConfig.Font> fonts, 150 @NonNull String languageTags, 151 @FontConfig.FontFamily.Variant int variant, 152 @NonNull Map<String, ByteBuffer> cache) { 153 if (fonts.size() == 0) { 154 return null; 155 } 156 157 FontFamily.Builder b = null; 158 for (int i = 0; i < fonts.size(); i++) { 159 final FontConfig.Font fontConfig = fonts.get(i); 160 final String fullPath = fontConfig.getFile().getAbsolutePath(); 161 ByteBuffer buffer = cache.get(fullPath); 162 if (buffer == null) { 163 if (cache.containsKey(fullPath)) { 164 continue; // Already failed to mmap. Skip it. 165 } 166 buffer = mmap(fullPath); 167 cache.put(fullPath, buffer); 168 if (buffer == null) { 169 continue; 170 } 171 } 172 173 final Font font; 174 try { 175 font = new Font.Builder(buffer, new File(fullPath), languageTags) 176 .setWeight(fontConfig.getStyle().getWeight()) 177 .setSlant(fontConfig.getStyle().getSlant()) 178 .setTtcIndex(fontConfig.getTtcIndex()) 179 .setFontVariationSettings(fontConfig.getFontVariationSettings()) 180 .build(); 181 } catch (IOException e) { 182 throw new RuntimeException(e); // Never reaches here 183 } 184 185 if (b == null) { 186 b = new FontFamily.Builder(font); 187 } else { 188 b.addFont(font); 189 } 190 } 191 return b == null ? null : b.build(languageTags, variant, false /* isCustomFallback */); 192 } 193 appendNamedFamily(@onNull FontConfig.FontFamily xmlFamily, @NonNull ArrayMap<String, ByteBuffer> bufferCache, @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap)194 private static void appendNamedFamily(@NonNull FontConfig.FontFamily xmlFamily, 195 @NonNull ArrayMap<String, ByteBuffer> bufferCache, 196 @NonNull ArrayMap<String, ArrayList<FontFamily>> fallbackListMap) { 197 final String familyName = xmlFamily.getName(); 198 final FontFamily family = createFontFamily( 199 familyName, xmlFamily.getFontList(), 200 xmlFamily.getLocaleList().toLanguageTags(), xmlFamily.getVariant(), 201 bufferCache); 202 if (family == null) { 203 return; 204 } 205 final ArrayList<FontFamily> fallback = new ArrayList<>(); 206 fallback.add(family); 207 fallbackListMap.put(familyName, fallback); 208 } 209 210 /** 211 * Get the updated FontConfig. 212 * 213 * @param updatableFontMap a font mapping of updated font files. 214 * @hide 215 */ getSystemFontConfig( @ullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion )216 public static @NonNull FontConfig getSystemFontConfig( 217 @Nullable Map<String, File> updatableFontMap, 218 long lastModifiedDate, 219 int configVersion 220 ) { 221 return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, 222 updatableFontMap, lastModifiedDate, configVersion); 223 } 224 225 /** 226 * Get the system preinstalled FontConfig. 227 * @hide 228 */ getSystemPreinstalledFontConfig()229 public static @NonNull FontConfig getSystemPreinstalledFontConfig() { 230 return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, null, 231 0, 0); 232 } 233 getSystemFontConfigInternal( @onNull String fontsXml, @NonNull String systemFontDir, @Nullable String oemXml, @Nullable String productFontDir, @Nullable Map<String, File> updatableFontMap, long lastModifiedDate, int configVersion )234 /* package */ static @NonNull FontConfig getSystemFontConfigInternal( 235 @NonNull String fontsXml, 236 @NonNull String systemFontDir, 237 @Nullable String oemXml, 238 @Nullable String productFontDir, 239 @Nullable Map<String, File> updatableFontMap, 240 long lastModifiedDate, 241 int configVersion 242 ) { 243 try { 244 return FontListParser.parse(fontsXml, systemFontDir, oemXml, productFontDir, 245 updatableFontMap, lastModifiedDate, configVersion); 246 } catch (IOException e) { 247 Log.e(TAG, "Failed to open/read system font configurations.", e); 248 return new FontConfig(Collections.emptyList(), Collections.emptyList(), 0, 0); 249 } catch (XmlPullParserException e) { 250 Log.e(TAG, "Failed to parse the system font configuration.", e); 251 return new FontConfig(Collections.emptyList(), Collections.emptyList(), 0, 0); 252 } 253 } 254 255 /** 256 * Build the system fallback from FontConfig. 257 * @hide 258 */ 259 @VisibleForTesting buildSystemFallback(FontConfig fontConfig)260 public static Map<String, FontFamily[]> buildSystemFallback(FontConfig fontConfig) { 261 return buildSystemFallback(fontConfig, new ArrayMap<>()); 262 } 263 264 /** @hide */ 265 @VisibleForTesting buildSystemFallback(FontConfig fontConfig, ArrayMap<String, ByteBuffer> outBufferCache)266 public static Map<String, FontFamily[]> buildSystemFallback(FontConfig fontConfig, 267 ArrayMap<String, ByteBuffer> outBufferCache) { 268 final Map<String, FontFamily[]> fallbackMap = new ArrayMap<>(); 269 final List<FontConfig.FontFamily> xmlFamilies = fontConfig.getFontFamilies(); 270 271 final ArrayMap<String, ArrayList<FontFamily>> fallbackListMap = new ArrayMap<>(); 272 // First traverse families which have a 'name' attribute to create fallback map. 273 for (final FontConfig.FontFamily xmlFamily : xmlFamilies) { 274 final String familyName = xmlFamily.getName(); 275 if (familyName == null) { 276 continue; 277 } 278 appendNamedFamily(xmlFamily, outBufferCache, fallbackListMap); 279 } 280 281 // Then, add fallback fonts to the each fallback map. 282 for (int i = 0; i < xmlFamilies.size(); i++) { 283 final FontConfig.FontFamily xmlFamily = xmlFamilies.get(i); 284 // The first family (usually the sans-serif family) is always placed immediately 285 // after the primary family in the fallback. 286 if (i == 0 || xmlFamily.getName() == null) { 287 pushFamilyToFallback(xmlFamily, fallbackListMap, outBufferCache); 288 } 289 } 290 291 // Build the font map and fallback map. 292 for (int i = 0; i < fallbackListMap.size(); i++) { 293 final String fallbackName = fallbackListMap.keyAt(i); 294 final List<FontFamily> familyList = fallbackListMap.valueAt(i); 295 fallbackMap.put(fallbackName, familyList.toArray(new FontFamily[0])); 296 } 297 298 return fallbackMap; 299 } 300 301 /** 302 * Build the system Typeface mappings from FontConfig and FallbackMap. 303 * @hide 304 */ 305 @VisibleForTesting buildSystemTypefaces( FontConfig fontConfig, Map<String, FontFamily[]> fallbackMap)306 public static Map<String, Typeface> buildSystemTypefaces( 307 FontConfig fontConfig, 308 Map<String, FontFamily[]> fallbackMap) { 309 final ArrayMap<String, Typeface> result = new ArrayMap<>(); 310 Typeface.initSystemDefaultTypefaces(fallbackMap, fontConfig.getAliases(), result); 311 return result; 312 } 313 } 314