xref: /aosp_15_r20/cts/tests/tests/widget/src/android/widget/cts/TextViewFontScalingTest.kt (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget.cts
18 
19 import android.app.Activity
20 import android.app.Instrumentation
21 import android.os.Bundle
22 import android.provider.Settings
23 import android.util.TypedValue
24 import android.widget.TextView
25 import androidx.test.InstrumentationRegistry
26 import androidx.test.ext.junit.rules.ActivityScenarioRule
27 import androidx.test.ext.junit.runners.AndroidJUnit4
28 import androidx.test.filters.MediumTest
29 import com.android.bedstead.harrier.DeviceState
30 import com.android.bedstead.multiuser.annotations.RequireRunNotOnVisibleBackgroundNonProfileUser
31 import com.android.compatibility.common.util.PollingCheck
32 import com.android.compatibility.common.util.ShellIdentityUtils
33 import com.google.common.truth.Truth.assertThat
34 import java.util.concurrent.atomic.AtomicBoolean
35 import org.junit.After
36 import org.junit.Before
37 import org.junit.ClassRule
38 import org.junit.Rule
39 import org.junit.Test
40 import org.junit.runner.RunWith
41 
42 /**
43  * Test {@link TextView} under non-linear font scaling.
44  */
45 // The RequireRunNotOnVisibleBackgroundNonProfileUser annotation is added to prevent running as
46 // a visible background user, because currently updating the font scale is allowed only for the
47 // current user. (b/342307420)
48 // TODO(b/342307420): Remove the annotation after updating font scale is allowed for visible
49 // background users.
50 @RequireRunNotOnVisibleBackgroundNonProfileUser
51 @MediumTest
52 @RunWith(AndroidJUnit4::class)
53 class TextViewFontScalingTest {
54     lateinit var mInstrumentation: Instrumentation
55 
56     @get:Rule
57     val scenarioRule = ActivityScenarioRule(TextViewFontScalingActivity::class.java)
58 
59     @Before
60     fun setup() {
61         mInstrumentation = InstrumentationRegistry.getInstrumentation()
62     }
63 
64     @After
65     fun teardown() {
66         restoreSystemFontScaleToDefault()
67     }
68 
69     @Test
70     @Throws(Throwable::class)
71     fun testNonLinearFontScaling_testSetLineHeightSpAndSetTextSizeSp() {
72         setSystemFontScale(2f)
73         scenarioRule.scenario.onActivity { activity ->
74             assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
75 
76             val textView = TextView(activity)
77             val textSizeSp = 20f
78             val lineHeightSp = 40f
79 
80             textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp)
81             textView.setLineHeight(TypedValue.COMPLEX_UNIT_SP, lineHeightSp)
82 
83             verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
84         }
85     }
86 
87     @Test
88     @Throws(Throwable::class)
89     fun testNonLinearFontScaling_testSetLineHeightSpFirstAndSetTextSizeSpAfter() {
90         setSystemFontScale(2f)
91         scenarioRule.scenario.onActivity { activity ->
92             assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
93 
94             val textView = TextView(activity)
95             val textSizeSp = 20f
96             val lineHeightSp = 40f
97 
98             textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp)
99             textView.setLineHeight(TypedValue.COMPLEX_UNIT_SP, lineHeightSp)
100 
101             val newTextSizeSp = 10f
102             textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, newTextSizeSp)
103 
104             verifyLineHeightIsIntendedProportions(lineHeightSp, newTextSizeSp, activity, textView)
105         }
106     }
107 
108     @Test
109     @Throws(Throwable::class)
110     fun testNonLinearFontScaling_overwriteXml_testSetLineHeightSpAndSetTextSizeSp() {
111         setSystemFontScale(2f)
112         scenarioRule.scenario.onActivity { activity ->
113             assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
114 
115             val textView = findTextView(activity, R.id.textview_lineheight2x)
116             val textSizeSp = 20f
117             val lineHeightSp = 40f
118 
119             textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSizeSp)
120             textView.setLineHeight(TypedValue.COMPLEX_UNIT_SP, lineHeightSp)
121 
122             verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
123         }
124     }
125 
126     @Test
127     @Throws(Throwable::class)
128     fun testNonLinearFontScaling_xml_testLineHeightAttrSpAndTextSizeAttrSp() {
129         setSystemFontScale(2f)
130         scenarioRule.scenario.onActivity { activity ->
131             assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
132 
133             val textView = findTextView(activity, R.id.textview_lineheight2x)
134             val textSizeSp = 20f
135             val lineHeightSp = 40f
136 
137             verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
138         }
139     }
140 
141     @Test
142     @Throws(Throwable::class)
143     fun testNonLinearFontScaling_overwriteXml_testLineHeightAttrSpAndSetTextSizeSpAfter() {
144         setSystemFontScale(2f)
145         scenarioRule.scenario.onActivity { activity ->
146             assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
147 
148             val textView = findTextView(activity, R.id.textview_lineheight2x)
149             val newTextSizeSp = 10f
150             val lineHeightSp = 40f
151 
152             textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, newTextSizeSp)
153 
154             verifyLineHeightIsIntendedProportions(lineHeightSp, newTextSizeSp, activity, textView)
155         }
156     }
157 
158     @Test
159     @Throws(Throwable::class)
160     fun testNonLinearFontScaling_dimenXml_testLineHeightAttrSpAndTextSizeAttrSp() {
161         setSystemFontScale(2f)
162         scenarioRule.scenario.onActivity { activity ->
163             assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
164 
165             val textView = findTextView(activity, R.id.textview_lineheight_dimen3x)
166             val textSizeSp = 20f
167             val lineHeightSp = 60f
168 
169             verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
170         }
171     }
172 
173     @Test
174     @Throws(Throwable::class)
175     fun testNonLinearFontScaling_styleXml_testLineHeightAttrSpAndTextSizeAttrSp() {
176         setSystemFontScale(2f)
177         scenarioRule.scenario.onActivity { activity ->
178             assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
179 
180             val textView = findTextView(activity, R.id.textview_lineheight_style3x)
181             val textSizeSp = 20f
182             val lineHeightSp = 60f
183 
184             verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
185         }
186     }
187 
188     @Test
189     @Throws(Throwable::class)
190     fun testNonLinearFontScaling_dimenXml_testSetLineHeightSpAndTextSizeAttrSp() {
191         setSystemFontScale(2f)
192         scenarioRule.scenario.onActivity { activity ->
193             assertThat(activity.resources.configuration.fontScale).isWithin(0.02f).of(2f)
194 
195             val textView = findTextView(activity, R.id.textview_lineheight_dimen3x)
196             val textSizeSp = 20f
197             val lineHeightSp = 30f
198 
199             textView.setLineHeight(TypedValue.COMPLEX_UNIT_SP, lineHeightSp)
200 
201             verifyLineHeightIsIntendedProportions(lineHeightSp, textSizeSp, activity, textView)
202         }
203     }
204 
205     private fun findTextView(activity: Activity, id: Int): TextView {
206         return activity.findViewById(id)!!
207     }
208 
209     private fun setSystemFontScale(fontScale: Float) {
210         ShellIdentityUtils.invokeWithShellPermissions {
211             Settings.System.putFloat(
212                     mInstrumentation.context.contentResolver,
213                     Settings.System.FONT_SCALE,
214                     fontScale
215             )
216         }
217         PollingCheck.waitFor(/* timeout= */ 5000) {
218             val isActivityAtCorrectScale = AtomicBoolean(false)
219             scenarioRule.scenario.onActivity { it ->
220                 isActivityAtCorrectScale.set(it.resources.configuration.fontScale == fontScale)
221             }
222             isActivityAtCorrectScale.get() && mInstrumentation
223                     .context
224                     .resources
225                     .configuration.fontScale == fontScale
226         }
227     }
228 
229     private fun restoreSystemFontScaleToDefault() {
230         ShellIdentityUtils.invokeWithShellPermissions {
231             // TODO(b/279083734): would use Settings.System.resetToDefaults() if it existed
232             Settings.System.putString(
233                 mInstrumentation.context.contentResolver,
234                 Settings.System.FONT_SCALE,
235                 null,
236                 /* overrideableByRestore= */
237                 true
238             )
239         }
240         PollingCheck.waitFor(/* timeout= */ 5000) {
241             mInstrumentation
242                 .context
243                 .resources
244                 .configuration.fontScale == 1f
245         }
246     }
247 
248     class TextViewFontScalingActivity : Activity() {
249         override fun onCreate(savedInstanceState: Bundle?) {
250             super.onCreate(savedInstanceState)
251             setContentView(R.layout.textview_fontscaling_layout)
252         }
253     }
254 
255     companion object {
256         /**
257          * Tolerance for comparing expected float lineHeight to the integer one returned by
258          * getLineHeight(). It is pretty lenient to account for integer rounding when text size is
259          * loaded from an attribute. (When loading an SP resource from an attribute for textSize,
260          * it is rounded to the nearest pixel, which can throw off calculations quite a lot. Not
261          * enough to make much of a difference to the user, but enough to need a wide tolerance in
262          * tests. See b/279456702 for more details.)
263          */
264         const val TOLERANCE = 5f
265 
266         private fun verifyLineHeightIsIntendedProportions(
267             lineHeightSp: Float,
268             textSizeSp: Float,
269             activity: Activity,
270             textView: TextView
271         ) {
272             val lineHeightMultiplier = lineHeightSp / textSizeSp
273             // Calculate what line height would be without non-linear font scaling compressing it.
274             // The trick is multiplying afterwards (by the pixel value) instead of before (by the SP
275             // value)
276             val expectedLineHeightPx = lineHeightMultiplier * TypedValue.applyDimension(
277                 TypedValue.COMPLEX_UNIT_SP,
278                 textSizeSp,
279                 activity.resources.displayMetrics
280             )
281             assertThat(textView.lineHeight.toFloat())
282                 .isWithin(TOLERANCE)
283                 .of(expectedLineHeightPx)
284         }
285 
286         @JvmField
287         @ClassRule
288         @Rule
289         val sDeviceState = DeviceState()
290     }
291 }
292