diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 64b028ecc8d27..28c025545b0e7 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -258,6 +258,7 @@ import com.android.internal.util.Preconditions; import com.android.internal.util.StringCache; import com.android.internal.util.function.pooled.PooledLambda; +import com.android.internal.util.halcyon.FontController; import com.android.org.conscrypt.TrustedCertificateStore; import com.android.server.am.BitmapDumpProto; import com.android.server.am.MemInfoDumpProto; @@ -7187,6 +7188,8 @@ public void handleConfigurationChanged(Configuration config, int deviceId) { mConfigurationController.handleConfigurationChanged(config); updateDeviceIdForNonUIContexts(deviceId); + FontController.OnConfigurationChanged(getApplication().getResources()); + // These are only done to maintain @UnsupportedAppUsage and should be removed someday. mCurDefaultDisplayDpi = mConfigurationController.getCurDefaultDisplayDpi(); mConfiguration = mConfigurationController.getConfiguration(); @@ -7941,6 +7944,9 @@ private void handleBindApplication(AppBindData data) { data.info = getPackageInfo(data.appInfo, mCompatibilityInfo, null /* baseLoader */, false /* securityViolation */, true /* includeCode */, false /* registerPackage */, isSdkSandbox); + + FontController.OnConfigurationChanged(data.info.getResources()); + if (isSdkSandbox) { data.info.setSdkSandboxStorage(data.sdkSandboxClientAppVolumeUuid, data.sdkSandboxClientAppPackage); diff --git a/core/java/android/app/ConfigurationController.java b/core/java/android/app/ConfigurationController.java index f491e3d274dba..a58da795c9fc8 100644 --- a/core/java/android/app/ConfigurationController.java +++ b/core/java/android/app/ConfigurationController.java @@ -29,6 +29,9 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.HardwareRenderer; +import android.graphics.Typeface; +import android.inputmethodservice.InputMethodService; +import android.os.Build; import android.os.LocaleList; import android.os.Trace; import android.util.DisplayMetrics; @@ -198,6 +201,7 @@ private void handleConfigurationChangedInner(@Nullable Configuration config, final Application app = mActivityThread.getApplication(); final Resources appResources = app.getResources(); + Typeface.updateDefaultFont(appResources); mResourcesManager.applyConfigurationToResources(config, compat); updateLocaleListFromAppContext(app.getApplicationContext()); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 0589afd1185e7..695d5e2835a35 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -873,6 +873,7 @@ private void applyErrorDrawableIfNeeded(int layoutDirection) { // more bold. private int mFontWeightAdjustment; private Typeface mOriginalTypeface; + private String mFontFamily; // True if setKeyListener() has been explicitly called private boolean mListenerChanged = false; @@ -4388,6 +4389,7 @@ private void readTextAppearance(Context context, TypedArray appearance, attributes.mTypefaceIndex = appearance.getInt(attr, attributes.mTypefaceIndex); if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) { attributes.mFontFamily = null; + mFontFamily = null; } break; case com.android.internal.R.styleable.TextAppearance_fontFamily: @@ -4400,6 +4402,7 @@ private void readTextAppearance(Context context, TypedArray appearance, } if (attributes.mFontTypeface == null) { attributes.mFontFamily = appearance.getString(attr); + mFontFamily = attributes.mFontFamily; } attributes.mFontFamilyExplicit = true; break; @@ -4495,6 +4498,7 @@ private void applyTextAppearance(TextAppearanceAttributes attributes) { if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) { attributes.mFontFamily = null; + mFontFamily = null; } setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily, attributes.mTypefaceIndex, attributes.mTextStyle, attributes.mFontWeight); @@ -4528,6 +4532,10 @@ private void applyTextAppearance(TextAppearanceAttributes attributes) { setFontVariationSettings(attributes.mFontVariationSettings); } + if (Typeface.getFontName().equals("inter")) { + setFontFeatureSettings("'ss01'"); + } + if (attributes.mHasLineBreakStyle || attributes.mHasLineBreakWordStyle) { updateLineBreakConfigFromTextAppearance(attributes.mHasLineBreakStyle, attributes.mHasLineBreakWordStyle, attributes.mLineBreakStyle, @@ -4669,6 +4677,13 @@ protected void onConfigurationChanged(Configuration newConfig) { invalidate(); } } + + if (!TextUtils.equals(mFontFamily, Typeface.getFontName())) { + Typeface tf = Typeface.getOverrideTypeface(mFontFamily); + setTypeface(tf); + mFontFamily = Typeface.getFontName(); + } + if (mFontWeightAdjustment != newConfig.fontWeightAdjustment) { mFontWeightAdjustment = newConfig.fontWeightAdjustment; setTypeface(getTypeface()); diff --git a/core/java/com/android/internal/util/halcyon/FontController.java b/core/java/com/android/internal/util/halcyon/FontController.java new file mode 100644 index 0000000000000..560284908c11c --- /dev/null +++ b/core/java/com/android/internal/util/halcyon/FontController.java @@ -0,0 +1,183 @@ +/* + * Copyright (C) 2025 AxionOS + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.util.halcyon; + +import android.app.ActivityThread; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Typeface; +import android.os.SystemProperties; +import android.util.ArrayMap; +import android.util.Log; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.regex.Pattern; +import java.util.Set; + +public class FontController { + + private static final String TAG = "FontController"; + + private static FontController sInstance = null; + + private static final Set OVERRIDE_FONTS = new HashSet<>(Arrays.asList( + "google", "sans-serif", "gsf-" + )); + + private static final Set EXCLUDED_APPS = new HashSet<>(Arrays.asList( + "it.subito", + "tv.arte.plus7", + "com.google.android.gm" + )); + + private static final Map WEIGHT_MAP = new ArrayMap<>(); + static { + WEIGHT_MAP.put("thin", 100); + WEIGHT_MAP.put("extralight", 200); + WEIGHT_MAP.put("light", 300); + WEIGHT_MAP.put("normal", 400); + WEIGHT_MAP.put("regular", 400); + WEIGHT_MAP.put("medium", 500); + WEIGHT_MAP.put("semibold", 600); + WEIGHT_MAP.put("bold", 700); + WEIGHT_MAP.put("extrabold", 800); + WEIGHT_MAP.put("black", 900); + } + + public static FontController get() { + if (sInstance == null) { + sInstance = new FontController(); + } + return sInstance; + } + + private FontController() {} + + public static void OnConfigurationChanged(Resources res) { + get().handleOnConfiguration(res); + } + + public static Typeface getOverrideTypeface(String fontToOverride) { + if (fontToOverride == null) return null; + + String pkgName = getCurrentPackageName(); + if (pkgName != null && EXCLUDED_APPS.contains(pkgName)) { + logger("Excluded app, skipping override: " + pkgName); + return null; + } + + String currentFont = Typeface.getFontName(); + + if (fontToOverride.matches("^" + Pattern.quote(currentFont) + "(-.*)?$")) { + logger(fontToOverride + " matches current font root '" + currentFont + "', skipping override!"); + return null; + } + + boolean override = OVERRIDE_FONTS.stream().anyMatch(fontToOverride::contains); + if (!override) { + logger("Not on override list, skipping override: " + fontToOverride); + return null; + } + + int adjustment = getFontWeightAdjustment(); + return TypefaceFactory.create(fontToOverride, currentFont, adjustment); + } + + private void handleOnConfiguration(Resources res) { + String pkgName = getCurrentPackageName(); + if (pkgName == null || EXCLUDED_APPS.contains(pkgName)) return; + logger("handleOnConfiguration: Changing default font to: " + Typeface.getFontName()); + Typeface.changeFont(res); + } + + private static String getCurrentPackageName() { + try { + return ActivityThread.currentPackageName(); + } catch (Exception e) { + logger("getCurrentPackageName failed: " + e.getMessage()); + return null; + } + } + + private static Resources getResources() { + try { + return Resources.getSystem(); + } catch (Exception e) { + logger("getResources failed: " + e.getMessage()); + return null; + } + } + + private static int getFontWeightAdjustment() { + try { + Resources res = getResources(); + if (res == null) return 0; + Configuration cfg = res.getConfiguration(); + return cfg != null ? cfg.fontWeightAdjustment : 0; + } catch (Exception e) { + logger("getFontWeightAdjustment failed: " + e.getMessage()); + return 0; + } + } + + private static void logger(String msg) { + if (SystemProperties.getBoolean("persist.sys.ax_font_debug", false)) { + Log.d(TAG, msg); + } + } + + private static class TypefaceFactory { + + public static Typeface create(String fontToOverride, String currentFont, int fontWeightAdjustment) { + int weight = resolveWeightByName(fontToOverride); + + if (fontWeightAdjustment != 0) { + weight = Math.min(1000, Math.max(100, weight + fontWeightAdjustment)); + } + + boolean isBold = weight >= 700; + boolean isItalic = fontToOverride.contains("italic"); + + int style = Typeface.NORMAL; + if (isBold && isItalic) style = Typeface.BOLD_ITALIC; + else if (isBold) style = Typeface.BOLD; + else if (isItalic) style = Typeface.ITALIC; + + Typeface base = Typeface.getSystemDefaultTypeface(currentFont); + Typeface result = Typeface.create(base, style); + result = Typeface.create(result, weight, isItalic); + + logger("TypefaceFactory.create: fontToOverride=" + fontToOverride + + ", style=" + style + + ", weight=" + weight + + ", adj=" + fontWeightAdjustment + + ", isItalic=" + isItalic + + ", success=" + (result != null)); + + return result; + } + + private static int resolveWeightByName(String familyName) { + for (Map.Entry entry : WEIGHT_MAP.entrySet()) { + if (familyName.contains(entry.getKey())) { + return entry.getValue(); + } + } + return 400; + } + } +} diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index 6f94566599bd7..dda318a1e05a7 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -29,6 +29,7 @@ import android.annotation.UiThread; import android.compat.annotation.UnsupportedAppUsage; import android.content.res.AssetManager; +import android.content.res.Resources; import android.graphics.fonts.Font; import android.graphics.fonts.FontFamily; import android.graphics.fonts.FontStyle; @@ -57,6 +58,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; +import com.android.internal.util.halcyon.FontController; import com.android.text.flags.Flags; import dalvik.annotation.optimization.CriticalNative; @@ -71,12 +73,14 @@ import java.io.InputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -169,6 +173,9 @@ public static void clearTypefaceCachesForTestingPurpose() { } } + // For dynamic default font styles + private static final HashMap sSystemFontOverrides = new HashMap<>(); + @GuardedBy("SYSTEM_FONT_MAP_LOCK") static Typeface sDefaultTypeface; @@ -178,7 +185,7 @@ public static void clearTypefaceCachesForTestingPurpose() { */ @GuardedBy("SYSTEM_FONT_MAP_LOCK") @UnsupportedAppUsage(trackingBug = 123769347) - static final Map sSystemFontMap = new ArrayMap<>(); + static final Map sSystemFontMap = new HashMap<>(); // DirectByteBuffer object to hold sSystemFontMap's backing memory mapping. static ByteBuffer sSystemFontMapBuffer = null; @@ -315,6 +322,8 @@ private void completeTypefaceInitialization(@NonNull Typeface initializedTypefac } } + private static volatile String sFontName = DEFAULT_FAMILY; + // Style value for building typeface. private static final int STYLE_NORMAL = 0; private static final int STYLE_ITALIC = 1; @@ -997,7 +1006,7 @@ public CustomFallbackBuilder(@NonNull FontFamily family) { * @return The best matching typeface. */ public static Typeface create(String familyName, @Style int style) { - return create(getSystemDefaultTypeface(familyName), style); + return create(getOverrideTypeface(familyName), style); } /** @@ -1380,7 +1389,14 @@ public void releaseNativeObjectForTest() { mCleaner.run(); } - private static Typeface getSystemDefaultTypeface(@NonNull String familyName) { + /** @hide */ + public static Typeface getOverrideTypeface(@NonNull String familyName) { + Typeface tf = FontController.getOverrideTypeface(familyName); + return tf == null ? getSystemDefaultTypeface(familyName) : tf; + } + + /** @hide */ + public static Typeface getSystemDefaultTypeface(@NonNull String familyName) { Typeface tf = sSystemFontMap.get(familyName); return tf == null ? Typeface.DEFAULT : tf; } @@ -1561,6 +1577,74 @@ public static void setSystemFontMap(@Nullable SharedMemory sharedMemory) } } + private static void setPublicDefaults(String familyName) { + synchronized (SYSTEM_FONT_MAP_LOCK) { + sDefaults = new Typeface[] { + DEFAULT, + DEFAULT_BOLD, + create(getSystemDefaultTypeface(familyName), Typeface.ITALIC), + create(getSystemDefaultTypeface(familyName), Typeface.BOLD_ITALIC), + }; + } + } + + private static void setFinalField(String fieldName, Typeface value) { + synchronized (SYSTEM_FONT_MAP_LOCK) { + try { + Field field = Typeface.class.getDeclaredField(fieldName); + // isAccessible bypasses final on ART + field.setAccessible(true); + field.set(null, value); + field.setAccessible(false); + } catch (NoSuchFieldException | IllegalAccessException e) { + Log.e(TAG, "Failed to set Typeface." + fieldName, e); + } + } + } + + /** @hide */ + public static void updateDefaultFont(Resources res) { + synchronized (SYSTEM_FONT_MAP_LOCK) { + String familyName = res.getString(com.android.internal.R.string.config_bodyFontFamily); + String headlineFamilyName = res.getString( + com.android.internal.R.string.config_headlineFontFamily); + Typeface typeface = sSystemFontMap.get(familyName); + Typeface headlineTypeface = sSystemFontMap.get(headlineFamilyName); + if (typeface == null) { + // This should never happen, but if the system font family name is invalid, just + // return instead of crashing the app. + return; + } + if (headlineTypeface == null) { + // Fallback to body font + headlineTypeface = typeface; + } + + setDefault(typeface); + + // Static typefaces in public API + setFinalField("DEFAULT", create(getSystemDefaultTypeface(familyName), 0)); + setFinalField("DEFAULT_BOLD", create(getSystemDefaultTypeface(familyName), Typeface.BOLD)); + setFinalField("SANS_SERIF", DEFAULT); + + // For default aliases used in framework styles + sSystemFontOverrides.put("sans-serif", typeface); + sSystemFontOverrides.put("sans-serif-thin", create(typeface, 100, false)); + sSystemFontOverrides.put("sans-serif-light", create(typeface, 300, false)); + sSystemFontOverrides.put("sans-serif-medium", create(typeface, 500, false)); + sSystemFontOverrides.put("sans-serif-black", create(typeface, 900, false)); + + setPublicDefaults(familyName); + + // Replace google fonts + sSystemFontOverrides.put("google-sans-text", typeface); + sSystemFontOverrides.put("google-sans-text-medium", create(typeface, 500, false)); + + sSystemFontOverrides.put("google-sans", headlineTypeface); + sSystemFontOverrides.put("google-sans-medium", create(headlineTypeface, 500, false)); + } + } + /** @hide */ @GuardedBy("SYSTEM_FONT_MAP_LOCK") @VisibleForTesting @@ -1584,6 +1668,42 @@ public static void initializePendingTypefaceLocked(Typeface pending, String fami systemFontMap.put(familyName, pending); } + /** @hide */ + public static void changeFont(Resources res) { + if (res == null) { + return; + } + + synchronized (sDynamicCacheLock) { + sDynamicTypefaceCache.evictAll(); + } + + int configId = res.getIdentifier("config_bodyFontFamily", "string", "android"); + String fontFamily = res.getString(configId); + + sFontName = fontFamily; + + Typeface tf = getOverrideTypeface(sFontName); + + Typeface tfBold = create(tf, BOLD); + Typeface tfItalic = create(tf, ITALIC); + Typeface tfItalicBold = create(tf, BOLD_ITALIC); + + nativeForceSetStaticFinalField("DEFAULT", tf); + nativeForceSetStaticFinalField("DEFAULT_BOLD", tfBold); + nativeForceSetStaticFinalField("SANS_SERIF", tf); + + changeDefaultFontForTest( + Arrays.asList( + tf, tfBold, tfItalic, tfItalicBold), + Arrays.asList(tf, Typeface.SERIF, Typeface.MONOSPACE)); + } + + /** @hide */ + public static String getFontName() { + return sFontName; + } + /** @hide */ @VisibleForTesting public static void setSystemFontMap(Map systemFontMap) { @@ -1639,12 +1759,7 @@ public static void setSystemFontMap(Map systemFontMap) { create("monospace", Typeface.NORMAL)); } - sDefaults = new Typeface[]{ - DEFAULT, - DEFAULT_BOLD, - create((String) null, Typeface.ITALIC), - create((String) null, Typeface.BOLD_ITALIC), - }; + setPublicDefaults(null); // A list of generic families to be registered in native. // https://www.w3.org/TR/css-fonts-4/#generic-font-families @@ -1719,6 +1834,9 @@ public static void init() { preloadFontFile(SystemFonts.SYSTEM_FONT_DIR + "Roboto-Regular.ttf"); preloadFontFile(SystemFonts.SYSTEM_FONT_DIR + "RobotoStatic-Regular.ttf"); + preloadFontFile(SystemFonts.SYSTEM_FONT_DIR + "GoogleSans-Regular.ttf"); + preloadFontFile(SystemFonts.SYSTEM_FONT_DIR + "GoogleSans-Italic.ttf"); + String locale = SystemProperties.get("persist.sys.locale", "en-US"); String script = ULocale.addLikelySubtags(ULocale.forLanguageTag(locale)).getScript();