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