Merge "Fix minimum line height for TextView use case" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index cc8a213..9c3196d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -1804,6 +1804,7 @@
     field public static final int useEmbeddedDex = 16844190; // 0x101059e
     field public static final int useIntrinsicSizeAsMinimum = 16843536; // 0x1010310
     field public static final int useLevel = 16843167; // 0x101019f
+    field @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public static final int useLocalePreferredLineHeightForMinimum;
     field public static final int userVisible = 16843409; // 0x1010291
     field public static final int usesCleartextTraffic = 16844012; // 0x10104ec
     field public static final int usesPermissionFlags = 16844356; // 0x1010644
@@ -60535,6 +60536,7 @@
     method public boolean isFallbackLineSpacing();
     method public final boolean isHorizontallyScrollable();
     method public boolean isInputMethodTarget();
+    method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public boolean isLocalePreferredLineHeightForMinimumUsed();
     method public boolean isSingleLine();
     method public boolean isSuggestionsEnabled();
     method public boolean isTextSelectable();
@@ -60617,6 +60619,7 @@
     method public final void setLinkTextColor(@ColorInt int);
     method public final void setLinkTextColor(android.content.res.ColorStateList);
     method public final void setLinksClickable(boolean);
+    method @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") public void setLocalePreferredLineHeightForMinimumUsed(boolean);
     method public void setMarqueeRepeatLimit(int);
     method public void setMaxEms(int);
     method public void setMaxHeight(int);
