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