1 /*
2  * 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.content.res;
18 
19 import static androidx.test.espresso.Espresso.onView;
20 import static androidx.test.espresso.assertion.ViewAssertions.matches;
21 import static androidx.test.espresso.matcher.ViewMatchers.withId;
22 
23 import static com.google.common.truth.Truth.assertThat;
24 
25 import android.app.Activity;
26 import android.compat.testing.PlatformCompatChangeRule;
27 import android.os.Bundle;
28 import android.platform.test.annotations.PlatinumTest;
29 import android.platform.test.annotations.Presubmit;
30 import android.provider.Settings;
31 import android.util.PollingCheck;
32 import android.view.View;
33 import android.widget.TextView;
34 
35 import androidx.test.core.app.ActivityScenario;
36 import androidx.test.espresso.matcher.BoundedMatcher;
37 import androidx.test.ext.junit.rules.ActivityScenarioRule;
38 import androidx.test.ext.junit.runners.AndroidJUnit4;
39 import androidx.test.filters.LargeTest;
40 import androidx.test.platform.app.InstrumentationRegistry;
41 
42 import com.android.compatibility.common.util.ShellIdentityUtils;
43 import com.android.frameworks.coretests.R;
44 
45 import org.hamcrest.Description;
46 import org.hamcrest.Matcher;
47 import org.junit.After;
48 import org.junit.Rule;
49 import org.junit.Test;
50 import org.junit.rules.TestRule;
51 import org.junit.runner.RunWith;
52 
53 import java.util.concurrent.atomic.AtomicBoolean;
54 import java.util.concurrent.atomic.AtomicReference;
55 
56 /**
57  * Test for verifying non-linear font scaling behavior.
58  * Build/Install/Run:
59  *  atest FrameworksCoreTests:android.content.res.FontScaleConverterActivityTest
60  */
61 @RunWith(AndroidJUnit4.class)
62 @LargeTest
63 @Presubmit
64 public class FontScaleConverterActivityTest {
65     @Rule
66     public ActivityScenarioRule<TestActivity> rule = new ActivityScenarioRule<>(TestActivity.class);
67     @Rule
68     public TestRule compatChangeRule = new PlatformCompatChangeRule();
69 
70     @After
teardown()71     public void teardown() {
72         restoreSystemFontScaleToDefault();
73     }
74 
75     @PlatinumTest(focusArea = "accessibility")
76     @Test
testFontsScaleNonLinearly()77     public void testFontsScaleNonLinearly() {
78         final ActivityScenario<TestActivity> scenario = rule.getScenario();
79 
80         setSystemFontScale(2f);
81 
82         var densityRef = new AtomicReference<Float>();
83         scenario.onActivity(activity -> {
84             assertThat(activity.getResources().getConfiguration().fontScale)
85                 .isWithin(0.05f)
86                 .of(2f);
87             densityRef.compareAndSet(null, activity.getResources().getDisplayMetrics().density);
88         });
89         var density = densityRef.get();
90         assertThat(density).isNotNull();
91 
92         onView(withId(R.id.text8sp)).check(matches(withTextSizeInRange(
93                 15f * density,
94                 16f * density
95         )));
96         onView(withId(R.id.text18sp)).check(matches(withTextSizeInRange(
97                 20f * density,
98                 36f * density
99         )));
100         onView(withId(R.id.text35sp)).check(matches(withTextSizeInRange(
101                 35 * density,
102                 60 * density
103         )));
104     }
105 
106     @PlatinumTest(focusArea = "accessibility")
107     @Test
testOnConfigurationChanged_doesNotCrash()108     public void testOnConfigurationChanged_doesNotCrash() {
109         final ActivityScenario<TestActivity> scenario = rule.getScenario();
110 
111         scenario.onActivity(activity -> {
112             var config = new Configuration(activity.getResources().getConfiguration());
113             config.fontScale = 1.2f;
114 
115             activity.onConfigurationChanged(config);
116             activity.finish();
117         });
118     }
119 
120     @PlatinumTest(focusArea = "accessibility")
121     @Test
testUpdateConfiguration_doesNotCrash()122     public void testUpdateConfiguration_doesNotCrash() {
123         final ActivityScenario<TestActivity> scenario = rule.getScenario();
124 
125         scenario.onActivity(activity -> {
126             activity.updateConfiguration();
127             activity.finish();
128         });
129     }
130 
setSystemFontScale(float fontScale)131     private void setSystemFontScale(float fontScale) {
132         ShellIdentityUtils.invokeWithShellPermissions(() -> {
133             Settings.System.putFloat(
134                     InstrumentationRegistry.getInstrumentation().getContext().getContentResolver(),
135                     Settings.System.FONT_SCALE,
136                     fontScale
137             );
138         });
139 
140         PollingCheck.waitFor(/* timeout= */ 7000, () -> {
141             AtomicBoolean isActivityAtCorrectScale = new AtomicBoolean(false);
142             rule.getScenario().onActivity(activity ->
143                     isActivityAtCorrectScale.set(
144                             activity.getResources()
145                                 .getConfiguration()
146                                 .fontScale == fontScale
147                     )
148             );
149             return isActivityAtCorrectScale.get();
150         });
151     }
152 
restoreSystemFontScaleToDefault()153     private static void restoreSystemFontScaleToDefault() {
154         ShellIdentityUtils.invokeWithShellPermissions(() -> {
155             // TODO(b/279083734): would use Settings.System.resetToDefaults() if it existed
156             Settings.System.putString(
157                     InstrumentationRegistry.getInstrumentation()
158                             .getContext()
159                             .getContentResolver(),
160                     Settings.System.FONT_SCALE,
161                     null,
162                     /* overrideableByRestore= */ true);
163         });
164 
165         PollingCheck.waitFor(
166                 /* timeout= */ 5000,
167                 () -> InstrumentationRegistry.getInstrumentation()
168                                         .getContext()
169                                         .getResources()
170                                         .getConfiguration()
171                                         .fontScale == 1
172         );
173     }
174 
withTextSizeInRange(float sizeStartPx, float sizeEndPx)175     private Matcher<View> withTextSizeInRange(float sizeStartPx, float sizeEndPx) {
176         return new BoundedMatcher<>(TextView.class) {
177             private static final float TOLERANCE = 0.05f;
178 
179             @Override
180             public void describeMismatch(Object item, Description description) {
181                 super.describeMismatch(item, description);
182                 description.appendText("was ").appendValue(((TextView) item).getTextSize());
183             }
184 
185             @Override
186             public void describeTo(Description description) {
187                 description.appendText("withTextSize between " + sizeStartPx + " and " + sizeEndPx);
188             }
189 
190             @Override
191             protected boolean matchesSafely(TextView textView) {
192                 var textSize = textView.getTextSize();
193                 return sizeStartPx - TOLERANCE < textSize && textSize < sizeEndPx + TOLERANCE;
194             }
195         };
196     }
197 
198     /** Test Activity */
199     public static class TestActivity extends Activity {
200         final Configuration mConfig = new Configuration();
201 
202         @Override
onCreate(Bundle savedInstanceState)203         protected void onCreate(Bundle savedInstanceState) {
204             super.onCreate(savedInstanceState);
205             getWindow().getDecorView().setKeepScreenOn(true);
206             setShowWhenLocked(true);
207             setTurnScreenOn(true);
208 
209             setContentView(R.layout.font_scale_converter_activity);
210         }
211 
updateConfiguration()212         public Configuration updateConfiguration() {
213             Settings.System.getConfiguration(getContentResolver(), mConfig);
214             return mConfig;
215         }
216     }
217 }
218