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