Add setLineBreakConfig API

Add new API to allow applications to specify the line break word style. The line break style is one of the locale extension. When the line break style is set, it will be brought to ICU for calculation.

Bug: 183780874
Test: atest minikin_tests; atest TextViewTest; atest MeasuredTextTest; atest PrecomputedTextTest
Change-Id: Ia9cdb5b83e346f96ae22abbb1bcce05c43207bba
diff --git a/core/api/current.txt b/core/api/current.txt
index 4f15fa9..d08286d 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -948,6 +948,7 @@
     field public static final int left = 16843181; // 0x10101ad
     field public static final int letterSpacing = 16843958; // 0x10104b6
     field public static final int level = 16844032; // 0x1010500
+    field public static final int lineBreakStyle = 16844365; // 0x101064d
     field public static final int lineHeight = 16844159; // 0x101057f
     field public static final int lineSpacingExtra = 16843287; // 0x1010217
     field public static final int lineSpacingMultiplier = 16843288; // 0x1010218
@@ -17520,6 +17521,17 @@
 
 package android.graphics.text {
 
+  public final class LineBreakConfig {
+    ctor public LineBreakConfig();
+    method public int getLineBreakStyle();
+    method public void set(@Nullable android.graphics.text.LineBreakConfig);
+    method public void setLineBreakStyle(int);
+    field public static final int LINE_BREAK_STYLE_LOOSE = 1; // 0x1
+    field public static final int LINE_BREAK_STYLE_NONE = 0; // 0x0
+    field public static final int LINE_BREAK_STYLE_NORMAL = 2; // 0x2
+    field public static final int LINE_BREAK_STYLE_STRICT = 3; // 0x3
+  }
+
   public class LineBreaker {
     method @NonNull public android.graphics.text.LineBreaker.Result computeLineBreaks(@NonNull android.graphics.text.MeasuredText, @NonNull android.graphics.text.LineBreaker.ParagraphConstraints, @IntRange(from=0) int);
     field public static final int BREAK_STRATEGY_BALANCED = 2; // 0x2
@@ -17575,6 +17587,7 @@
     ctor public MeasuredText.Builder(@NonNull android.graphics.text.MeasuredText);
     method @NonNull public android.graphics.text.MeasuredText.Builder appendReplacementRun(@NonNull android.graphics.Paint, @IntRange(from=0) int, @FloatRange(from=0) @Px float);
     method @NonNull public android.graphics.text.MeasuredText.Builder appendStyleRun(@NonNull android.graphics.Paint, @IntRange(from=0) int, boolean);
+    method @NonNull public android.graphics.text.MeasuredText.Builder appendStyleRun(@NonNull android.graphics.Paint, @Nullable android.graphics.text.LineBreakConfig, @IntRange(from=0) int, boolean);
     method @NonNull public android.graphics.text.MeasuredText build();
     method @Deprecated @NonNull public android.graphics.text.MeasuredText.Builder setComputeHyphenation(boolean);
     method @NonNull public android.graphics.text.MeasuredText.Builder setComputeHyphenation(int);
@@ -45283,6 +45296,7 @@
   public static final class PrecomputedText.Params {
     method public int getBreakStrategy();
     method public int getHyphenationFrequency();
+    method @Nullable public android.graphics.text.LineBreakConfig getLineBreakConfig();
     method @NonNull public android.text.TextDirectionHeuristic getTextDirection();
     method @NonNull public android.text.TextPaint getTextPaint();
   }
@@ -45293,6 +45307,7 @@
     method @NonNull public android.text.PrecomputedText.Params build();
     method public android.text.PrecomputedText.Params.Builder setBreakStrategy(int);
     method public android.text.PrecomputedText.Params.Builder setHyphenationFrequency(int);
+    method @NonNull public android.text.PrecomputedText.Params.Builder setLineBreakConfig(@NonNull android.graphics.text.LineBreakConfig);
     method public android.text.PrecomputedText.Params.Builder setTextDirection(@NonNull android.text.TextDirectionHeuristic);
   }
 
@@ -45453,6 +45468,7 @@
     method @NonNull public android.text.StaticLayout.Builder setIncludePad(boolean);
     method @NonNull public android.text.StaticLayout.Builder setIndents(@Nullable int[], @Nullable int[]);
     method @NonNull public android.text.StaticLayout.Builder setJustificationMode(int);
+    method @NonNull public android.text.StaticLayout.Builder setLineBreakConfig(@NonNull android.graphics.text.LineBreakConfig);
     method @NonNull public android.text.StaticLayout.Builder setLineSpacing(float, @FloatRange(from=0.0) float);
     method @NonNull public android.text.StaticLayout.Builder setMaxLines(@IntRange(from=0) int);
     method public android.text.StaticLayout.Builder setText(CharSequence);
@@ -57467,6 +57483,7 @@
     method public final android.text.Layout getLayout();
     method public float getLetterSpacing();
     method public int getLineBounds(int, android.graphics.Rect);
+    method @NonNull public android.graphics.text.LineBreakConfig getLineBreakConfig();
     method public int getLineCount();
     method public int getLineHeight();
     method public float getLineSpacingExtra();
@@ -57594,6 +57611,7 @@
     method public void setKeyListener(android.text.method.KeyListener);
     method public void setLastBaselineToBottomHeight(@IntRange(from=0) @Px int);
     method public void setLetterSpacing(float);
+    method public void setLineBreakConfig(@NonNull android.graphics.text.LineBreakConfig);
     method public void setLineHeight(@IntRange(from=0) @Px int);
     method public void setLineSpacing(float, float);
     method public void setLines(int);
diff --git a/core/java/android/text/MeasuredParagraph.java b/core/java/android/text/MeasuredParagraph.java
index 6a3c618..748d551 100644
--- a/core/java/android/text/MeasuredParagraph.java
+++ b/core/java/android/text/MeasuredParagraph.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.graphics.Paint;
 import android.graphics.Rect;
+import android.graphics.text.LineBreakConfig;
 import android.graphics.text.MeasuredText;
 import android.text.AutoGrowArray.ByteArray;
 import android.text.AutoGrowArray.FloatArray;
@@ -124,7 +125,7 @@
     // The native MeasuredParagraph.
     private @Nullable MeasuredText mMeasuredText;
 
-    // Following two objects are for avoiding object allocation.
+    // Following three objects are for avoiding object allocation.
     private @NonNull TextPaint mCachedPaint = new TextPaint();
     private @Nullable Paint.FontMetricsInt mCachedFm;
 
@@ -350,7 +351,8 @@
         if (mt.mSpanned == null) {
             // No style change by MetricsAffectingSpan. Just measure all text.
             mt.applyMetricsAffectingSpan(
-                    paint, null /* spans */, start, end, null /* native builder ptr */);
+                    paint, null /* lineBreakConfig */, null /* spans */, start, end,
+                    null /* native builder ptr */);
         } else {
             // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
             int spanEnd;
@@ -360,7 +362,8 @@
                         MetricAffectingSpan.class);
                 spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
                 mt.applyMetricsAffectingSpan(
-                        paint, spans, spanStart, spanEnd, null /* native builder ptr */);
+                        paint, null /* line break config */, spans, spanStart, spanEnd,
+                        null /* native builder ptr */);
             }
         }
         return mt;