diff --git a/core/java/android/text/BoringLayout.java b/core/java/android/text/BoringLayout.java
index a6d3bb4..6410609 100644
--- a/core/java/android/text/BoringLayout.java
+++ b/core/java/android/text/BoringLayout.java
@@ -585,9 +585,7 @@
         }
 
         if (ClientFlags.fixLineHeightForLocale()) {
-            if (minimumFontMetrics == null) {
-                paint.getFontMetricsIntForLocale(fm);
-            } else {
+            if (minimumFontMetrics != null) {
                 fm.set(minimumFontMetrics);
                 // Because the font metrics is provided by public APIs, adjust the top/bottom with
                 // ascent/descent: top must be smaller than ascent, bottom must be larger than
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 99bd2ff..5986238 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -767,22 +767,14 @@
         }
 
         int defaultTop;
-        int defaultAscent;
-        int defaultDescent;
+        final int defaultAscent;
+        final int defaultDescent;
         int defaultBottom;
-        if (ClientFlags.fixLineHeightForLocale()) {
-            if (b.mMinimumFontMetrics != null) {
-                defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top);
-                defaultAscent = Math.round(b.mMinimumFontMetrics.ascent);
-                defaultDescent = Math.round(b.mMinimumFontMetrics.descent);
-                defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom);
-            } else {
-                paint.getFontMetricsIntForLocale(fm);
-                defaultTop = fm.top;
-                defaultAscent = fm.ascent;
-                defaultDescent = fm.descent;
-                defaultBottom = fm.bottom;
-            }
+        if (ClientFlags.fixLineHeightForLocale() && b.mMinimumFontMetrics != null) {
+            defaultTop = (int) Math.floor(b.mMinimumFontMetrics.top);
+            defaultAscent = Math.round(b.mMinimumFontMetrics.ascent);
+            defaultDescent = Math.round(b.mMinimumFontMetrics.descent);
+            defaultBottom = (int) Math.ceil(b.mMinimumFontMetrics.bottom);
 
             // Because the font metrics is provided by public APIs, adjust the top/bottom with
             // ascent/descent: top must be smaller than ascent, bottom must be larger than descent.
@@ -1043,10 +1035,10 @@
 
                     if (endPos < spanEnd) {
                         // preserve metrics for current span
-                        fmTop = fm.top;
-                        fmBottom = fm.bottom;
-                        fmAscent = fm.ascent;
-                        fmDescent = fm.descent;
+                        fmTop = Math.min(defaultTop, fm.top);
+                        fmBottom = Math.max(defaultBottom, fm.bottom);
+                        fmAscent = Math.min(defaultAscent, fm.ascent);
+                        fmDescent = Math.max(defaultDescent, fm.descent);
                     } else {
                         fmTop = fmBottom = fmAscent = fmDescent = 0;
                     }
@@ -1069,7 +1061,7 @@
                 && mLineCount < mMaximumVisibleLineCount) {
             final MeasuredParagraph measuredPara =
                     MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null);
-            if (ClientFlags.fixLineHeightForLocale()) {
+            if (defaultAscent != 0 && defaultDescent != 0) {
                 fm.top = defaultTop;
                 fm.ascent = defaultAscent;
                 fm.descent = defaultDescent;
diff --git a/core/java/android/widget/EditText.java b/core/java/android/widget/EditText.java
index aa2474d7..3e0161a 100644
--- a/core/java/android/widget/EditText.java
+++ b/core/java/android/widget/EditText.java
@@ -16,9 +16,13 @@
 
 package android.widget;
 
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.content.Context;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.os.Build;
 import android.text.Editable;
 import android.text.Selection;
 import android.text.Spannable;
@@ -29,6 +33,8 @@
 import android.util.AttributeSet;
 import android.view.KeyEvent;
 
+import com.android.internal.R;
+
 /*
  * This is supposed to be a *very* thin veneer over TextView.
  * Do not make any changes here that do anything that a TextView
@@ -85,6 +91,11 @@
     private static final int ID_ITALIC = android.R.id.italic;
     private static final int ID_UNDERLINE = android.R.id.underline;
 
+    /** @hide */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    public static final long LINE_HEIGHT_FOR_LOCALE = 303326708L;
+
     public EditText(Context context) {
         this(context, null);
     }
@@ -104,15 +115,39 @@
         final TypedArray a = theme.obtainStyledAttributes(attrs,
                 com.android.internal.R.styleable.EditText, defStyleAttr, defStyleRes);
 
-        final int n = a.getIndexCount();
-        for (int i = 0; i < n; ++i) {
-            int attr = a.getIndex(i);
-            switch (attr) {
-                case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts:
-                    mStyleShortcutsEnabled = a.getBoolean(attr, false);
-                    break;
+        try {
+            final int n = a.getIndexCount();
+            for (int i = 0; i < n; ++i) {
+                int attr = a.getIndex(i);
+                switch (attr) {
+                    case com.android.internal.R.styleable.EditText_enableTextStylingShortcuts:
+                        mStyleShortcutsEnabled = a.getBoolean(attr, false);
+                        break;
+                }
             }
+        } finally {
+            a.recycle();
         }
+
+        boolean hasUseLocalePreferredLineHeightForMinimumInt = false;
+        boolean useLocalePreferredLineHeightForMinimumInt = false;
+        TypedArray tvArray = theme.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
+        try {
+            hasUseLocalePreferredLineHeightForMinimumInt =
+                    tvArray.hasValue(R.styleable.TextView_useLocalePreferredLineHeightForMinimum);
+            if (hasUseLocalePreferredLineHeightForMinimumInt) {
+                useLocalePreferredLineHeightForMinimumInt = tvArray.getBoolean(
+                        R.styleable.TextView_useLocalePreferredLineHeightForMinimum, false);
+            }
+        } finally {
+            tvArray.recycle();
+        }
+        if (!hasUseLocalePreferredLineHeightForMinimumInt) {
+            useLocalePreferredLineHeightForMinimumInt =
+                    CompatChanges.isChangeEnabled(LINE_HEIGHT_FOR_LOCALE);
+        }
+        setLocalePreferredLineHeightForMinimumUsed(useLocalePreferredLineHeightForMinimumInt);
     }
 
     @Override
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 9a4106d9..57e4e6a 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -867,6 +867,8 @@
 
     private boolean mUseBoundsForWidth;
     @Nullable private Paint.FontMetrics mMinimumFontMetrics;
+    @Nullable private Paint.FontMetrics mLocalePreferredFontMetrics;
+    private boolean mUseLocalePreferredLineHeightForMinimum;
 
     @ViewDebug.ExportedProperty(category = "text")
     @UnsupportedAppUsage
@@ -1617,6 +1619,11 @@
                 case com.android.internal.R.styleable.TextView_useBoundsForWidth:
                     mUseBoundsForWidth = a.getBoolean(attr, false);
                     hasUseBoundForWidthValue = true;
+                    break;
+                case com.android.internal.R.styleable
+                        .TextView_useLocalePreferredLineHeightForMinimum:
+                    mUseLocalePreferredLineHeightForMinimum = a.getBoolean(attr, false);
+                    break;
             }
         }
 
@@ -4992,6 +4999,41 @@
     }
 
     /**
+     * Returns true if the locale preferred line height is used for the minimum line height.
+     *
+     * @return true if using locale preferred line height for the minimum line height. Otherwise
+     *         false.
+     *
+     * @see #setLocalePreferredLineHeightForMinimumUsed(boolean)
+     * @see #setMinimumFontMetrics(Paint.FontMetrics)
+     * @see #getMinimumFontMetrics()
+     */
+    @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
+    public boolean isLocalePreferredLineHeightForMinimumUsed() {
+        return mUseLocalePreferredLineHeightForMinimum;
+    }
+
+    /**
+     * Set true if the locale preferred line height is used for the minimum line height.
+     *
+     * By setting this flag to true is equivalenet to call
+     * {@link #setMinimumFontMetrics(Paint.FontMetrics)} with the one obtained by
+     * {@link Paint#getFontMetricsForLocale(Paint.FontMetrics)}.
+     *
+     * If custom minimum line height was specified by
+     * {@link #setMinimumFontMetrics(Paint.FontMetrics)}, this flag will be ignored.
+     *
+     * @param flag true for using locale preferred line height for the minimum line height.
+     * @see #isLocalePreferredLineHeightForMinimumUsed()
+     * @see #setMinimumFontMetrics(Paint.FontMetrics)
+     * @see #getMinimumFontMetrics()
+     */
+    @FlaggedApi(FLAG_FIX_LINE_HEIGHT_FOR_LOCALE)
+    public void setLocalePreferredLineHeightForMinimumUsed(boolean flag) {
+        mUseLocalePreferredLineHeightForMinimum = flag;
+    }
+
+    /**
      * @return whether fallback line spacing is enabled, {@code true} by default
      *
      * @see #setFallbackLineSpacing(boolean)
@@ -10728,6 +10770,21 @@
         return alignment;
     }
 
+    private Paint.FontMetrics getResolvedMinimumFontMetrics() {
+        if (mMinimumFontMetrics != null) {
+            return mMinimumFontMetrics;
+        }
+        if (!mUseLocalePreferredLineHeightForMinimum) {
+            return null;
+        }
+
+        if (mLocalePreferredFontMetrics == null) {
+            mLocalePreferredFontMetrics = new Paint.FontMetrics();
+        }
+        mTextPaint.getFontMetricsForLocale(mLocalePreferredFontMetrics);
+        return mLocalePreferredFontMetrics;
+    }
+
     /**
      * The width passed in is now the desired layout width,
      * not the full view width with padding.
@@ -10792,7 +10849,8 @@
             if (hintBoring == UNKNOWN_BORING) {
                 hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
                         isFallbackLineSpacingForBoringLayout(),
-                        mMinimumFontMetrics, mHintBoring);
+                        getResolvedMinimumFontMetrics(), mHintBoring);
+
                 if (hintBoring != null) {
                     mHintBoring = hintBoring;
                 }
@@ -10842,7 +10900,8 @@
                         .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
                                 mLineBreakStyle, mLineBreakWordStyle))
                         .setUseBoundsForWidth(mUseBoundsForWidth)
-                        .setMinimumFontMetrics(mMinimumFontMetrics);
+                        .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
+
                 if (shouldEllipsize) {
                     builder.setEllipsize(mEllipsize)
                             .setEllipsizedWidth(ellipsisWidth);
@@ -10907,12 +10966,13 @@
                     .setUseBoundsForWidth(mUseBoundsForWidth)
                     .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null)
                     .setEllipsizedWidth(ellipsisWidth)
-                    .setMinimumFontMetrics(mMinimumFontMetrics);
+                    .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
             result = builder.build();
         } else {
             if (boring == UNKNOWN_BORING) {
                 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir,
-                        isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring);
+                        isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
+                        mBoring);
                 if (boring != null) {
                     mBoring = boring;
                 }
@@ -10926,7 +10986,7 @@
                                 wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                 boring, mIncludePad, null, wantWidth,
                                 isFallbackLineSpacingForBoringLayout(),
-                                mUseBoundsForWidth, mMinimumFontMetrics);
+                                mUseBoundsForWidth, getResolvedMinimumFontMetrics());
                     } else {
                         result = new BoringLayout(
                                 mTransformed,
@@ -10941,7 +11001,7 @@
                                 null,
                                 boring,
                                 mUseBoundsForWidth,
-                                mMinimumFontMetrics);
+                                getResolvedMinimumFontMetrics());
                     }
 
                     if (useSaved) {
@@ -10953,7 +11013,7 @@
                                 wantWidth, alignment, mSpacingMult, mSpacingAdd,
                                 boring, mIncludePad, effectiveEllipsize,
                                 ellipsisWidth, isFallbackLineSpacingForBoringLayout(),
-                                mUseBoundsForWidth, mMinimumFontMetrics);
+                                mUseBoundsForWidth, getResolvedMinimumFontMetrics());
                     } else {
                         result = new BoringLayout(
                                 mTransformed,
@@ -10968,7 +11028,7 @@
                                 effectiveEllipsize,
                                 boring,
                                 mUseBoundsForWidth,
-                                mMinimumFontMetrics);
+                                getResolvedMinimumFontMetrics());
                     }
                 }
             }
@@ -10988,7 +11048,7 @@
                     .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
                             mLineBreakStyle, mLineBreakWordStyle))
                     .setUseBoundsForWidth(mUseBoundsForWidth)
-                    .setMinimumFontMetrics(mMinimumFontMetrics);
+                    .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
             if (shouldEllipsize) {
                 builder.setEllipsize(effectiveEllipsize)
                         .setEllipsizedWidth(ellipsisWidth);
@@ -11116,7 +11176,8 @@
 
             if (des < 0) {
                 boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir,
-                        isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics, mBoring);
+                        isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
+                        mBoring);
                 if (boring != null) {
                     mBoring = boring;
                 }
@@ -11156,7 +11217,7 @@
 
                 if (hintDes < 0) {
                     hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
-                            isFallbackLineSpacingForBoringLayout(), mMinimumFontMetrics,
+                            isFallbackLineSpacingForBoringLayout(), getResolvedMinimumFontMetrics(),
                             mHintBoring);
                     if (hintBoring != null) {
                         mHintBoring = hintBoring;
@@ -11370,7 +11431,7 @@
                 .setLineBreakConfig(LineBreakConfig.getLineBreakConfig(
                         mLineBreakStyle, mLineBreakWordStyle))
                 .setUseBoundsForWidth(mUseBoundsForWidth)
-                .setMinimumFontMetrics(mMinimumFontMetrics);
+                .setMinimumFontMetrics(getResolvedMinimumFontMetrics());
 
         final StaticLayout layout = layoutBuilder.build();
 
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 45861a3..41bc825 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5857,6 +5857,23 @@
           use glyph bound's as a source of text width.  -->
         <!-- @FlaggedApi("com.android.text.flags.use_bounds_for_width") -->
         <attr name="useBoundsForWidth" format="boolean" />
+        <!-- Whether to use the locale preferred line height for the minimum line height.
+
+          This flag is useful for preventing jitter of entering letters into empty EditText.
+          The line height of the text is determined by the font files used for drawing text in a
+          line. However, in case of the empty text case, the line height cannot be determined and
+          the default line height: usually it is came from a font of Latin script. By making this
+          attribute to true, the TextView/EditText uses a line height that is likely used for the
+          locale associated with the widget. For example, if the system locale is Japanese, the
+          height of the EditText will be adjusted to meet the height of the Japanese font even if
+          the text is empty.
+
+          The default value for EditText is true if targetSdkVersion is
+          {@link android.os.Build.VERSION_CODE#VANILLA_ICE_CREAM} or later, otherwise false.
+          For other TextViews, the default value is false.
+        -->
+        <!-- @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") -->
+        <attr name="useLocalePreferredLineHeightForMinimum" format="boolean" />
     </declare-styleable>
     <declare-styleable name="TextViewAppearance">
         <!-- Base text color, typeface, size, and style. -->
diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml
index 0a6779a9..5ee5555 100644
--- a/core/res/res/values/public-staging.xml
+++ b/core/res/res/values/public-staging.xml
@@ -153,6 +153,8 @@
     <public name="requireContentUriPermissionFromCaller" />
     <!-- @FlaggedApi("android.view.inputmethod.ime_switcher_revamp") -->
     <public name="languageSettingsActivity"/>
+    <!-- @FlaggedApi("com.android.text.flags.fix_line_height_for_locale") -->
+    <public name="useLocalePreferredLineHeightForMinimum"/>
   </staging-public-group>
 
   <staging-public-group type="id" first-id="0x01bc0000">