-
Notifications
You must be signed in to change notification settings - Fork 11
Font System
This page documents how Android's font system works in AOSP 16 QPR2, how font names get resolved, the build-time and runtime configuration paths, and the theming options available (custom fonts, per-user font overrides, RROs). Written as a general reference — no specific ROM assumed.
The font system has two layers:
-
Build-time: font files + JSON config are assembled by Soong into a single
font_fallback.xmlinstalled at/system/etc/. OEMs can extend this viafonts_customization.xmlinstalled on/productor/vendor. -
Runtime:
FontManagerService(a system service) parses the combined config into a font map, serializes it to shared memory, and hands it to each app at bindApplication time.Typeface.create(name, style)looks up the map.
Up to Android 14, frameworks/base/data/fonts/fonts.xml was hand-maintained and installed verbatim to /system/etc/fonts.xml. From Android 15 onwards, font_fallback.xml is generated from structured inputs by the generate_fonts_xml tool (driven by the generate_font_fallback genrule in frameworks/base/data/fonts/Android.bp).
Inputs to the generator:
| File | Purpose |
|---|---|
frameworks/base/data/fonts/alias.json |
Maps common names (arial, helvetica) and weight variants (sans-serif-light) to canonical families |
frameworks/base/data/fonts/fallback_order.json |
Script-specific fallback chains (Arabic, CJK, etc.) |
Font packages (:Roboto, :RobotoFlex, etc.) |
Font files + per-package font_config.json declaring family ownership |
Output: /system/etc/font_fallback.xml. The first named family in the generated config is the platform default (Roboto declares itself as sans-serif). fonts.xml is still installed for backward compatibility but is no longer authoritative.
prebuilt_font {
name: "MyFont-Regular.ttf",
src: "MyFont-Regular.ttf",
product_specific: true, // installs to /product/fonts/
}
filegroup {
name: "my_fonts_customization",
srcs: ["fonts_customization.xml"],
}
runtime_resource_overlay {
name: "FontMyFontOverlay",
theme: "FontMyFont",
product_specific: true,
}
-
prebuilt_fontinstalls a TTF/OTF file to the right fonts directory for the partition -
filegroupexposesfonts_customization.xmlto genrules (see below) -
runtime_resource_overlaybuilds an APK that changesconfig_*FontFamilystrings
Installed at /product/etc/fonts_customization.xml or /system_ext/etc/fonts_customization.xml, merged with the base config at boot. This is how Pixel gets google-sans, google-sans-text, the variable-* family set, etc. — none of those are in base AOSP.
Supported elements:
<fonts-modification version="1">
<!-- Add a new named family -->
<family customizationType="new-named-family" name="my-font">
<font weight="400" style="normal">MyFont-Regular.ttf</font>
<font weight="700" style="normal">MyFont-Bold.ttf</font>
</family>
<!-- Add multiple families at once -->
<family-list customizationType="new-named-family">
<family name="variable-title-large">
<font weight="400" style="normal">
MyFont-Variable.ttf
<axis tag="wght" stylevalue="400"/>
</font>
</family>
</family-list>
<!-- Create an alias to an existing family -->
<alias name="my-font-medium" to="my-font" weight="500" />
</fonts-modification>Supported variable-font axes: wght (weight), ital (italic 0/1), wdth (width), opsz (optical size). Android handles optical sizing internally; typically only wght needs to be declared explicitly.
When a view needs to render text with a particular typeface:
XML attribute or programmatic call
│ android:fontFamily="sans-serif-medium" or Typeface.create("sans-serif-medium", BOLD)
▼
Typeface.create(String familyName, int style)
│
▼
sSystemFontMap (process-local Map<String, Typeface>)
│ populated from the serialized font map sent by system_server at bindApplication
▼
Returns Typeface or null
│
▼
Null → fall back to default (Typeface.DEFAULT), i.e. the first entry in the font config
- On boot,
FontManagerServicecallsSystemFonts.buildSystemFallback(fontConfig, ...)with the parsed config (base +fonts_customization.xml+ any runtime additions from/data/fonts). -
SystemFonts.buildSystemTypefacesturns the resolved families into aMap<String, Typeface>keyed by family name. - The map is serialized into
SharedMemoryand handed to each app process on launch viaFontManagerInternal.getSerializedSystemFontMap()→bindApplication→Typeface.setSystemFontMapNative. - All processes for a given boot share the same map. Changes to the font config require process restart to take effect.
At runtime both appear as entries in the font map. A family with a forced weight behaves identically whether it was originally declared via <family> or <alias ... weight="500"/>.
-
Typeface.create("sans-serif-medium", NORMAL)returns a Typeface drawing from thesans-seriffamily with weight 500. -
Typeface.create("nonexistent", NORMAL)returns null →TextViewfalls back toTypeface.DEFAULT. No error, no warning.
SystemFonts.getAvailableFonts() returns the resolved font set without the app having to parse any XML. Preferred over reading /system/etc/font_fallback.xml directly.
Because Material styles need to vary by OEM, AOSP adds a layer of indirection via string resources in core/res/res/values/config.xml:
| Resource | Default value | Purpose |
|---|---|---|
config_bodyFontFamily |
sans-serif |
Body text |
config_bodyFontFamilyMedium |
sans-serif-medium |
Emphasized body text |
config_headlineFontFamily |
sans-serif |
Headlines |
config_headlineFontFamilyMedium |
sans-serif-medium |
Emphasized headlines |
config_clockFontFamily |
(empty) | Lock screen / keyguard clock |
config_clockFontFeatureSettings |
(empty) | OpenType features for clock (e.g. ss01 for alternate digits) |
config_lightFontFamily |
(not in stock AOSP) | Light-weight font (added by The-Clover-Project / XOS) |
config_regularFontFamily |
(not in stock AOSP) | Regular-weight font (added by The-Clover-Project / XOS) |
Styles reference these via @string/config_bodyFontFamily or @*android:string/config_bodyFontFamily from the public namespace. Anything that reads the string through the resource system picks up RRO overrides automatically.
The font_family_*_material indirection (declared in core/res/res/values/donottranslate_material.xml) maps Material 1 names to config strings:
<string name="font_family_display_1_material">@string/config_bodyFontFamily</string>
<string name="font_family_headline_material">@string/config_headlineFontFamily</string>
...So @string/font_family_body_1_material → @string/config_bodyFontFamily → "sans-serif" → resolved in font map.
config_clockFontFeatureSettings is a newer addition used by DefaultClockFaceController (SystemUI) to enable OpenType features on clock text:
val clockFontFeatureSettings = resources.getString(
com.android.internal.R.string.config_clockFontFeatureSettings
)
if (clockFontFeatureSettings.isNotEmpty()) {
view.fontFeatureSettings = clockFontFeatureSettings
}Common values:
-
ss01: alternate digits (tabular or stylistic variant) -
ss02–ss20: other stylistic sets - Multiple features comma-separated:
"ss01,ss02"
To verify a font actually supports a feature before enabling it:
nix-shell -p python3Packages.fonttools --run "ttx -t GSUB -o - MyFont.ttf | grep -B2 -A10 'ss01'"Not everything goes through the resource indirection. Both XML styles and programmatic calls frequently embed the family name as a string literal. These are not RRO-overridable directly — you can only reach them by injecting entries into the font map itself.
Audited across core/res, packages/SettingsLib, packages/SystemUI, packages/apps/Settings, packages/apps/Launcher3, packages/apps/PackageInstaller.
sans-serif family (from base AOSP fonts/alias.json):
sans-serifsans-serif-mediumsans-serif-lightsans-serif-thinsans-serif-blacksans-serif-regularsans-serif-condensedsans-serif-condensed-mediumsans-serif-condensed-light
GMS / Pixel names (defined via fonts_customization.xml on Pixel; absent on stock AOSP):
google-sansgoogle-sans-clockgoogle-sans-flexgoogle-sans-mediumgoogle-sans-textgoogle-sans-text-mediumroboto-regularfont-family-flex-device-default
Variable-axis family set (GSF-defined, referenced by Material 3 Expressive styles in SystemUI / Launcher3 / PackageInstaller):
Pattern: variable-{display|headline|title|label|body}-{large|medium|small} with optional -emphasized suffix (36 combinations total).
On a device where these aren't declared in any fonts_customization.xml, Typeface.create("variable-title-large", ...) returns null and falls back silently to the default family — the XML inflates without error but text renders in whatever the default is.
A grep across packages/SystemUI, packages/SettingsLib, packages/apps/Settings, frameworks/base/core/java/android/widget/ turns up:
Typeface.create("sans-serif", ...)
Typeface.create("sans-serif-condensed", ...)
Typeface.create("google-sans", ...)
Typeface.create("variable-body-medium", ...)These bypass android:fontFamily entirely and go straight to the font map.
Standard approach for changing text appearance across the system. Use a runtime_resource_overlay Soong module that ships a res/values/config.xml:
<!-- my overlay's res/values/config.xml -->
<string name="config_bodyFontFamily" translatable="false">MyCustomFont-Regular</string>
<string name="config_headlineFontFamily" translatable="false">MyCustomFont-Regular</string>Covers: every style that reads @string/config_*FontFamily, transitively including font_family_*_material (used by AppCompat and older Material styles).
Misses: everything with a hardcoded family string — SystemUI clock (google-sans-clock), QS tile labels (variable-title-medium), Launcher icon labels (variable-body-medium), system dialogs using sans-serif-light directly.
Requires: the custom font must already exist in the font map (either baked into the device via fonts_customization.xml or installed at runtime via FontManagerService).
Build a device-level fonts_customization.xml that points hardcoded names at a custom font:
<fonts-modification version="1">
<family-list customizationType="new-named-family">
<family name="variable-title-large">
<font weight="400" style="normal">MyFont-Variable.ttf</font>
</family>
<family name="google-sans">
<font weight="400" style="normal">MyFont-Variable.ttf</font>
</family>
...
</family-list>
</fonts-modification>Pros: covers everything, including programmatic Typeface.create calls. Works for third-party apps too.
Cons: device-wide, applies to all users; static at build time unless combined with installCustomFontFamily.
Declared in an Android.bp via prebuilt_etc with the XML file, plus a set of prebuilt_font modules for the font files. Typically packaged together with a runtime_resource_overlay that also overrides the config_*FontFamily strings.
AOSP has a dynamic font-update API (originally added for font updatability, e.g. emoji updates) gated by the signature-only INSTALL_CUSTOM_FONTS permission:
-
installCustomFontFamily(List<FontUpdateRequest>)— writes a new family into/data/fonts/files, updates/data/fonts/config/config.xml, and rebuilds the serialized font map. -
setActiveCustomFontFamily(familyName)— marks one of the installed families as "active" for the calling user. Stock AOSP only stores the state; a ROM has to wire up what "active" actually does.
Common wiring: setActiveCustomFontFamily commits a fabricated FabricatedOverlay that overrides config_bodyFontFamily etc. to the active family's PostScript name. Fabricated overlays are per-user by construction (each user's overlay targets only their own process set), so different users see different fonts without conflict.
This still only covers approach 1 — hardcoded names slip through unless also handled.
Combines approach 1 and approach 2 without needing a static fonts_customization.xml:
- Declare a string resource for each hardcoded family name:
config_fontFamilyAlias_sans_serif,config_fontFamilyAlias_variable_title_large, etc. Default value: the name itself (identity mapping). - Add a small shim inside
Typeface.create(String, int)that looks up the alias resource before querying the font map:actualName = resources.getString(config_fontFamilyAlias_<sanitized_name>). - When a custom font is activated, commit a fabricated RRO that overrides every
config_fontFamilyAlias_*string to the custom font's PostScript name.
End result: Typeface.create("variable-title-large", ...) resolves to the custom font via the RRO, per-user, without having to patch any AOSP style XML.
Tradeoff: process restart required for new processes to pick up the overlay — same as approach 1. Resources.getString on every Typeface.create is a small cost (optionally cache the resource IDs per-process).
| File | Role |
|---|---|
frameworks/base/graphics/java/android/graphics/Typeface.java |
Typeface.create(String, int), setSystemFontMap, sSystemFontMap
|
frameworks/base/graphics/java/android/graphics/fonts/SystemFonts.java |
Parses fonts config, builds named family map and fallback arrays |
frameworks/base/services/core/java/com/android/server/graphics/fonts/FontManagerService.java |
System service, owns the serialized font map, implements font update API |
frameworks/base/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java |
On-disk /data/fonts state, merges runtime additions into the FontConfig |
frameworks/base/services/core/java/com/android/server/graphics/fonts/FontManagerInternal.java |
Local-services handle used by ActivityManagerService at bindApplication
|
frameworks/base/data/fonts/Android.bp |
generate_font_fallback genrule, font packages |
frameworks/base/data/fonts/alias.json |
Font-name alias declarations |
frameworks/base/data/fonts/fallback_order.json |
Script-specific fallback order |
frameworks/base/data/fonts/fonts.xml |
Legacy/compat font config |
frameworks/base/core/res/res/values/config.xml |
config_*FontFamily string defaults |
frameworks/base/core/res/res/values/donottranslate_material.xml |
Material 1 font_family_*_material → config_*FontFamily redirection |
packages/SystemUI/customization/src/.../DefaultClockController.kt |
Reads config_clockFontFamily and config_clockFontFeatureSettings
|
What font is a specific view actually using?
TextView.getTypeface().toString() doesn't print the family name — wrap with a probe at the call site, or look at the attribute set fed to setTypefaceFromAttrs.
Is a family name present in the font map?
adb shell cmd font dump
dumps the current FontConfig. The resolved names appear as NamedFamilyList entries. Names absent from the dump silently fall back to default.
Did an RRO apply?
adb shell cmd overlay list android
adb shell cmd overlay dump <overlay-package-name>
Shows whether the overlay is enabled for the current user and which resources it overrides.
When does the font map actually get rebuilt?
-
installCustomFontFamily,updateFontFamily,removeCustomFontFamilyall callupdateSerializedFontMap(). -
setActiveCustomFontFamilyin stock AOSP does not — but a ROM that uses it for per-user theming typically doesn't rebuild the map here either; it commits an RRO, which triggers a resource reload on next process start. - The running process's
sSystemFontMapis not mutated afterbindApplication. Seeing changes always requires a process restart.
Inspecting OpenType features in a font:
nix-shell -p python3Packages.fonttools --run "ttx -t GSUB -o - MyFont.ttf | less"Shows which stylistic sets (ss01…ss20), ligatures (liga, dlig), and other features are supported. Useful when setting fontFeatureSettings for a clock or any TextView.
- Keep font customizations in the product/vendor layer when they're device-specific; avoid modifying
frameworks/base/data/fonts/. - Use generic names (
adwaita-sans,MyFontFamily) rather than ROM-specific ones so the fonts and overlays can be reused across projects. - Variable fonts are preferred over static weight files — one TTF can cover all weights via the
wghtaxis, reducing system image size. - A
runtime_resource_overlaySoong module is the cleanest way to shipconfig_*FontFamilyoverrides at build time; useFabricatedOverlayfor per-user runtime overrides. -
installCustomFontFamilyrejects static fonts without awghtaxis when enforcing variable-only fonts — design for that constraint if you're shipping a custom font that the user can pick at runtime.