• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.app.Activity;
44 import android.content.ComponentName;
45 import android.content.res.Configuration;
46 import android.graphics.Rect;
47 import android.os.Bundle;
48 import android.platform.test.annotations.Presubmit;
49 import android.server.wm.CommandSession.ActivityCallback;
50 import android.server.wm.TestJournalProvider.TestJournalContainer;
51 
52 import com.android.compatibility.common.util.SystemUtil;
53 
54 import org.junit.Test;
55 
56 import java.util.Arrays;
57 import java.util.List;
58 
59 /**
60  * Build/Install/Run:
61  *     atest CtsWindowManagerDeviceTestCases:ConfigChangeTests
62  */
63 @Presubmit
64 public class ConfigChangeTests extends ActivityManagerTestBase {
65 
66     private static final float EXPECTED_FONT_SIZE_SP = 10.0f;
67 
68     @Test
testRotation90Relaunch()69     public void testRotation90Relaunch() {
70         assumeTrue("Skipping test: no rotation support", supportsRotation());
71 
72         // Should relaunch on every rotation and receive no onConfigurationChanged()
73         testRotation(TEST_ACTIVITY, 1, 1, 0);
74     }
75 
76     @Test
testRotation90NoRelaunch()77     public void testRotation90NoRelaunch() {
78         assumeTrue("Skipping test: no rotation support", supportsRotation());
79 
80         // Should receive onConfigurationChanged() on every rotation and no relaunch
81         testRotation(NO_RELAUNCH_ACTIVITY, 1, 0, 1);
82     }
83 
84     @Test
testRotation180_RegularActivity()85     public void testRotation180_RegularActivity() {
86         assumeTrue("Skipping test: no rotation support", supportsRotation());
87         assumeFalse("Skipping test: display cutout present, can't predict exact lifecycle",
88                 hasDisplayCutout());
89 
90         // Should receive nothing
91         testRotation(TEST_ACTIVITY, 2, 0, 0);
92     }
93 
94     @Test
testRotation180_NoRelaunchActivity()95     public void testRotation180_NoRelaunchActivity() {
96         assumeTrue("Skipping test: no rotation support", supportsRotation());
97         assumeFalse("Skipping test: display cutout present, can't predict exact lifecycle",
98                 hasDisplayCutout());
99 
100         // Should receive nothing
101         testRotation(NO_RELAUNCH_ACTIVITY, 2, 0, 0);
102     }
103 
104     /**
105      * Test activity configuration changes for devices with cutout(s). Landscape and
106      * reverse-landscape rotations should result in same screen space available for apps.
107      */
108     @Test
testRotation180RelaunchWithCutout()109     public void testRotation180RelaunchWithCutout() {
110         assumeTrue("Skipping test: no rotation support", supportsRotation());
111         assumeTrue("Skipping test: no display cutout", hasDisplayCutout());
112 
113         testRotation180WithCutout(TEST_ACTIVITY, false /* canHandleConfigChange */);
114     }
115 
116     @Test
testRotation180NoRelaunchWithCutout()117     public void testRotation180NoRelaunchWithCutout() {
118         assumeTrue("Skipping test: no rotation support", supportsRotation());
119         assumeTrue("Skipping test: no display cutout", hasDisplayCutout());
120 
121         testRotation180WithCutout(NO_RELAUNCH_ACTIVITY, true /* canHandleConfigChange */);
122     }
123 
testRotation180WithCutout(ComponentName activityName, boolean canHandleConfigChange)124     private void testRotation180WithCutout(ComponentName activityName,
125             boolean canHandleConfigChange) {
126         launchActivity(activityName);
127         mWmState.computeState(activityName);
128 
129         final RotationSession rotationSession = createManagedRotationSession();
130         final ActivityLifecycleCounts count1 = getLifecycleCountsForRotation(activityName,
131                 rotationSession, ROTATION_0 /* before */, ROTATION_180 /* after */,
132                 canHandleConfigChange);
133         final int configChangeCount1 = count1.getCount(ActivityCallback.ON_CONFIGURATION_CHANGED);
134         final int relaunchCount1 = count1.getCount(ActivityCallback.ON_CREATE);
135 
136         final ActivityLifecycleCounts count2 = getLifecycleCountsForRotation(activityName,
137                 rotationSession, ROTATION_90 /* before */, ROTATION_270 /* after */,
138                 canHandleConfigChange);
139         final int configChangeCount2 = count2.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 in the"
146                     + " 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 relaunch"
154                 + " or a configuration change.").that(relaunch + configChange).isLessThan(2);
155 
156         final boolean resize1 = configChangeCount1 + relaunchCount1 > 0;
157         final boolean resize2 = configChangeCount2 + relaunchCount2 > 0;
158         // There should at least one 180 rotation without resize.
159         final boolean sameSize = !resize1 || !resize2;
160 
161         assertTrue("A device with cutout should have the same available screen space"
162                 + " in landscape and reverse-landscape", sameSize);
163     }
164 
prepareRotation(ComponentName activityName, RotationSession session, int currentRotation, int initialRotation, boolean canHandleConfigChange)165     private void prepareRotation(ComponentName activityName, RotationSession session,
166             int currentRotation, int initialRotation, boolean canHandleConfigChange) {
167         final boolean is90DegreeDelta = Math.abs(currentRotation - initialRotation) % 2 != 0;
168         if (is90DegreeDelta) {
169             separateTestJournal();
170         }
171         session.set(initialRotation);
172         if (is90DegreeDelta) {
173             // Consume the changes of "before" rotation to make sure the activity is in a stable
174             // state to apply "after" rotation.
175             final ActivityCallback expectedCallback = canHandleConfigChange
176                     ? ActivityCallback.ON_CONFIGURATION_CHANGED
177                     : ActivityCallback.ON_CREATE;
178             Condition.waitFor(new ActivityLifecycleCounts(activityName)
179                     .countWithRetry("activity rotated with 90 degree delta",
180                             countSpec(expectedCallback, CountSpec.GREATER_THAN, 0)));
181         }
182     }
183 
getLifecycleCountsForRotation(ComponentName activityName, RotationSession session, int before, int after, boolean canHandleConfigChange)184     private ActivityLifecycleCounts getLifecycleCountsForRotation(ComponentName activityName,
185             RotationSession session, int before, int after, boolean canHandleConfigChange)  {
186         final int currentRotation = mWmState.getRotation();
187         // The test verifies the events from "before" rotation to "after" rotation. So when
188         // preparing "before" rotation, the changes should be consumed to avoid being mixed into
189         // the result to verify.
190         prepareRotation(activityName, session, currentRotation, before, canHandleConfigChange);
191         separateTestJournal();
192         session.set(after);
193         mWmState.computeState(activityName);
194         return new ActivityLifecycleCounts(activityName);
195     }
196 
197     @Test
testChangeFontScaleRelaunch()198     public void testChangeFontScaleRelaunch() {
199         // Should relaunch and receive no onConfigurationChanged()
200         testChangeFontScale(FONT_SCALE_ACTIVITY, true /* relaunch */);
201     }
202 
203     @Test
testChangeFontScaleNoRelaunch()204     public void testChangeFontScaleNoRelaunch() {
205         // Should receive onConfigurationChanged() and no relaunch
206         testChangeFontScale(FONT_SCALE_NO_RELAUNCH_ACTIVITY, false /* relaunch */);
207     }
208 
testRotation(ComponentName activityName, int rotationStep, int numRelaunch, int numConfigChange)209     private void testRotation(ComponentName activityName, int rotationStep, int numRelaunch,
210             int numConfigChange) {
211         launchActivity(activityName, WINDOWING_MODE_FULLSCREEN);
212         mWmState.computeState(activityName);
213 
214         final int initialRotation = 4 - rotationStep;
215         final RotationSession rotationSession = createManagedRotationSession();
216         prepareRotation(activityName, rotationSession, mWmState.getRotation(), initialRotation,
217                 numConfigChange > 0);
218         final int actualStackId =
219                 mWmState.getTaskByActivity(activityName).mRootTaskId;
220         final int displayId = mWmState.getRootTask(actualStackId).mDisplayId;
221         final int newDeviceRotation = getDeviceRotation(displayId);
222         if (newDeviceRotation == INVALID_DEVICE_ROTATION) {
223             logE("Got an invalid device rotation value. "
224                     + "Continuing the test despite of that, but it is likely to fail.");
225         } else if (newDeviceRotation != initialRotation) {
226             log("This device doesn't support user rotation "
227                     + "mode. Not continuing the rotation checks.");
228             return;
229         }
230 
231         for (int rotation = 0; rotation < 4; rotation += rotationStep) {
232             separateTestJournal();
233             rotationSession.set(rotation);
234             mWmState.computeState(activityName);
235             assertRelaunchOrConfigChanged(activityName, numRelaunch, numConfigChange);
236         }
237     }
238 
testChangeFontScale(ComponentName activityName, boolean relaunch)239     private void testChangeFontScale(ComponentName activityName, boolean relaunch) {
240         final FontScaleSession fontScaleSession = createManagedFontScaleSession();
241         fontScaleSession.set(1.0f);
242         separateTestJournal();
243         launchActivity(activityName);
244         mWmState.computeState(activityName);
245 
246         final Bundle extras = TestJournalContainer.get(activityName).extras;
247         if (!extras.containsKey(EXTRA_FONT_ACTIVITY_DPI)) {
248             fail("No fontActivityDpi reported from activity " + activityName);
249         }
250         final int densityDpi = extras.getInt(EXTRA_FONT_ACTIVITY_DPI);
251 
252         for (float fontScale = 0.85f; fontScale <= 1.3f; fontScale += 0.15f) {
253             separateTestJournal();
254             fontScaleSession.set(fontScale);
255             mWmState.computeState(activityName);
256             // The number of config changes could be greater than expected as there may have
257             // other configuration change events triggered after font scale changed, such as
258             // NavigationBar recreated.
259             new ActivityLifecycleCounts(activityName).assertCountWithRetry(
260                     "relaunch or config changed",
261                     countSpec(ActivityCallback.ON_DESTROY, CountSpec.EQUALS, relaunch ? 1 : 0),
262                     countSpec(ActivityCallback.ON_CREATE, CountSpec.EQUALS, relaunch ? 1 : 0),
263                     countSpec(ActivityCallback.ON_CONFIGURATION_CHANGED,
264                             CountSpec.GREATER_THAN_OR_EQUALS, relaunch ? 0 : 1));
265 
266             // Verify that the display metrics are updated, and therefore the text size is also
267             // updated accordingly.
268             final Bundle changedExtras = TestJournalContainer.get(activityName).extras;
269             final float scale = fontScale;
270             waitForOrFail("reported fontPixelSize from " + activityName,
271                     () -> scaledPixelsToPixels(EXPECTED_FONT_SIZE_SP, scale, densityDpi)
272                             == changedExtras.getInt(EXTRA_FONT_PIXEL_SIZE));
273         }
274     }
275 
276     /**
277      * Test updating application info when app is running. An activity with matching package name
278      * must be recreated and its asset sequence number must be incremented.
279      */
280     @Test
testUpdateApplicationInfo()281     public void testUpdateApplicationInfo() throws Exception {
282         separateTestJournal();
283 
284         // Launch an activity that prints applied config.
285         launchActivity(TEST_ACTIVITY);
286         final int assetSeq = getAssetSeqNumber(TEST_ACTIVITY);
287 
288         separateTestJournal();
289         // Update package info.
290         updateApplicationInfo(Arrays.asList(TEST_ACTIVITY.getPackageName()));
291         mWmState.waitForWithAmState((amState) -> {
292             // Wait for activity to be resumed and asset seq number to be updated.
293             try {
294                 return getAssetSeqNumber(TEST_ACTIVITY) == assetSeq + 1
295                         && amState.hasActivityState(TEST_ACTIVITY, STATE_RESUMED);
296             } catch (Exception e) {
297                 logE("Error waiting for valid state: " + e.getMessage());
298                 return false;
299             }
300         }, "asset sequence number to be updated and for activity to be resumed.");
301 
302         // Check if activity is relaunched and asset seq is updated.
303         assertRelaunchOrConfigChanged(TEST_ACTIVITY, 1 /* numRelaunch */,
304                 0 /* numConfigChange */);
305         final int newAssetSeq = getAssetSeqNumber(TEST_ACTIVITY);
306         assertTrue("Asset sequence number must be incremented.", assetSeq < newAssetSeq);
307     }
308 
getAssetSeqNumber(ComponentName activityName)309     private static int getAssetSeqNumber(ComponentName activityName) {
310         return TestJournalContainer.get(activityName).extras.getInt(EXTRA_CONFIG_ASSETS_SEQ);
311     }
312 
313     // Calculate the scaled pixel size just like the device is supposed to.
scaledPixelsToPixels(float sp, float fontScale, int densityDpi)314     private static int scaledPixelsToPixels(float sp, float fontScale, int densityDpi) {
315         final int DEFAULT_DENSITY = 160;
316         float f = densityDpi * (1.0f / DEFAULT_DENSITY) * fontScale * sp;
317         return (int) ((f >= 0) ? (f + 0.5f) : (f - 0.5f));
318     }
319 
updateApplicationInfo(List<String> packages)320     private void updateApplicationInfo(List<String> packages) {
321         SystemUtil.runWithShellPermissionIdentity(
322                 () -> mAm.scheduleApplicationInfoChanged(packages,
323                         android.os.Process.myUserHandle().getIdentifier())
324         );
325     }
326 
327     /**
328      * Verifies if Activity receives {@link Activity#onConfigurationChanged(Configuration)} even if
329      * the size change is small.
330      */
331     @Test
testResizeWithoutCrossingSizeBucket()332     public void testResizeWithoutCrossingSizeBucket() {
333         assumeTrue(supportsSplitScreenMultiWindow());
334 
335         launchActivity(NO_RELAUNCH_ACTIVITY);
336 
337         waitAndAssertResumedActivity(NO_RELAUNCH_ACTIVITY, "Activity must be resumed");
338         final int taskId = mWmState.getTaskByActivity(NO_RELAUNCH_ACTIVITY).mTaskId;
339 
340         separateTestJournal();
341         mTaskOrganizer.putTaskInSplitPrimary(taskId);
342 
343         // It is expected a config change callback because the Activity goes to split mode.
344         assertRelaunchOrConfigChanged(NO_RELAUNCH_ACTIVITY, 0 /* numRelaunch */,
345                 1 /* numConfigChange */);
346 
347         // Resize task a little and verify if the Activity still receive config changes.
348         separateTestJournal();
349         final Rect taskBounds = mTaskOrganizer.getPrimaryTaskBounds();
350         taskBounds.set(taskBounds.left, taskBounds.top, taskBounds.right, taskBounds.bottom + 10);
351         mTaskOrganizer.setRootPrimaryTaskBounds(taskBounds);
352 
353         mWmState.waitForValidState(NO_RELAUNCH_ACTIVITY);
354 
355         assertRelaunchOrConfigChanged(NO_RELAUNCH_ACTIVITY, 0 /* numRelaunch */,
356                 1 /* numConfigChange */);
357     }
358 }
359