@@ -373,6 +376,7 @@
      * result to recycle and returns recycle.
      *
      * @param paint the paint to be used for rendering the text.
+     * @param lineBreakConfig the line break configuration for text wrapping.
      * @param text the character sequence to be measured
      * @param start the inclusive start offset of the target region in the text
      * @param end the exclusive end offset of the target region in the text
@@ -386,6 +390,7 @@
      */
     public static @NonNull MeasuredParagraph buildForStaticLayout(
             @NonNull TextPaint paint,
+            @Nullable LineBreakConfig lineBreakConfig,
             @NonNull CharSequence text,
             @IntRange(from = 0) int start,
             @IntRange(from = 0) int end,
@@ -411,7 +416,8 @@
         } else {
             if (mt.mSpanned == null) {
                 // No style change by MetricsAffectingSpan. Just measure all text.
-                mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, builder);
+                mt.applyMetricsAffectingSpan(paint, lineBreakConfig, null /* spans */, start, end,
+                        builder);
                 mt.mSpanEndCache.append(end);
             } else {
                 // There may be a MetricsAffectingSpan. Split into span transitions and apply
@@ -424,7 +430,9 @@
                             MetricAffectingSpan.class);
                     spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
                                                        MetricAffectingSpan.class);
-                    mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd, builder);
+                    // TODO: Update line break config with spans.
+                    mt.applyMetricsAffectingSpan(paint, lineBreakConfig, spans, spanStart, spanEnd,
+                            builder);
                     mt.mSpanEndCache.append(spanEnd);
                 }
             }
