fix(non linear font scaling)!: add setLineHeight(unit, lineHeight) to TextView compat classes

This uses the new APIs in Android U.

Also added TypedValueCompat.getUnitFromComplexDimension() so we can use
units in AppCompatTextView.

Tests copied from
platform/cts/tests/tests/widget/src/android/widget/cts/TextViewFontScalingTest.kt

Bug: 273326061
Fix: 278145408
Test: ./gradlew appcompat:appcompat:connectedCheck --info --daemon -Pandroid.testInstrumentationRunnerArguments.class=androidx.appcompat.widget.AppCompatTextViewFontScalingTest

Relnote: added setLineHeight(unit, lineHeight) to TextView compat classes

Change-Id: Ib2ee1334bd0396a8d92a87b8c2268029a36b1148
diff --git a/appcompat/appcompat/src/androidTest/AndroidManifest.xml b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
index fb17a4b..aa16fad 100644
--- a/appcompat/appcompat/src/androidTest/AndroidManifest.xml
+++ b/appcompat/appcompat/src/androidTest/AndroidManifest.xml
@@ -131,6 +131,11 @@
             android:theme="@style/Theme.TextColors"/>
 
         <activity
+            android:name="androidx.appcompat.widget.AppCompatTextViewFontScalingTest$TextViewFontScalingActivity"
+            android:label="@string/app_compat_text_view_activity"
+            android:theme="@style/Theme.TextColors" />
+
+        <activity
             android:name="androidx.appcompat.widget.AppCompatTextViewAutoSizeActivity"
             android:label="@string/app_compat_text_view_auto_size_activity"
             android:theme="@style/Theme.AppCompat.Light"/>
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatTextViewFontScalingTest.kt b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatTextViewFontScalingTest.kt
new file mode 100644
index 0000000..a87d598
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatTextViewFontScalingTest.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2023 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 androidx.appcompat.widget
+
+import android.app.Activity
+import android.os.Build
+import android.os.Bundle
+import android.util.TypedValue
+import android.widget.TextView
+import androidx.appcompat.test.R
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.testutils.AndroidFontScaleHelper.resetSystemFontScale
+import androidx.testutils.AndroidFontScaleHelper.setSystemFontScale
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * Test {@link AppCompatTextView} under non-linear font scaling.
+ */
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class AppCompatTextViewFontScalingTest {
+    @get:Rule
+    val scenarioRule = ActivityScenarioRule(TextViewFontScalingActivity::class.java)
+
+    @After
+    fun teardown() {
+        // Have to manually check the version here because if we try to rely on the assumeTrue() in
+        // resetSystemFontScale(), it is called twice (again in setSystemFontScale()) and the test
+        // fails with a "TestCouldNotBeSkippedException: Test could not be skipped due to other
+        // failures" because it thinks the second assumeTrue() was a separate error.
+        // tl;dr avoids a bug in jUnit when multiple assumeTrue()s happen in @Test and @After
+        if (Build.VERSION.SDK_INT >= 29) {
+            resetSystemFontScale(scenarioRule.scenario)
+        }
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun testNonLinearFontScaling_testSetLineHeightSpAndSetTextSizeSp() {
+        setSystemFontScale(2f, scenarioRule.scenario)
+        scenarioRule.scenario.onActivity { activity ->
+            assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+            val textView = AppCompatTextView(activity)
+            val textSizeSp = 20f
+            val lineHeightSp = 40f
+
+            textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp)
+            textView.setLineHeight(TypedValue.COMPLEX_UNIT_SP, lineHeightSp)
+
+            verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+        }
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun testNonLinearFontScaling_overwriteXml_testSetLineHeightSpAndSetTextSizeSp() {
+        setSystemFontScale(2f, scenarioRule.scenario)
+        scenarioRule.scenario.onActivity { activity ->
+            assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+            val textView = findTextView(activity, R.id.textview_lineheight2x)
+            val textSizeSp = 20f
+            val lineHeightSp = 40f
+
+            textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp)
+            textView.setLineHeight(TypedValue.COMPLEX_UNIT_SP, lineHeightSp)
+
+            verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+        }
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun testNonLinearFontScaling_xml_testLineHeightAttrSpAndTextSizeAttrSp() {
+        setSystemFontScale(2f, scenarioRule.scenario)
+        scenarioRule.scenario.onActivity { activity ->
+            assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+            val textView = findTextView(activity, R.id.textview_lineheight2x)
+            val textSizeSp = 20f
+            val lineHeightSp = 40f
+
+            verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+        }
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun testNonLinearFontScaling_dimenXml_testLineHeightAttrSpAndTextSizeAttrSp() {
+        setSystemFontScale(2f, scenarioRule.scenario)
+        scenarioRule.scenario.onActivity { activity ->
+            assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+            val textView = findTextView(activity, R.id.textview_lineheight_dimen3x)
+            val textSizeSp = 20f
+            val lineHeightSp = 60f
+
+            verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+        }
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun testNonLinearFontScaling_styleXml_testLineHeightAttrSpAndTextSizeAttrSp() {
+        setSystemFontScale(2f, scenarioRule.scenario)
+        scenarioRule.scenario.onActivity { activity ->
+            assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+            val textView = findTextView(activity, R.id.textview_lineheight_style3x)
+            val textSizeSp = 20f
+            val lineHeightSp = 60f
+
+            verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+        }
+    }
+
+    @Test
+    @Throws(Throwable::class)
+    fun testNonLinearFontScaling_dimenXml_testSetLineHeightSpAndTextSizeAttrSp() {
+        setSystemFontScale(2f, scenarioRule.scenario)
+        scenarioRule.scenario.onActivity { activity ->
+            assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
+
+            val textView = findTextView(activity, R.id.textview_lineheight_dimen3x)
+            val textSizeSp = 20f
+            val lineHeightSp = 30f
+
+            textView.setLineHeight(TypedValue.COMPLEX_UNIT_SP, lineHeightSp)
+
+            verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
+        }
+    }
+
+    private fun findTextView(activity: Activity, id: Int): AppCompatTextView {
+        return activity.findViewById(id)!!
+    }
+
+    class TextViewFontScalingActivity : Activity() {
+        override fun onCreate(savedInstanceState: Bundle?) {
+            super.onCreate(savedInstanceState)
+            setContentView(R.layout.appcompat_textview_fontscaling_activity)
+        }
+    }
+
+    companion object {
+        /**
+         * Tolerance for comparing expected float lineHeight to the integer one returned by
+         * getLineHeight(). It is pretty lenient to account for integer rounding when text size is
+         * loaded from an attribute. (When loading an SP resource from an attribute for textSize,
+         * it is rounded to the nearest pixel, which can throw off calculations quite a lot. Not
+         * enough to make much of a difference to the user, but enough to need a wide tolerance in
+         * tests. See b/279456702 for more details.)
+         */
+        private const val TOLERANCE = 5f
+
+        private fun verifyLineHeightIsIntendedProportions(
+            lineHeightSp: Float,
+            textSizeSp: Float,
+            activity: Activity,
+            textView: TextView
+        ) {
+            val lineHeightMultiplier = lineHeightSp / textSizeSp
+            // Calculate what line height would be without non-linear font scaling compressing it.
+            // The trick is multiplying afterwards (by the pixel value) instead of before (by the SP
+            // value)
+            val expectedLineHeightPx = lineHeightMultiplier * TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_SP,
+                textSizeSp,
+                activity.resources.displayMetrics
+            )
+            assertThat(textView.lineHeight.toFloat())
+                .isWithin(TOLERANCE)
+                .of(expectedLineHeightPx)
+        }
+    }
+}
diff --git a/appcompat/appcompat/src/androidTest/res/layout/appcompat_textview_fontscaling_activity.xml b/appcompat/appcompat/src/androidTest/res/layout/appcompat_textview_fontscaling_activity.xml
new file mode 100644
index 0000000..fadabf4
--- /dev/null
+++ b/appcompat/appcompat/src/androidTest/res/layout/appcompat_textview_fontscaling_activity.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/container"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:id="@+id/layout_textviewtest"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <!-- Tests line height 2x the text size -->
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/textview_lineheight2x"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/sample_text_long"
+            android:textSize="20sp"
+            android:lineHeight="40sp" />
+
+        <!-- Tests line height 3x the text size -->
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/textview_lineheight_dimen3x"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/sample_text_long"
+            android:textSize="@dimen/textview_fontScaling_textSize"
+            android:lineHeight="@dimen/textview_fontScaling_lineHeight" />
+
+        <!-- Tests line height 3x the text size -->
+        <androidx.appcompat.widget.AppCompatTextView
+            android:id="@+id/textview_lineheight_style3x"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/sample_text_long"
+            style="@style/TextAppearance.FontScaling" />
+
+    </LinearLayout>
+
+</ScrollView>
diff --git a/appcompat/appcompat/src/androidTest/res/values/dimens.xml b/appcompat/appcompat/src/androidTest/res/values/dimens.xml
index d4a8a0e..aed0b5d 100644
--- a/appcompat/appcompat/src/androidTest/res/values/dimens.xml
+++ b/appcompat/appcompat/src/androidTest/res/values/dimens.xml
@@ -27,4 +27,7 @@
     <dimen name="textview_firstBaselineToTopHeight">100dp</dimen>
     <dimen name="textview_lastBaselineToBottomHeight">30dp</dimen>
     <dimen name="textview_lineHeight">60dp</dimen>
+
+    <dimen name="textview_fontScaling_textSize">20sp</dimen>
+    <dimen name="textview_fontScaling_lineHeight">60sp</dimen>
 </resources>
\ No newline at end of file
diff --git a/appcompat/appcompat/src/androidTest/res/values/donottranslate-strings.xml b/appcompat/appcompat/src/androidTest/res/values/donottranslate-strings.xml
index 151400a..7a07538 100644
--- a/appcompat/appcompat/src/androidTest/res/values/donottranslate-strings.xml
+++ b/appcompat/appcompat/src/androidTest/res/values/donottranslate-strings.xml
@@ -71,6 +71,16 @@
     <string name="app_compat_button_auto_size_activity">AppCompat button auto-size</string>
     <string name="sample_text1">Sample text 1</string>
     <string name="sample_text2">Sample text 2</string>
+    <string name="sample_text_long">This is a really long string which exceeds the width of the
+view. New devices have a much larger screen which actually enables long strings to be displayed
+with no fading. I have made this string longer to fix this case. If you are correcting this
+text, I would love to see the kind of devices you guys now use! Guys, maybe some devices need longer
+string! I think so, so how about double this string, like copy and paste!
+This is a really long string which exceeds the width of the view.
+New devices have a much larger screen which actually enables long strings to be displayed
+with no fading. I have made this string longer to fix this case. If you are correcting this
+text, I would love to see the kind of devices you guys now use! Guys, maybe some devices need longer
+string! I think so, so how about double this string, like copy and paste!</string>
     <string name="app_compat_image_button_activity">AppCompat image button</string>
     <string name="app_compat_image_view_activity">AppCompat image view</string>
     <string name="app_compat_button_activity">AppCompat button</string>
diff --git a/appcompat/appcompat/src/androidTest/res/values/styles.xml b/appcompat/appcompat/src/androidTest/res/values/styles.xml
index 0b540bf..a076019 100644
--- a/appcompat/appcompat/src/androidTest/res/values/styles.xml
+++ b/appcompat/appcompat/src/androidTest/res/values/styles.xml
@@ -146,4 +146,9 @@
         <item name="android:colorForeground">@color/color_state_list_lilac</item>
     </style>
 
+    <!-- Tests line height 3x the text size -->
+    <style name="TextAppearance.FontScaling">
+        <item name="android:textSize">@dimen/textview_fontScaling_textSize</item>
+        <item name="android:lineHeight">@dimen/textview_fontScaling_lineHeight</item>
+    </style>
 </resources>
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java
index fda4335..4eb3371 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextHelper.java
@@ -42,6 +42,7 @@
 import androidx.annotation.RestrictTo;
 import androidx.appcompat.R;
 import androidx.core.content.res.ResourcesCompat;
+import androidx.core.util.TypedValueCompat;
 import androidx.core.view.ViewCompat;
 import androidx.core.view.inputmethod.EditorInfoCompat;
 import androidx.core.widget.TextViewCompat;
@@ -326,8 +327,20 @@
                 R.styleable.AppCompatTextView_firstBaselineToTopHeight, -1);
         final int lastBaselineToBottomHeight = a.getDimensionPixelSize(
                 R.styleable.AppCompatTextView_lastBaselineToBottomHeight, -1);
-        final int lineHeight = a.getDimensionPixelSize(
-                R.styleable.AppCompatTextView_lineHeight, -1);
+        float lineHeight = -1;
+        int lineHeightUnit = -1;
+        if (a.hasValue(R.styleable.AppCompatTextView_lineHeight)) {
+            TypedValue peekValue = a.peekValue(R.styleable.AppCompatTextView_lineHeight);
+            if (peekValue != null && peekValue.type == TypedValue.TYPE_DIMENSION) {
+                lineHeightUnit = TypedValueCompat.getUnitFromComplexDimension(peekValue.data);
+                lineHeight = TypedValue.complexToFloat(peekValue.data);
+            } else {
+                lineHeight = a.getDimensionPixelSize(
+                        R.styleable.AppCompatTextView_lineHeight,
+                        -1
+                );
+            }
+        }
 
         a.recycle();
         if (firstBaselineToTopHeight != -1) {
@@ -337,7 +350,11 @@
             TextViewCompat.setLastBaselineToBottomHeight(mView, lastBaselineToBottomHeight);
         }
         if (lineHeight != -1) {
-            TextViewCompat.setLineHeight(mView, lineHeight);
+            if (lineHeightUnit == -1) {
+                TextViewCompat.setLineHeight(mView, (int) lineHeight);
+            } else {
+                TextViewCompat.setLineHeight(mView, lineHeightUnit, lineHeight);
+            }
         }
     }
 
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextView.java b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextView.java
index 1e92e4a..1f0e15a 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextView.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/widget/AppCompatTextView.java
@@ -37,6 +37,7 @@
 import android.widget.TextView;
 
 import androidx.annotation.DrawableRes;
