1 /*
2  * Copyright (C) 2016 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.server.wm;
18 
19 import static android.server.wm.ActivityManagerState.STATE_RESUMED;
20 import static android.server.wm.StateLogger.log;
21 import static android.server.wm.StateLogger.logE;
22 import static android.server.wm.app.Components.FONT_SCALE_ACTIVITY;
23 import static android.server.wm.app.Components.FONT_SCALE_NO_RELAUNCH_ACTIVITY;
24 import static android.server.wm.app.Components.FontScaleActivity.EXTRA_FONT_ACTIVITY_DPI;
25 import static android.server.wm.app.Components.FontScaleActivity.EXTRA_FONT_PIXEL_SIZE;
26 import static android.server.wm.app.Components.NO_RELAUNCH_ACTIVITY;
27 import static android.server.wm.app.Components.TEST_ACTIVITY;
28 import static android.server.wm.app.Components.TestActivity.EXTRA_CONFIG_ASSETS_SEQ;
29 import static android.view.Surface.ROTATION_0;
30 import static android.view.Surface.ROTATION_180;
31 import static android.view.Surface.ROTATION_270;
32 import static android.view.Surface.ROTATION_90;
33 
34 import static com.google.common.truth.Truth.assertWithMessage;
35 
36 import static org.junit.Assert.assertEquals;
37 import static org.junit.Assert.assertTrue;
38 import static org.junit.Assert.fail;
39 import static org.junit.Assume.assumeFalse;
40 import static org.junit.Assume.assumeTrue;
41 
42 import android.content.ComponentName;
43 import android.os.Bundle;
44 import android.platform.test.annotations.Presubmit;
45 import android.provider.Settings;
46 import android.server.wm.CommandSession.ActivityCallback;
47 import android.server.wm.TestJournalProvider.TestJournalContainer;
48 import android.server.wm.settings.SettingsSession;
49 
50 import com.android.compatibility.common.util.SystemUtil;
51 
52 import org.junit.Test;
53 
54 import java.util.Arrays;
55 import java.util.List;
56 
57 /**
58  * Build/Install/Run:
59  *     atest CtsWindowManagerDeviceTestCases:ConfigChangeTests
60  */
61 @Presubmit
62 public class ConfigChangeTests extends ActivityManagerTestBase {
63 
64     private static final float EXPECTED_FONT_SIZE_SP = 10.0f;
65 
66     @Test
testRotation90Relaunch()67     public void testRotation90Relaunch() throws Exception{
68         assumeTrue("Skipping test: no rotation support", supportsRotation());
69 
70         // Should relaunch on every rotation and receive no onConfigurationChanged()
71         testRotation(TEST_ACTIVITY, 1, 1, 0);
72     }
73 
74     @Test
testRotation90NoRelaunch()75     public void testRotation90NoRelaunch() throws Exception {
76         assumeTrue("Skipping test: no rotation support", supportsRotation());
77 
78         // Should receive onConfigurationChanged() on every rotation and no relaunch
79         testRotation(NO_RELAUNCH_ACTIVITY, 1, 0, 1);
80     }
81 
82     @Test
testRotation180_RegularActivity()83     public void testRotation180_RegularActivity() throws Exception {
84         assumeTrue("Skipping test: no rotation support", supportsRotation());
85         assumeFalse("Skipping test: display cutout present, can't predict exact lifecycle",
86                 hasDisplayCutout());
87 
88         // Should receive nothing
89         testRotation(TEST_ACTIVITY, 2, 0, 0);
90     }
91 
92     @Test
testRotation180_NoRelaunchActivity()93     public void testRotation180_NoRelaunchActivity() throws Exception {
94         assumeTrue("Skipping test: no rotation support", supportsRotation());
95         assumeFalse("Skipping test: display cutout present, can't predict exact lifecycle",
96                 hasDisplayCutout());
97 
98         // Should receive nothing
99         testRotation(NO_RELAUNCH_ACTIVITY, 2, 0, 0);
100     }
101 
102     /**
103      * Test activity configuration changes for devices with cutout(s). Landscape and
104      * reverse-landscape rotations should result in same screen space available for apps.
105      */
106     @Test
testRotation180RelaunchWithCutout()107     public void testRotation180RelaunchWithCutout() throws Exception {
108         assumeTrue("Skipping test: no rotation support", supportsRotation());
109         assumeTrue("Skipping test: no display cutout", hasDisplayCutout());
110 
111         testRotation180WithCutout(TEST_ACTIVITY, false /* canHandleConfigChange */);
112     }
113 
114     @Test
testRotation180NoRelaunchWithCutout()115     public void testRotation180NoRelaunchWithCutout() throws Exception {
116         assumeTrue("Skipping test: no rotation support", supportsRotation());
117         assumeTrue("Skipping test: no display cutout", hasDisplayCutout());
118 
119         testRotation180WithCutout(NO_RELAUNCH_ACTIVITY, true /* canHandleConfigChange */);
120     }
121 
testRotation180WithCutout(ComponentName activityName, boolean canHandleConfigChange)122     private void testRotation180WithCutout(ComponentName activityName,
123             boolean canHandleConfigChange) throws Exception {
124         launchActivity(activityName);
125         mAmWmState.computeState(activityName);
126 
127         try(final RotationSession rotationSession = new RotationSession()) {
128             final ActivityLifecycleCounts count1 = getLifecycleCountsForRotation(activityName,
129                     rotationSession, ROTATION_0 /* before */, ROTATION_180 /* after */,
130                     canHandleConfigChange);
131             final int configChangeCount1 = count1
132                     .getCount(ActivityCallback.ON_CONFIGURATION_CHANGED);
133             final int relaunchCount1 = count1.getCount(ActivityCallback.ON_CREATE);
134 
135             final ActivityLifecycleCounts count2 = getLifecycleCountsForRotation(activityName,
136                     rotationSession, ROTATION_90 /* before */, ROTATION_270 /* after */,
137                     canHandleConfigChange);
138             final int configChangeCount2 = count2
139                     .getCount(ActivityCallback.ON_CONFIGURATION_CHANGED);
140             final int relaunchCount2 = count2.getCount(ActivityCallback.ON_CREATE);
141 
142             final int configChange = configChangeCount1 + configChangeCount2;
143             final int relaunch = relaunchCount1 + relaunchCount2;
144             if (canHandleConfigChange) {
145                 assertWithMessage("There must be at most one 180 degree rotation that results "
146                         + "in the same configuration.").that(configChange).isLessThan(2);
147                 assertEquals("There must be no relaunch during test", 0, relaunch);
148                 return;
149             }
150 
151             // If the size change does not cross the threshold, the activity will receive
152             // onConfigurationChanged instead of relaunching.
153             assertWithMessage("There must be at most one 180 degree rotation that results in "
154                     + "relaunch or a configuration change.").that(relaunch + configChange)
155                     .isLessThan(2);
156 
157             final boolean resize1 = configChangeCount1 + relaunchCount1 > 0;
158             final boolean resize2 = configChangeCount2 + relaunchCount2 > 0;
159             // There should at least one 180 rotation without resize.
160             final boolean sameSize = !resize1 || !resize2;
161 
162             assertTrue("A device with cutout should have the same available screen space"
163                     + " in landscape and reverse-landscape", sameSize);
164         }
165     }
166 
getLifecycleCountsForRotation(ComponentName activityName, RotationSession session, int before, int after, boolean canHandleConfigChange)167     private ActivityLifecycleCounts getLifecycleCountsForRotation(ComponentName activityName,
168             RotationSession session, int before, int after, boolean canHandleConfigChange)  {
169         final int currentRotation = mAmWmState.getWmState().getRotation();
170         // The test verifies the events from "before" rotation to "after" rotation. So when
171         // preparing "before" rotation, the changes should be consumed to avoid being mixed into
172         // the result to verify.
173         final boolean is90DegreeDelta = Math.abs(currentRotation - before) % 2 != 0;
174         if (is90DegreeDelta) {
175             separateTestJournal();
176         }
177         session.set(before);
178         if (is90DegreeDelta) {
179             // Consume the changes of "before" rotation to make sure the activity is in a stable
180             // state to apply "after" rotation.
181             final ActivityCallback expectedCallback = canHandleConfigChange
182                     ? ActivityCallback.ON_CONFIGURATION_CHANGED
183                     : ActivityCallback.ON_CREATE;
184             mAmWmState.waitFor("activity rotated with 90 degree delta",
185                     () -> new ActivityLifecycleCounts(activityName).getCount(expectedCallback) > 0);
186         }
187         separateTestJournal();
188         session.set(after);
189         mAmWmState.computeState(activityName);
190         return new ActivityLifecycleCounts(activityName);
191     }
192 
193     @Test
testChangeFontScaleRelaunch()194     public void testChangeFontScaleRelaunch() throws Exception {
195         // Should relaunch and receive no onConfigurationChanged()
196         testChangeFontScale(FONT_SCALE_ACTIVITY, true /* relaunch */);
197     }
198 
199     @Test
testChangeFontScaleNoRelaunch()200     public void testChangeFontScaleNoRelaunch() throws Exception {
201         // Should receive onConfigurationChanged() and no relaunch
202         testChangeFontScale(FONT_SCALE_NO_RELAUNCH_ACTIVITY, false /* relaunch */);
203     }
204 
testRotation(ComponentName activityName, int rotationStep, int numRelaunch, int numConfigChange)205     private void testRotation(ComponentName activityName, int rotationStep, int numRelaunch,
206             int numConfigChange) throws Exception {
207         launchActivity(activityName);
208 
209         mAmWmState.computeState(activityName);
210 
211         final int initialRotation = 4 - rotationStep;
212         try (final RotationSession rotationSession = new RotationSession()) {
213             rotationSession.set(initialRotation);
214             mAmWmState.computeState(activityName);
215             final int actualStackId =
216                     mAmWmState.getAmState().getTaskByActivity(activityName).mStackId;
217             final int displayId = mAmWmState.getAmState().getStackById(actualStackId).mDisplayId;
218             final int newDeviceRotation = getDeviceRotation(displayId);
219             if (newDeviceRotation == INVALID_DEVICE_ROTATION) {
220                 logE("Got an invalid device rotation value. "
221                         + "Continuing the test despite of that, but it is likely to fail.");
222             } else if (newDeviceRotation != initialRotation) {
223                 log("This device doesn't support user rotation "
224                         + "mode. Not continuing the rotation checks.");
225                 return;
226             }
227 
228             for (int rotation = 0; rotation < 4; rotation += rotationStep) {
229                 separateTestJournal();
230                 rotationSession.set(rotation);
231                 mAmWmState.computeState(activityName);
232                 assertRelaunchOrConfigChanged(activityName, numRelaunch, numConfigChange);
233             }
234         }
235     }
236 
237     /** Helper class to save, set, and restore font_scale preferences. */
238     private static class FontScaleSession extends SettingsSession<Float> {
FontScaleSession()239         FontScaleSession() {
240             super(Settings.System.getUriFor(Settings.System.FONT_SCALE),
241                     Settings.System::getFloat,
242                     Settings.System::putFloat);
243         }
244     }
245 
testChangeFontScale( ComponentName activityName, boolean relaunch)246     private void testChangeFontScale(
247             ComponentName activityName, boolean relaunch) throws Exception {
248         try (final FontScaleSession fontScaleSession = new FontScaleSession()) {
249             fontScaleSession.set(1.0f);
250             separateTestJournal();
251             launchActivity(activityName);
252             mAmWmState.computeState(activityName);
253 
254             final int densityDpi = getActivityDensityDpi(activityName);
255 
256             for (float fontScale = 0.85f; fontScale <= 1.3f; fontScale += 0.15f) {
257                 separateTestJournal();
258                 fontScaleSession.set(fontScale);
259                 mAmWmState.computeState(activityName);
260                 assertRelaunchOrConfigChanged(activityName, relaunch ? 1 : 0, relaunch ? 0 : 1);
261 
262                 // Verify that the display metrics are updated, and therefore the text size is also
263                 // updated accordingly.
264                 assertExpectedFontPixelSize(activityName,
265                         scaledPixelsToPixels(EXPECTED_FONT_SIZE_SP, fontScale, densityDpi));
266             }
267         }
268     }
269 
270     /**
271      * Test updating application info when app is running. An activity with matching package name
272      * must be recreated and its asset sequence number must be incremented.
273      */
274     @Test
testUpdateApplicationInfo()275     public void testUpdateApplicationInfo() throws Exception {
276         separateTestJournal();
277 
278         // Launch an activity that prints applied config.
279         launchActivity(TEST_ACTIVITY);
280         final int assetSeq = getAssetSeqNumber(TEST_ACTIVITY);
281 
282         separateTestJournal();
283         // Update package info.
284         updateApplicationInfo(Arrays.asList(TEST_ACTIVITY.getPackageName()));
285         mAmWmState.waitForWithAmState((amState) -> {
286             // Wait for activity to be resumed and asset seq number to be updated.
287             try {
288                 return getAssetSeqNumber(TEST_ACTIVITY) == assetSeq + 1
289                         && amState.hasActivityState(TEST_ACTIVITY, STATE_RESUMED);
290             } catch (Exception e) {
291                 logE("Error waiting for valid state: " + e.getMessage());
292                 return false;
293             }
294         }, "Waiting asset sequence number to be updated and for activity to be resumed.");
295 
296         // Check if activity is relaunched and asset seq is updated.
297         assertRelaunchOrConfigChanged(TEST_ACTIVITY, 1 /* numRelaunch */,
298                 0 /* numConfigChange */);
299         final int newAssetSeq = getAssetSeqNumber(TEST_ACTIVITY);
300         assertEquals("Asset sequence number must be incremented.", assetSeq + 1, newAssetSeq);
301     }
302 
getAssetSeqNumber(ComponentName activityName)303     private static int getAssetSeqNumber(ComponentName activityName) {
304         return TestJournalContainer.get(activityName).extras.getInt(EXTRA_CONFIG_ASSETS_SEQ);
305     }
306 
307     // Calculate the scaled pixel size just like the device is supposed to.
scaledPixelsToPixels(float sp, float fontScale, int densityDpi)308     private static int scaledPixelsToPixels(float sp, float fontScale, int densityDpi) {
309         final int DEFAULT_DENSITY = 160;
310         float f = densityDpi * (1.0f / DEFAULT_DENSITY) * fontScale * sp;
311         return (int) ((f >= 0) ? (f + 0.5f) : (f - 0.5f));
312     }
313 
getActivityDensityDpi(ComponentName activityName)314     private static int getActivityDensityDpi(ComponentName activityName)
315             throws Exception {
316         final Bundle extras = TestJournalContainer.get(activityName).extras;
317         if (!extras.containsKey(EXTRA_FONT_ACTIVITY_DPI)) {
318             fail("No fontActivityDpi reported from activity " + activityName);
319             return -1;
320         }
321         return extras.getInt(EXTRA_FONT_ACTIVITY_DPI);
322     }
323 
assertExpectedFontPixelSize(ComponentName activityName, int fontPixelSize)324     private void assertExpectedFontPixelSize(ComponentName activityName, int fontPixelSize)
325             throws Exception {
326         final Bundle extras = TestJournalContainer.get(activityName).extras;
327         if (!extras.containsKey(EXTRA_FONT_PIXEL_SIZE)) {
328             fail("No fontPixelSize reported from activity " + activityName);
329         }
330         assertEquals("Expected font pixel size does not match", fontPixelSize,
331                 extras.getInt(EXTRA_FONT_PIXEL_SIZE));
332     }
333 
updateApplicationInfo(List<String> packages)334     private void updateApplicationInfo(List<String> packages) {
335         SystemUtil.runWithShellPermissionIdentity(
336                 () -> mAm.scheduleApplicationInfoChanged(packages,
337                         android.os.Process.myUserHandle().getIdentifier())
338         );
339     }
340 }
341