@@ -500,12 +508,13 @@
     private void applyReplacementRun(@NonNull ReplacementSpan replacement,
                                      @IntRange(from = 0) int start,  // inclusive, in copied buffer
                                      @IntRange(from = 0) int end,  // exclusive, in copied buffer
+                                     @NonNull TextPaint paint,
                                      @Nullable MeasuredText.Builder builder) {
         // Use original text. Shouldn't matter.
         // TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
         //       backward compatibility? or Should we initialize them for getFontMetricsInt?
         final float width = replacement.getSize(
-                mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
+                paint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
         if (builder == null) {
             // Assigns all width to the first character. This is the same behavior as minikin.
             mWidths.set(start, width);
@@ -514,22 +523,24 @@
             }
             mWholeWidth += width;
         } else {
-            builder.appendReplacementRun(mCachedPaint, end - start, width);
+            builder.appendReplacementRun(paint, end - start, width);
         }
     }
 
     private void applyStyleRun(@IntRange(from = 0) int start,  // inclusive, in copied buffer
                                @IntRange(from = 0) int end,  // exclusive, in copied buffer
+                               @NonNull TextPaint paint,
+                               @Nullable LineBreakConfig config,
                                @Nullable MeasuredText.Builder builder) {
 
         if (mLtrWithoutBidi) {
             // If the whole text is LTR direction, just apply whole region.
             if (builder == null) {
-                mWholeWidth += mCachedPaint.getTextRunAdvances(
+                mWholeWidth += paint.getTextRunAdvances(
                         mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
                         mWidths.getRawArray(), start);
             } else {
-                builder.appendStyleRun(mCachedPaint, end - start, false /* isRtl */);
+                builder.appendStyleRun(paint, config, end - start, false /* isRtl */);
             }
         } else {
             // If there is multiple bidi levels, split into individual bidi level and apply style.
@@ -541,11 +552,11 @@
                     final boolean isRtl = (level & 0x1) != 0;
                     if (builder == null) {
                         final int levelLength = levelEnd - levelStart;
-                        mWholeWidth += mCachedPaint.getTextRunAdvances(
+                        mWholeWidth += paint.getTextRunAdvances(
                                 mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
                                 isRtl, mWidths.getRawArray(), levelStart);
                     } else {
-                        builder.appendStyleRun(mCachedPaint, levelEnd - levelStart, isRtl);
+                        builder.appendStyleRun(paint, config, levelEnd - levelStart, isRtl);
                     }
                     if (levelEnd == end) {
                         break;
@@ -559,6 +570,7 @@
 
     private void applyMetricsAffectingSpan(
             @NonNull TextPaint paint,
+            @Nullable LineBreakConfig lineBreakConfig,
             @Nullable MetricAffectingSpan[] spans,
             @IntRange(from = 0) int start,  // inclusive, in original text buffer
             @IntRange(from = 0) int end,  // exclusive, in original text buffer
@@ -595,9 +607,11 @@
         }
 
         if (replacement != null) {
-            applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, builder);
+            applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, mCachedPaint,
+                    builder);
         } else {
-            applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, builder);
+            applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, mCachedPaint,
+                    lineBreakConfig, builder);
         }
 
         if (needFontMetrics) {
diff --git a/core/java/android/text/PrecomputedText.java b/core/java/android/text/PrecomputedText.java
index 152570f..ce63376 100644
--- a/core/java/android/text/PrecomputedText.java
+++ b/core/java/android/text/PrecomputedText.java
@@ -22,6 +22,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.graphics.Rect;
+import android.graphics.text.LineBreakConfig;
 import android.graphics.text.MeasuredText;
 import android.text.style.MetricAffectingSpan;
 
@@ -96,6 +97,9 @@
         // The hyphenation frequency for this measured text.
         private final @Layout.HyphenationFrequency int mHyphenationFrequency;
 
+        // The line break configuration for calculating text wrapping.
+        private final @Nullable LineBreakConfig mLineBreakConfig;
+
         /**
          * A builder for creating {@link Params}.
          */
@@ -113,6 +117,9 @@
             private @Layout.HyphenationFrequency int mHyphenationFrequency =
                     Layout.HYPHENATION_FREQUENCY_NORMAL;
 
+            // The line break configuration for calculating text wrapping.
+            private @Nullable LineBreakConfig mLineBreakConfig;
+
             /**
              * Builder constructor.
              *
@@ -130,6 +137,7 @@
                 mTextDir = params.mTextDir;
                 mBreakStrategy = params.mBreakStrategy;
                 mHyphenationFrequency = params.mHyphenationFrequency;
+                mLineBreakConfig = params.mLineBreakConfig;
             }
 
             /**
@@ -177,24 +185,41 @@
             }
 
             /**
+             * Set the line break config for the text wrapping.
+             *
+             * @param lineBreakConfig the newly line break configuration.
+             * @return this builder, useful for chaining.
+             * @see StaticLayout.Builder#setLineBreakConfig
+             */
+            public @NonNull Builder setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) {
+                mLineBreakConfig = lineBreakConfig;
+                return this;
+            }
+
+            /**
              * Build the {@link Params}.
              *
              * @return the layout parameter
              */
             public @NonNull Params build() {
-                return new Params(mPaint, mTextDir, mBreakStrategy, mHyphenationFrequency);
+                return new Params(mPaint, mLineBreakConfig, mTextDir, mBreakStrategy,
+                        mHyphenationFrequency);
             }
         }
 
         // This is public hidden for internal use.
         // For the external developers, use Builder instead.
         /** @hide */