+import androidx.annotation.FloatRange;
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -477,6 +478,15 @@
         TextViewCompat.setLineHeight(this, lineHeight);
     }
 
+    @Override
+    public void setLineHeight(int unit, @FloatRange(from = 0) float lineHeight) {
+        if (Build.VERSION.SDK_INT >= 34) {
+            getSuperCaller().setLineHeight(unit, lineHeight);
+        } else {
+            TextViewCompat.setLineHeight(this, unit, lineHeight);
+        }
+    }
+
     /**
      * See
      * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
@@ -789,7 +799,9 @@
     @RequiresApi(api = 26)
     SuperCaller getSuperCaller() {
         if (mSuperCaller == null) {
-            if (Build.VERSION.SDK_INT >= 28) {
+            if (Build.VERSION.SDK_INT >= 34) {
+                mSuperCaller = new SuperCallerApi34();
+            } else if (Build.VERSION.SDK_INT >= 28) {
                 mSuperCaller = new SuperCallerApi28();
             } else if (Build.VERSION.SDK_INT >= 26) {
                 mSuperCaller = new SuperCallerApi26();
@@ -817,6 +829,9 @@
         // api 28
         void setFirstBaselineToTopHeight(@Px int firstBaselineToTopHeight);
         void setLastBaselineToBottomHeight(@Px int lastBaselineToBottomHeight);
+
+        // api 34
+        void setLineHeight(int unit, @FloatRange(from = 0) float lineHeight);
     }
 
     @RequiresApi(api = 26)
@@ -878,6 +893,9 @@
 
         @Override
         public void setLastBaselineToBottomHeight(int lastBaselineToBottomHeight) {}
+
+        @Override
+        public void setLineHeight(int unit, float lineHeight) {}
     }
 
     @RequiresApi(api = 28)
@@ -893,4 +911,12 @@
             AppCompatTextView.super.setLastBaselineToBottomHeight(lastBaselineToBottomHeight);
         }
     }
+
+    @RequiresApi(api = 34)
+    class SuperCallerApi34 extends SuperCallerApi28 {
+        @Override
+        public void setLineHeight(int unit, float lineHeight) {
+            AppCompatTextView.super.setLineHeight(unit, lineHeight);
+        }
+    }
 }
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index b85e5f0..16f2c54 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -2389,6 +2389,7 @@
   public class TypedValueCompat {
     method public static float deriveDimension(int, float, android.util.DisplayMetrics);
     method public static float dpToPx(float, android.util.DisplayMetrics);
+    method public static int getUnitFromComplexDimension(int);
     method public static float pxToDp(float, android.util.DisplayMetrics);
     method public static float pxToSp(float, android.util.DisplayMetrics);
     method public static float spToPx(float, android.util.DisplayMetrics);
@@ -4156,6 +4157,7 @@
     method public static void setFirstBaselineToTopHeight(android.widget.TextView, @IntRange(from=0) @Px int);
     method public static void setLastBaselineToBottomHeight(android.widget.TextView, @IntRange(from=0) @Px int);
     method public static void setLineHeight(android.widget.TextView, @IntRange(from=0) @Px int);
+    method public static void setLineHeight(android.widget.TextView, int, @FloatRange(from=0) float);
     method public static void setPrecomputedText(android.widget.TextView, androidx.core.text.PrecomputedTextCompat);
     method public static void setTextAppearance(android.widget.TextView, @StyleRes int);
     method public static void setTextMetricsParams(android.widget.TextView, androidx.core.text.PrecomputedTextCompat.Params);
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index c8b618c..0f6c6a4 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2813,6 +2813,7 @@
   public class TypedValueCompat {
     method public static float deriveDimension(int, float, android.util.DisplayMetrics);
     method public static float dpToPx(float, android.util.DisplayMetrics);
+    method public static int getUnitFromComplexDimension(int);
     method public static float pxToDp(float, android.util.DisplayMetrics);
     method public static float pxToSp(float, android.util.DisplayMetrics);
     method public static float spToPx(float, android.util.DisplayMetrics);
@@ -4657,6 +4658,7 @@
     method public static void setFirstBaselineToTopHeight(android.widget.TextView, @IntRange(from=0) @Px int);
     method public static void setLastBaselineToBottomHeight(android.widget.TextView, @IntRange(from=0) @Px int);
     method public static void setLineHeight(android.widget.TextView, @IntRange(from=0) @Px int);
+    method public static void setLineHeight(android.widget.TextView, int, @FloatRange(from=0) float);
     method public static void setPrecomputedText(android.widget.TextView, androidx.core.text.PrecomputedTextCompat);
     method public static void setTextAppearance(android.widget.TextView, @StyleRes int);
     method public static void setTextMetricsParams(android.widget.TextView, androidx.core.text.PrecomputedTextCompat.Params);
diff --git a/core/core/src/main/java/androidx/core/util/TypedValueCompat.java b/core/core/src/main/java/androidx/core/util/TypedValueCompat.java
index 49f3bab..55f102d 100644
--- a/core/core/src/main/java/androidx/core/util/TypedValueCompat.java
+++ b/core/core/src/main/java/androidx/core/util/TypedValueCompat.java
@@ -44,6 +44,17 @@
     private TypedValueCompat() {}
 
     /**
+     * Return the complex unit type for the given complex dimension. For example, a dimen type
+     * with value 12sp will return {@link TypedValue#COMPLEX_UNIT_SP}.
+     *
+     * @param complexDimension the dimension, typically {@link TypedValue#data}
+     * @return The complex unit type
+     */
+    public static int getUnitFromComplexDimension(int complexDimension) {
+        return TypedValue.COMPLEX_UNIT_MASK & (complexDimension >> TypedValue.COMPLEX_UNIT_SHIFT);
+    }
+
+    /**
      * Converts a pixel value to the given dimension, e.g. PX to DP.
      *
      * <p>This is the inverse of {@link TypedValue#applyDimension(int, float, DisplayMetrics)}
diff --git a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
index 915d0af2..0e58694 100644
--- a/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
+++ b/core/core/src/main/java/androidx/core/widget/TextViewCompat.java
@@ -54,6 +54,7 @@
 
 import androidx.annotation.DoNotInline;
 import androidx.annotation.DrawableRes;
+import androidx.annotation.FloatRange;
 import androidx.annotation.IntDef;
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
@@ -806,6 +807,7 @@
      * Sets an explicit line height for this TextView. This is equivalent to the vertical distance
      * between subsequent baselines in the TextView.
      *
+     * @param textView the TextView to modify
      * @param lineHeight the line height in pixels
      *
      * @see TextView#setLineSpacing(float, float)
@@ -827,6 +829,38 @@
     }
 
     /**
+     * Sets an explicit line height to a given unit and value for the TextView. This is equivalent
+     * to the vertical distance between subsequent baselines in the TextView. See {@link
+     * TypedValue} for the possible dimension units.
+     *
+     * @param textView the TextView to modify
+     * @param unit The desired dimension unit. SP units are strongly recommended so that line height
+     *             stays proportional to the text size when fonts are scaled up for accessibility.
+     * @param lineHeight The desired line height in the given units.
+     *
+     * @see TextView#setLineSpacing(float, float)
+     * @see TextView#getLineSpacingExtra()
+     *
+     * @attr ref android.R.styleable#TextView_lineHeight
+     */
+    public static void setLineHeight(
+            @NonNull TextView textView,
+            int unit,
+            @FloatRange(from = 0) float lineHeight
+    ) {
+        if (Build.VERSION.SDK_INT >= 34) {
+            Api34Impl.setLineHeight(textView, unit, lineHeight);
+        } else {
+            float lineHeightPx = TypedValue.applyDimension(
+                    unit,
+                    lineHeight,
+                    textView.getResources().getDisplayMetrics()
+            );
+            setLineHeight(textView, Math.round(lineHeightPx));
+        }
+    }
+
+    /**
      * Gets the parameters for text layout precomputation, for use with
      * {@link PrecomputedTextCompat}.
      *
@@ -1283,4 +1317,20 @@
             return DecimalFormatSymbols.getInstance(locale);
         }
     }
+
+    @RequiresApi(34)
+    static class Api34Impl {
+        private Api34Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        public static void setLineHeight(
+                @NonNull TextView textView,
+                int unit,
+                @FloatRange(from = 0) float lineHeight
+        ) {
+            textView.setLineHeight(unit, lineHeight);
+        }
+    }
 }