Skip to content

Font System

Simão Gomes Viana edited this page Apr 19, 2026 · 1 revision

Font System - Architecture and Theming

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.

Overview

The font system has two layers:

  • Build-time: font files + JSON config are assembled by Soong into a single font_fallback.xml installed at /system/etc/. OEMs can extend this via fonts_customization.xml installed on /product or /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.

Build-time configuration

From fonts.xml to font_fallback.xml

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.

Font Soong modules

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_font installs a TTF/OTF file to the right fonts directory for the partition
  • filegroup exposes fonts_customization.xml to genrules (see below)
  • runtime_resource_overlay builds an APK that changes config_*FontFamily strings

fonts_customization.xml (OEM/product layer)

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.

Runtime resolution

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

How the font map is built

  1. On boot, FontManagerService calls SystemFonts.buildSystemFallback(fontConfig, ...) with the parsed config (base + fonts_customization.xml + any runtime additions from /data/fonts).
  2. SystemFonts.buildSystemTypefaces turns the resolved families into a Map<String, Typeface> keyed by family name.
  3. The map is serialized into SharedMemory and handed to each app process on launch via FontManagerInternal.getSerializedSystemFontMap()bindApplicationTypeface.setSystemFontMapNative.
  4. All processes for a given boot share the same map. Changes to the font config require process restart to take effect.

Named families vs aliases

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 the sans-serif family with weight 500.
  • Typeface.create("nonexistent", NORMAL) returns null → TextView falls back to Typeface.DEFAULT. No error, no warning.

Apps: reading the config directly

SystemFonts.getAvailableFonts() returns the resolved font set without the app having to parse any XML. Preferred over reading /system/etc/font_fallback.xml directly.

Resource-backed indirection (config_*FontFamily)

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.

Clock font feature settings (OpenType features)

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)
  • ss02ss20: 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'"

Hardcoded family names (the theming gap)

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.

Inventory (AOSP 16 QPR2)

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-serif
  • sans-serif-medium
  • sans-serif-light
  • sans-serif-thin
  • sans-serif-black
  • sans-serif-regular
  • sans-serif-condensed
  • sans-serif-condensed-medium
  • sans-serif-condensed-light

GMS / Pixel names (defined via fonts_customization.xml on Pixel; absent on stock AOSP):

  • google-sans
  • google-sans-clock
  • google-sans-flex
  • google-sans-medium
  • google-sans-text
  • google-sans-text-medium
  • roboto-regular
  • font-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.

Programmatic calls with literal names

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.

Theming approaches

1. Override config_*FontFamily via RRO

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).

2. Inject aliases into the font map via fonts_customization.xml

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.

3. FontManagerService.installCustomFontFamily / setActiveCustomFontFamily

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.

4. Resource-backed alias shim

Combines approach 1 and approach 2 without needing a static fonts_customization.xml:

  1. 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).
  2. 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>).
  3. 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).

Key source files

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_*_materialconfig_*FontFamily redirection
packages/SystemUI/customization/src/.../DefaultClockController.kt Reads config_clockFontFamily and config_clockFontFeatureSettings

Debugging tips

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, removeCustomFontFamily all call updateSerializedFontMap().
  • setActiveCustomFontFamily in 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 sSystemFontMap is not mutated after bindApplication. 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 (ss01ss20), ligatures (liga, dlig), and other features are supported. Useful when setting fontFeatureSettings for a clock or any TextView.

Best practices

  • 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 wght axis, reducing system image size.
  • A runtime_resource_overlay Soong module is the cleanest way to ship config_*FontFamily overrides at build time; use FabricatedOverlay for per-user runtime overrides.
  • installCustomFontFamily rejects static fonts without a wght axis when enforcing variable-only fonts — design for that constraint if you're shipping a custom font that the user can pick at runtime.

Clone this wiki locally