-        public Params(@NonNull TextPaint paint, @NonNull TextDirectionHeuristic textDir,
-                @Layout.BreakStrategy int strategy, @Layout.HyphenationFrequency int frequency) {
+        public Params(@NonNull TextPaint paint,
+                @Nullable LineBreakConfig lineBreakConfig,
+                @NonNull TextDirectionHeuristic textDir,
+                @Layout.BreakStrategy int strategy,
+                @Layout.HyphenationFrequency int frequency) {
             mPaint = paint;
             mTextDir = textDir;
             mBreakStrategy = strategy;
             mHyphenationFrequency = frequency;
+            mLineBreakConfig = lineBreakConfig;
         }
 
         /**
@@ -233,6 +258,15 @@
             return mHyphenationFrequency;
         }
 
+        /**
+         * Return the line break configuration for this text.
+         *
+         * @return the current line break configuration, null if no line break configuration is set.
+         */
+        public @Nullable LineBreakConfig getLineBreakConfig() {
+            return mLineBreakConfig;
+        }
+
         /** @hide */
         @IntDef(value = { UNUSABLE, NEED_RECOMPUTE, USABLE })
         @Retention(RetentionPolicy.SOURCE)
@@ -262,8 +296,9 @@
         /** @hide */
         public @CheckResultUsableResult int checkResultUsable(@NonNull TextPaint paint,
                 @NonNull TextDirectionHeuristic textDir, @Layout.BreakStrategy int strategy,
-                @Layout.HyphenationFrequency int frequency) {
+                @Layout.HyphenationFrequency int frequency, @Nullable LineBreakConfig lbConfig) {
             if (mBreakStrategy == strategy && mHyphenationFrequency == frequency
+                    && isLineBreakEquals(mLineBreakConfig, lbConfig)
                     && mPaint.equalsForTextMeasurement(paint)) {
                 return mTextDir == textDir ? USABLE : NEED_RECOMPUTE;
             } else {
@@ -272,6 +307,29 @@
         }
 
         /**
+         * Check the two LineBreakConfig instances are equal.
+         * This method assumes they are equal if one parameter is null and the other parameter has
+         * a LineBreakStyle value of LineBreakConfig.LINE_BREAK_STYLE_NONE.
+         *
+         * @param o1 the first LineBreakConfig instance.
+         * @param o2 the second LineBreakConfig instance.
+         * @return true if the two LineBreakConfig instances are equal.
+         */
+        private boolean isLineBreakEquals(LineBreakConfig o1, LineBreakConfig o2) {
+            if (Objects.equals(o1, o2)) {
+                return true;
+            }
+            if (o1 == null && (o2 != null
+                    && o2.getLineBreakStyle() == LineBreakConfig.LINE_BREAK_STYLE_NONE)) {
+                return true;
+            } else if (o2 == null && (o1 != null
+                    && o1.getLineBreakStyle() == LineBreakConfig.LINE_BREAK_STYLE_NONE)) {
+                return true;
+            }
+            return false;
+        }
+
+        /**
          * Check if the same text layout.
          *
          * @return true if this and the given param result in the same text layout
@@ -286,21 +344,25 @@
             }
             Params param = (Params) o;
             return checkResultUsable(param.mPaint, param.mTextDir, param.mBreakStrategy,
-                    param.mHyphenationFrequency) == Params.USABLE;
+                    param.mHyphenationFrequency, param.mLineBreakConfig) == Params.USABLE;
         }
 
         @Override
         public int hashCode() {
             // TODO: implement MinikinPaint::hashCode and use it to keep consistency with equals.
+            int lineBreakStyle = (mLineBreakConfig != null)
+                    ? mLineBreakConfig.getLineBreakStyle() : LineBreakConfig.LINE_BREAK_STYLE_NONE;
             return Objects.hash(mPaint.getTextSize(), mPaint.getTextScaleX(), mPaint.getTextSkewX(),
                     mPaint.getLetterSpacing(), mPaint.getWordSpacing(), mPaint.getFlags(),
                     mPaint.getTextLocales(), mPaint.getTypeface(),
                     mPaint.getFontVariationSettings(), mPaint.isElegantTextHeight(), mTextDir,
-                    mBreakStrategy, mHyphenationFrequency);
+                    mBreakStrategy, mHyphenationFrequency, lineBreakStyle);
         }
 
         @Override
         public String toString() {
+            int lineBreakStyle = (mLineBreakConfig != null)
+                    ? mLineBreakConfig.getLineBreakStyle() : LineBreakConfig.LINE_BREAK_STYLE_NONE;
             return "{"
                 + "textSize=" + mPaint.getTextSize()
                 + ", textScaleX=" + mPaint.getTextScaleX()
@@ -313,6 +375,7 @@
                 + ", textDir=" + mTextDir
                 + ", breakStrategy=" + mBreakStrategy
                 + ", hyphenationFrequency=" + mHyphenationFrequency
+                + ", lineBreakStyle=" + lineBreakStyle
                 + "}";
         }
     };
@@ -369,7 +432,8 @@
             final PrecomputedText.Params hintParams = hintPct.getParams();
             final @Params.CheckResultUsableResult int checkResult =
                     hintParams.checkResultUsable(params.mPaint, params.mTextDir,
-                            params.mBreakStrategy, params.mHyphenationFrequency);
+                            params.mBreakStrategy, params.mHyphenationFrequency,
+                            params.mLineBreakConfig);
             switch (checkResult) {
                 case Params.USABLE:
                     return hintPct;
@@ -418,9 +482,9 @@
             final int paraStart = pct.getParagraphStart(i);
             final int paraEnd = pct.getParagraphEnd(i);
             result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
-                    params.getTextPaint(), pct, paraStart, paraEnd, params.getTextDirection(),
-                    hyphenationMode, computeLayout, pct.getMeasuredParagraph(i),
-                    null /* no recycle */)));
+                    params.getTextPaint(), params.getLineBreakConfig(), pct, paraStart, paraEnd,
+                    params.getTextDirection(), hyphenationMode, computeLayout,
+                    pct.getMeasuredParagraph(i), null /* no recycle */)));
         }
         return result.toArray(new ParagraphInfo[result.size()]);
     }
