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