@@ -456,8 +520,9 @@
             }
 
             result.add(new ParagraphInfo(paraEnd, MeasuredParagraph.buildForStaticLayout(
-                    params.getTextPaint(), text, paraStart, paraEnd, params.getTextDirection(),
-                    hyphenationMode, computeLayout, null /* no hint */, null /* no recycle */)));
+                    params.getTextPaint(), params.getLineBreakConfig(), text, paraStart, paraEnd,
+                    params.getTextDirection(), hyphenationMode, computeLayout, null /* no hint */,
+                    null /* no recycle */)));
         }
         return result.toArray(new ParagraphInfo[result.size()]);
     }
@@ -544,11 +609,11 @@
     public @Params.CheckResultUsableResult int checkResultUsable(@IntRange(from = 0) int start,
             @IntRange(from = 0) int end, @NonNull TextDirectionHeuristic textDir,
             @NonNull TextPaint paint, @Layout.BreakStrategy int strategy,
-            @Layout.HyphenationFrequency int frequency) {
+            @Layout.HyphenationFrequency int frequency, @NonNull LineBreakConfig lbConfig) {
         if (mStart != start || mEnd != end) {
             return Params.UNUSABLE;
         } else {
-            return mParams.checkResultUsable(paint, textDir, strategy, frequency);
+            return mParams.checkResultUsable(paint, textDir, strategy, frequency, lbConfig);
         }
     }
 
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index b1bc766..b10fc37 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -22,6 +22,7 @@
 import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.graphics.Paint;
+import android.graphics.text.LineBreakConfig;
 import android.graphics.text.LineBreaker;
 import android.os.Build;
 import android.text.style.LeadingMarginSpan;
@@ -403,6 +404,21 @@
         }
 
         /**
+         * Set the line break configuration. The line break will be passed to native used for
+         * calculating the text wrapping. The default value of the line break style is
+         * {@link LineBreakConfig#LINE_BREAK_STYLE_NONE}
+         *
+         * @param lineBreakConfig the line break configuration for text wrapping.
+         * @return this builder, useful for chaining.
+         * @see android.widget.TextView#setLineBreakConfig
+         */
+        @NonNull
+        public Builder setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) {
+            mLineBreakConfig = lineBreakConfig;
+            return this;
+        }
+
+        /**
          * Build the {@link StaticLayout} after options have been set.
          *
          * <p>Note: the builder object must not be reused in any way after calling this
@@ -438,6 +454,7 @@
         @Nullable private int[] mRightIndents;
         private int mJustificationMode;
         private boolean mAddLastLineLineSpacing;
+        private LineBreakConfig mLineBreakConfig;
 
         private final Paint.FontMetricsInt mFontMetricsInt = new Paint.FontMetricsInt();
 
@@ -670,7 +687,7 @@
             PrecomputedText precomputed = (PrecomputedText) source;
             final @PrecomputedText.Params.CheckResultUsableResult int checkResult =
                     precomputed.checkResultUsable(bufStart, bufEnd, textDir, paint,
-                            b.mBreakStrategy, b.mHyphenationFrequency);
+                            b.mBreakStrategy, b.mHyphenationFrequency, b.mLineBreakConfig);
             switch (checkResult) {
                 case PrecomputedText.Params.UNUSABLE:
                     break;
@@ -680,6 +697,7 @@
                                 .setBreakStrategy(b.mBreakStrategy)
                                 .setHyphenationFrequency(b.mHyphenationFrequency)
                                 .setTextDirection(textDir)
+                                .setLineBreakConfig(b.mLineBreakConfig)
                                 .build();
                     precomputed = PrecomputedText.create(precomputed, newParams);
                     paragraphInfo = precomputed.getParagraphInfo();
@@ -692,8 +710,8 @@
         }
 
         if (paragraphInfo == null) {
-            final PrecomputedText.Params param = new PrecomputedText.Params(paint, textDir,
-                    b.mBreakStrategy, b.mHyphenationFrequency);
+            final PrecomputedText.Params param = new PrecomputedText.Params(paint,
+                    b.mLineBreakConfig, textDir, b.mBreakStrategy, b.mHyphenationFrequency);
             paragraphInfo = PrecomputedText.createMeasuredParagraphs(source, param, bufStart,
                     bufEnd, false /* computeLayout */);
         }
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 1a808b2..0fe06be 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -79,6 +79,7 @@
 import android.graphics.drawable.Drawable;
 import android.graphics.fonts.FontStyle;
 import android.graphics.fonts.FontVariationAxis;
+import android.graphics.text.LineBreakConfig;
 import android.icu.text.DecimalFormatSymbols;
 import android.os.AsyncTask;
 import android.os.Build;
@@ -348,6 +349,7 @@
  * @attr ref android.R.styleable#TextView_fontVariationSettings
  * @attr ref android.R.styleable#TextView_breakStrategy
  * @attr ref android.R.styleable#TextView_hyphenationFrequency
+ * @attr ref android.R.styleable#TextView_lineBreakStyle
  * @attr ref android.R.styleable#TextView_autoSizeTextType
  * @attr ref android.R.styleable#TextView_autoSizeMinTextSize
  * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
@@ -775,6 +777,7 @@
     private Layout mLayout;
     private boolean mLocalesChanged = false;
     private int mTextSizeUnit = -1;
+    private LineBreakConfig mLineBreakConfig = new LineBreakConfig();
 
     // This is used to reflect the current user preference for changing font weight and making text
     // more bold.
@@ -1442,6 +1445,11 @@
                     mHyphenationFrequency = a.getInt(attr, Layout.HYPHENATION_FREQUENCY_NONE);
                     break;
 
+                case com.android.internal.R.styleable.TextView_lineBreakStyle:
+                    mLineBreakConfig.setLineBreakStyle(
+                            a.getInt(attr, LineBreakConfig.LINE_BREAK_STYLE_NONE));
+                    break;
+
                 case com.android.internal.R.styleable.TextView_autoSizeTextType:
                     mAutoSizeTextType = a.getInt(attr, AUTO_SIZE_TEXT_TYPE_NONE);
                     break;
@@ -4788,13 +4796,50 @@
     }
 
     /**
+     * Sets line break configuration indicates which strategy needs to be used when calculating the
+     * text wrapping. There are thee strategies for the line break style(lb):
+     * {@link LineBreakConfig#LINE_BREAK_STYLE_LOOSE},
+     * {@link LineBreakConfig#LINE_BREAK_STYLE_NORMAL} and
+     * {@link LineBreakConfig#LINE_BREAK_STYLE_STRICT}.
+     * The default value of the line break style is {@link LineBreakConfig#LINE_BREAK_STYLE_NONE},
+     * which means no line break style is specified.
+     * See <a href="https://drafts.csswg.org/css-text/#line-break-property">
+     *         the line-break property</a>
+     *
+     * @param lineBreakConfig the line break config for text wrapping.
+     */
+    public void setLineBreakConfig(@NonNull LineBreakConfig lineBreakConfig) {
+        if (mLineBreakConfig.equals(lineBreakConfig)) {
+            return;
+        }
+        mLineBreakConfig.set(lineBreakConfig);
+        if (mLayout != null) {
+            nullLayouts();
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    /**
+     * Get the current line break configuration for text wrapping.
+     *
+     * @return the current line break configuration to be used for text wrapping.
+     */
+    public @NonNull LineBreakConfig getLineBreakConfig() {
+        LineBreakConfig lbConfig = new LineBreakConfig();
+        lbConfig.set(mLineBreakConfig);
+        return lbConfig;
+    }
+
+    /**
      * Gets the parameters for text layout precomputation, for use with {@link PrecomputedText}.
      *
      * @return a current {@link PrecomputedText.Params}
      * @see PrecomputedText
      */
     public @NonNull PrecomputedText.Params getTextMetricsParams() {
-        return new PrecomputedText.Params(new TextPaint(mTextPaint), getTextDirectionHeuristic(),
+        return new PrecomputedText.Params(new TextPaint(mTextPaint), mLineBreakConfig,
+                getTextDirectionHeuristic(),
                 mBreakStrategy, mHyphenationFrequency);
     }
 
@@ -4810,6 +4855,7 @@
         mTextDir = params.getTextDirection();
         mBreakStrategy = params.getBreakStrategy();
         mHyphenationFrequency = params.getHyphenationFrequency();
+        mLineBreakConfig.set(params.getLineBreakConfig());
         if (mLayout != null) {
             nullLayouts();
             requestLayout();
@@ -6348,7 +6394,7 @@
             }
             final @PrecomputedText.Params.CheckResultUsableResult int checkResult =
                     precomputed.getParams().checkResultUsable(getPaint(), mTextDir, mBreakStrategy,
-                            mHyphenationFrequency);
+                            mHyphenationFrequency, mLineBreakConfig);
             switch (checkResult) {
                 case PrecomputedText.Params.UNUSABLE:
                     throw new IllegalArgumentException(
@@ -9244,7 +9290,8 @@
                         .setBreakStrategy(mBreakStrategy)
                         .setHyphenationFrequency(mHyphenationFrequency)
                         .setJustificationMode(mJustificationMode)
-                        .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
+                        .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
+                        .setLineBreakConfig(mLineBreakConfig);
                 if (shouldEllipsize) {
                     builder.setEllipsize(mEllipsize)
                             .setEllipsizedWidth(ellipsisWidth);
@@ -9358,7 +9405,8 @@
                     .setBreakStrategy(mBreakStrategy)
                     .setHyphenationFrequency(mHyphenationFrequency)
                     .setJustificationMode(mJustificationMode)
-                    .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
+                    .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
+                    .setLineBreakConfig(mLineBreakConfig);
             if (shouldEllipsize) {
                 builder.setEllipsize(effectiveEllipsize)
                         .setEllipsizedWidth(ellipsisWidth);
@@ -9725,7 +9773,8 @@
                 .setHyphenationFrequency(getHyphenationFrequency())
                 .setJustificationMode(getJustificationMode())
                 .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
-                .setTextDirection(getTextDirectionHeuristic());
+                .setTextDirection(getTextDirectionHeuristic())
+                .setLineBreakConfig(mLineBreakConfig);
 
         final StaticLayout layout = layoutBuilder.build();
 
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 2267781..7b6e810 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -5417,6 +5417,17 @@
              ignores some hyphen character related typographic features, e.g. kerning. -->
             <enum name="fullFast" value="4" />
         </attr>
+        <!-- Indicates the line break strategies can be used when calculating the text wrapping. -->
+        <attr name="lineBreakStyle">
+            <!-- No line break style specific. -->
+            <enum name="none" value="0" />
+            <!-- Use the least restrictive rule for line-breaking. -->
+            <enum name="loose" value="1" />
+            <!-- Indicate breaking text with the most comment set of line-breaking rules. -->
+            <enum name="normal" value="2" />
+            <!-- ndicates breaking text with the most strictest line-breaking rules. -->
+            <enum name="strict" value="3" />
+        </attr>
         <!-- Specify the type of auto-size. Note that this feature is not supported by EditText,
         works only for TextView. -->
         <attr name="autoSizeTextType" format="enum">
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 5fa7409..5965be3 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -3211,6 +3211,7 @@
   </staging-public-group-final>
 
   <public type="attr" name="shouldUseDefaultUnfoldTransition" id="0x0101064c" />
+  <public type="attr" name="lineBreakStyle" id="0x0101064d" />
 
   <staging-public-group-final type="id" first-id="0x01fe0000">
     <public name="accessibilityActionDragStart" />
diff --git a/core/tests/coretests/src/android/text/MeasuredParagraphTest.java b/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
index d6a7682..045e746 100644
--- a/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
+++ b/core/tests/coretests/src/android/text/MeasuredParagraphTest.java
@@ -136,8 +136,8 @@
         MeasuredParagraph mt = null;
 
         mt = MeasuredParagraph.buildForStaticLayout(
-                PAINT, "XXX", 0, 3, LTR, MeasuredText.Builder.HYPHENATION_MODE_NONE, false,
-                null /* no hint */, null);
+                PAINT, null /* line break config */, "XXX", 0, 3, LTR,
+                MeasuredText.Builder.HYPHENATION_MODE_NONE, false, null /* no hint */, null);
         assertNotNull(mt);
         assertNotNull(mt.getChars());
         assertEquals("XXX", charsToString(mt.getChars()));
@@ -152,8 +152,8 @@
 
         // Recycle it
         MeasuredParagraph mt2 = MeasuredParagraph.buildForStaticLayout(
-                PAINT, "_VVV_", 1, 4, RTL, MeasuredText.Builder.HYPHENATION_MODE_NONE, false,
-                null /* no hint */, mt);
+                PAINT, null /* line break config */, "_VVV_", 1, 4, RTL,
+                MeasuredText.Builder.HYPHENATION_MODE_NONE, false, null /* no hint */, mt);
         assertEquals(mt2, mt);
         assertNotNull(mt2.getChars());
         assertEquals("VVV", charsToString(mt.getChars()));
diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java
new file mode 100644
index 0000000..4d81858
--- /dev/null
+++ b/graphics/java/android/graphics/text/LineBreakConfig.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * 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 android.graphics.text;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Indicates the strategies can be used when calculating the text wrapping.
+ *
+ * See <a href="https://drafts.csswg.org/css-text/#line-break-property">the line-break property</a>
+ */
+public final class LineBreakConfig {
+
+    /**
+     * No line break style specified.
+     */
+    public static final int LINE_BREAK_STYLE_NONE = 0;
+
+    /**
+     * Use the least restrictive rule for line-breaking. This is usually used for short lines.
+     */
+    public static final int LINE_BREAK_STYLE_LOOSE = 1;
+
+    /**
+     * Indicate breaking text with the most comment set of line-breaking rules.
+     */
+    public static final int LINE_BREAK_STYLE_NORMAL = 2;
+
+    /**
+     * Indicates breaking text with the most strictest line-breaking rules.
+     */
+    public static final int LINE_BREAK_STYLE_STRICT = 3;
+
+    /** @hide */
+    @IntDef(prefix = { "LINE_BREAK_STYLE_" }, value = {
+            LINE_BREAK_STYLE_NONE, LINE_BREAK_STYLE_LOOSE, LINE_BREAK_STYLE_NORMAL,
+            LINE_BREAK_STYLE_STRICT
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface LineBreakStyle {}
+
+    private @LineBreakStyle int mLineBreakStyle = LINE_BREAK_STYLE_NONE;
+
+    public LineBreakConfig() {
+    }
+
+    /**
+     * Set the line break configuration.
+     *
+     * @param config the new line break configuration.
+     */
+    public void set(@Nullable LineBreakConfig config) {
+        if (config != null) {
+            mLineBreakStyle = config.getLineBreakStyle();
+        } else {
+            mLineBreakStyle = LineBreakConfig.LINE_BREAK_STYLE_NONE;
+        }
+    }
+
+    /**
+     * Get the line break style.
+     *
+     * @return The current line break style to be used for the text wrapping.
+     */
+    public @LineBreakStyle int getLineBreakStyle() {
+        return mLineBreakStyle;
+    }
+
+    /**
+     * Set the line break style.
+     *
+     * @param lineBreakStyle the new line break style.
+     */
+    public void setLineBreakStyle(@LineBreakStyle int lineBreakStyle) {
+        mLineBreakStyle = lineBreakStyle;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == null) return false;
+        if (this == o) return true;
+        if (!(o instanceof LineBreakConfig)) return false;
+        LineBreakConfig that = (LineBreakConfig) o;
+        return mLineBreakStyle == that.mLineBreakStyle;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mLineBreakStyle);
+    }
+}
diff --git a/graphics/java/android/graphics/text/MeasuredText.java b/graphics/java/android/graphics/text/MeasuredText.java
index a34d0ab..5f4afb7 100644
--- a/graphics/java/android/graphics/text/MeasuredText.java
+++ b/graphics/java/android/graphics/text/MeasuredText.java
@@ -239,11 +239,33 @@
          */
         public @NonNull Builder appendStyleRun(@NonNull Paint paint, @IntRange(from = 0) int length,
                 boolean isRtl) {
+            return appendStyleRun(paint, null, length, isRtl);
+        }
+
+        /**
+         * Apply styles to the given length.
+         *
+         * Keeps an internal offset which increases at every append. The initial value for this
+         * offset is zero. After the style is applied the internal offset is moved to {@code offset
+         * + length}, and next call will start from this new position.
+         *
+         * @param paint a paint
+         * @param lineBreakConfig a line break configuration.
+         * @param length a length to be applied with a given paint, can not exceed the length of the
+         *               text
+         * @param isRtl true if the text is in RTL context, otherwise false.
+         */
+        public @NonNull Builder appendStyleRun(@NonNull Paint paint,
+                @Nullable LineBreakConfig lineBreakConfig, @IntRange(from = 0) int length,
+                boolean isRtl) {
             Preconditions.checkNotNull(paint);
             Preconditions.checkArgument(length > 0, "length can not be negative");
             final int end = mCurrentOffset + length;
             Preconditions.checkArgument(end <= mText.length, "Style exceeds the text length");
-            nAddStyleRun(mNativePtr, paint.getNativeInstance(), mCurrentOffset, end, isRtl);
+            int lbStyle = (lineBreakConfig != null) ? lineBreakConfig.getLineBreakStyle() :
+                    LineBreakConfig.LINE_BREAK_STYLE_NONE;
+            nAddStyleRun(mNativePtr, paint.getNativeInstance(), lbStyle, mCurrentOffset, end,
+                    isRtl);
             mCurrentOffset = end;
             return this;
         }
@@ -423,12 +445,14 @@
          *
          * @param nativeBuilderPtr The native MeasuredParagraph builder pointer.
          * @param paintPtr The native paint pointer to be applied.
+         * @param lineBreakStyle The line break style of the text.
          * @param start The start offset in the copied buffer.
          * @param end The end offset in the copied buffer.
          * @param isRtl True if the text is RTL.
          */
         private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr,
                                                 /* Non Zero */ long paintPtr,
+                                                int lineBreakStyle,
                                                 @IntRange(from = 0) int start,
                                                 @IntRange(from = 0) int end,
                                                 boolean isRtl);
diff --git a/libs/hwui/jni/text/MeasuredText.cpp b/libs/hwui/jni/text/MeasuredText.cpp
index bd9bd71..09539ec 100644
--- a/libs/hwui/jni/text/MeasuredText.cpp
+++ b/libs/hwui/jni/text/MeasuredText.cpp
@@ -65,11 +65,11 @@
 
 // Regular JNI
 static void nAddStyleRun(JNIEnv* /* unused */, jclass /* unused */, jlong builderPtr,
-                         jlong paintPtr, jint start, jint end, jboolean isRtl) {
+                         jlong paintPtr, jint lbStyle, jint start, jint end, jboolean isRtl) {
     Paint* paint = toPaint(paintPtr);
     const Typeface* typeface = Typeface::resolveDefault(paint->getAndroidTypeface());
     minikin::MinikinPaint minikinPaint = MinikinUtils::prepareMinikinPaint(paint, typeface);
-    toBuilder(builderPtr)->addStyleRun(start, end, std::move(minikinPaint), isRtl);
+    toBuilder(builderPtr)->addStyleRun(start, end, std::move(minikinPaint), lbStyle, isRtl);
 }
 
 // Regular JNI
@@ -144,7 +144,7 @@
 static const JNINativeMethod gMTBuilderMethods[] = {
         // MeasuredParagraphBuilder native functions.
         {"nInitBuilder", "()J", (void*)nInitBuilder},
-        {"nAddStyleRun", "(JJIIZ)V", (void*)nAddStyleRun},
+        {"nAddStyleRun", "(JJIIIZ)V", (void*)nAddStyleRun},
         {"nAddReplacementRun", "(JJIIF)V", (void*)nAddReplacementRun},
         {"nBuildMeasuredText", "(JJ[CZZZ)J", (void*)nBuildMeasuredText},
         {"nFreeBuilder", "(J)V", (void*)nFreeBuilder},