• 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.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 org.junit.Assert.assertEquals;
35 import static org.junit.Assert.assertTrue;
36 import static org.junit.Assert.fail;
37 import static org.junit.Assume.assumeFalse;
38 import static org.junit.Assume.assumeTrue;
39 
40 import android.content.ComponentName;
41 import android.os.Bundle;
42 import android.platform.test.annotations.Presubmit;
43 import android.provider.Settings;
44 import android.server.wm.CommandSession.ActivityCallback;
45 import android.server.wm.TestJournalProvider.TestJournalContainer;
46 import android.server.wm.settings.SettingsSession;
47 
48 import androidx.annotation.IntDef;
49 import androidx.test.filters.FlakyTest;
50 
51 import com.android.compatibility.common.util.SystemUtil;
52 
53 import org.junit.Test;
54 
55 import java.lang.annotation.Retention;
56 import java.lang.annotation.RetentionPolicy;
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     /** Verifies if the count of configuration changes is expected. */
70     private static final int TEST_MODE_CONFIGURATION_CHANGE = 1;
71     /** Verifies if the count of relaunch is expected. */
72     private static final int TEST_MODE_RELAUNCH_OR_CONFIG_CHANGE = 2;
73     /** Verifies if sizes match. */
74     private static final int TEST_MODE_RESIZE = 3;
75 
76     /** Test mode that defines which lifecycle callback is verified in a particular test */
77     @IntDef(flag = true, value = {
78             TEST_MODE_CONFIGURATION_CHANGE,
79             TEST_MODE_RELAUNCH_OR_CONFIG_CHANGE,
80             TEST_MODE_RESIZE
81     })
82     @Retention(RetentionPolicy.SOURCE)
83     private @interface TestMode {}
84 
85     @Test
testRotation90Relaunch()86     public void testRotation90Relaunch() throws Exception{
87         assumeTrue("Skipping test: no rotation support", supportsRotation());
88 
89         // Should relaunch on every rotation and receive no onConfigurationChanged()
90         testRotation(TEST_ACTIVITY, 1, 1, 0);
91     }
92 
93     @Test
testRotation90NoRelaunch()94     public void testRotation90NoRelaunch() throws Exception {
95         assumeTrue("Skipping test: no rotation support", supportsRotation());
96 
97         // Should receive onConfigurationChanged() on every rotation and no relaunch
98         testRotation(NO_RELAUNCH_ACTIVITY, 1, 0, 1);
99     }
100 
101     @Test
testRotation180_RegularActivity()102     public void testRotation180_RegularActivity() throws Exception {
103         assumeTrue("Skipping test: no rotation support", supportsRotation());
104         assumeFalse("Skipping test: display cutout present, can't predict exact lifecycle",
105                 hasDisplayCutout());
106 
107         // Should receive nothing
108         testRotation(TEST_ACTIVITY, 2, 0, 0);
109     }
110 
111     @Test
testRotation180_NoRelaunchActivity()112     public void testRotation180_NoRelaunchActivity() throws Exception {
113         assumeTrue("Skipping test: no rotation support", supportsRotation());
114         assumeFalse("Skipping test: display cutout present, can't predict exact lifecycle",
115                 hasDisplayCutout());
116 
117         // Should receive nothing
118         testRotation(NO_RELAUNCH_ACTIVITY, 2, 0, 0);
119     }
120 
121     @Test
122     @FlakyTest(bugId = 110533226, detail = "Promote to presubmit once confirm it's not flaky")
testRotation180RelaunchWithCutout()123     public void testRotation180RelaunchWithCutout() throws Exception {
124         assumeTrue("Skipping test: no rotation support", supportsRotation());
125         assumeTrue("Skipping test: no display cutout", hasDisplayCutout());
126 
127         testRotation180WithCutout(TEST_ACTIVITY, TEST_MODE_RELAUNCH_OR_CONFIG_CHANGE);
128     }
129 
130     @Test
131     @FlakyTest(bugId = 110533226, detail = "Promote to presubmit once confirm it's not flaky")
testRotation180NoRelaunchWithCutout()132     public void testRotation180NoRelaunchWithCutout() throws Exception {
133         assumeTrue("Skipping test: no rotation support", supportsRotation());
134         assumeTrue("Skipping test: no display cutout", hasDisplayCutout());
135 
136         testRotation180WithCutout(NO_RELAUNCH_ACTIVITY, TEST_MODE_CONFIGURATION_CHANGE);
137     }
138 
139     /**
140      * Test activity configuration changes for devices with cutout(s). Landscape and
141      * reverse-landscape rotations should result in same screen space available for apps.
142      */
143     @Test
144     @FlakyTest(bugId = 110533226, detail = "Promote to presubmit once confirm it's not flaky")
testConfigChangeWhenRotatingWithCutout()145     public void testConfigChangeWhenRotatingWithCutout() throws Exception {
146         assumeTrue("Skipping test: no rotation support", supportsRotation());
147         assumeTrue("Skipping test: no display cutout", hasDisplayCutout());
148 
149         testRotation180WithCutout(TEST_ACTIVITY, TEST_MODE_RESIZE);
150     }
151 
testRotation180WithCutout(ComponentName activityName, @TestMode int testMode)152     private void testRotation180WithCutout(ComponentName activityName, @TestMode int testMode)
153             throws Exception {
154         launchActivity(activityName);
155         mAmWmState.computeState(activityName);
156 
157         try(final RotationSession rotationSession = new RotationSession()) {
158             final StateCount count1 = getStateCountForRotation(activityName, rotationSession,
159                     ROTATION_0 /* before */, ROTATION_180 /* after */);
160             final StateCount count2 = getStateCountForRotation(activityName, rotationSession,
161                     ROTATION_90 /* before */, ROTATION_270 /* after */);
162 
163             final int configChange = count1.mConfigChangeCount + count2.mConfigChangeCount;
164             final int relaunch = count1.mRelaunchCount + count2.mRelaunchCount;
165             // There should at least one 180 rotation without resize.
166             final boolean sameSize = !count1.mResize || !count2.mResize;
167 
168             switch(testMode) {
169                 case TEST_MODE_CONFIGURATION_CHANGE: {
170                     assertTrue("There must be at most one 180 degree rotation that results in the"
171                             + " same configuration on device with cutout", configChange <= 1);
172                     assertEquals("There must be no relaunch during test", 0, relaunch);
173                     break;
174                 }
175                 case TEST_MODE_RELAUNCH_OR_CONFIG_CHANGE: {
176                     // If the size change does not cross the threshold, the activity will receive
177                     // onConfigurationChanged instead of relaunching.
178                     assertTrue("There must be at most one 180 degree rotation that results in"
179                             + " relaunch or a configuration change on device with cutout",
180                             relaunch + configChange <= 1);
181                     break;
182                 }
183                 case TEST_MODE_RESIZE: {
184                     assertTrue("A device with cutout should have the same available screen space"
185                             + " in landscape and reverse-landscape", sameSize);
186                     break;
187                 }
188                 default: {
189                     fail("unrecognized test mode: " + testMode);
190                 }
191             }
192         }
193     }
194 
getStateCountForRotation(ComponentName activityName, RotationSession session, int before, int after)195     private StateCount getStateCountForRotation(ComponentName activityName, RotationSession session,
196             int before, int after) throws Exception {
197         session.set(before);
198         separateTestJournal();
199         session.set(after);
200         mAmWmState.computeState(activityName);
201         final ActivityLifecycleCounts counter = new ActivityLifecycleCounts(activityName);
202 
203         int configChangeCount = counter.getCount(ActivityCallback.ON_CONFIGURATION_CHANGED);
204         int relaunchCount = counter.getCount(ActivityCallback.ON_CREATE);
205         boolean resize = getLastReportedSizesForActivity(activityName) != null;
206 
207         return new StateCount(configChangeCount, relaunchCount, resize);
208     }
209 
210     private final static class StateCount {
211         final int mConfigChangeCount;
212         final int mRelaunchCount;
213         final boolean mResize;
214 
StateCount(int configChangeCount, int relaunchCount, boolean resize)215         StateCount(int configChangeCount, int relaunchCount, boolean resize) {
216             mConfigChangeCount = configChangeCount;
217             mRelaunchCount = relaunchCount;
218             mResize = resize;
219         }
220     }
221 
222     @Test
testChangeFontScaleRelaunch()223     public void testChangeFontScaleRelaunch() throws Exception {
224         // Should relaunch and receive no onConfigurationChanged()
225         testChangeFontScale(FONT_SCALE_ACTIVITY, true /* relaunch */);
226     }
227 
228     @Test
testChangeFontScaleNoRelaunch()229     public void testChangeFontScaleNoRelaunch() throws Exception {
230         // Should receive onConfigurationChanged() and no relaunch
231         testChangeFontScale(FONT_SCALE_NO_RELAUNCH_ACTIVITY, false /* relaunch */);
232     }
233 
testRotation(ComponentName activityName, int rotationStep, int numRelaunch, int numConfigChange)234     private void testRotation(ComponentName activityName, int rotationStep, int numRelaunch,
235             int numConfigChange) throws Exception {
236         launchActivity(activityName);
237 
238         mAmWmState.computeState(activityName);
239 
240         final int initialRotation = 4 - rotationStep;
241         try (final RotationSession rotationSession = new RotationSession()) {
242             rotationSession.set(initialRotation);
243             mAmWmState.computeState(activityName);
244             final int actualStackId =
245                     mAmWmState.getAmState().getTaskByActivity(activityName).mStackId;
246             final int displayId = mAmWmState.getAmState().getStackById(actualStackId).mDisplayId;
247             final int newDeviceRotation = getDeviceRotation(displayId);
248             if (newDeviceRotation == INVALID_DEVICE_ROTATION) {
249                 logE("Got an invalid device rotation value. "
250                         + "Continuing the test despite of that, but it is likely to fail.");
251             } else if (newDeviceRotation != initialRotation) {
252                 log("This device doesn't support user rotation "
253                         + "mode. Not continuing the rotation checks.");
254                 return;
255             }
256 
257             for (int rotation = 0; rotation < 4; rotation += rotationStep) {
258                 separateTestJournal();
259                 rotationSession.set(rotation);
260                 mAmWmState.computeState(activityName);
261                 assertRelaunchOrConfigChanged(activityName, numRelaunch, numConfigChange);
262             }
263         }
264     }
265 
266     /** Helper class to save, set, and restore font_scale preferences. */
267     private static class FontScaleSession extends SettingsSession<Float> {
FontScaleSession()268         FontScaleSession() {
269             super(Settings.System.getUriFor(Settings.System.FONT_SCALE),
270                     Settings.System::getFloat,
271                     Settings.System::putFloat);
272         }
273     }
274 
testChangeFontScale( ComponentName activityName, boolean relaunch)275     private void testChangeFontScale(
276             ComponentName activityName, boolean relaunch) throws Exception {
277         try (final FontScaleSession fontScaleSession = new FontScaleSession()) {
278             fontScaleSession.set(1.0f);
279             separateTestJournal();
280             launchActivity(activityName);
281             mAmWmState.computeState(activityName);
282 
283             final int densityDpi = getActivityDensityDpi(activityName);
284 
285             for (float fontScale = 0.85f; fontScale <= 1.3f; fontScale += 0.15f) {
286                 separateTestJournal();
287                 fontScaleSession.set(fontScale);
288                 mAmWmState.computeState(activityName);
289                 assertRelaunchOrConfigChanged(activityName, relaunch ? 1 : 0, relaunch ? 0 : 1);
290 
291                 // Verify that the display metrics are updated, and therefore the text size is also
292                 // updated accordingly.
293                 assertExpectedFontPixelSize(activityName,
294                         scaledPixelsToPixels(EXPECTED_FONT_SIZE_SP, fontScale, densityDpi));
295             }
296         }
297     }
298 
299     /**
300      * Test updating application info when app is running. An activity with matching package name
301      * must be recreated and its asset sequence number must be incremented.
302      */
303     @Test
testUpdateApplicationInfo()304     public void testUpdateApplicationInfo() throws Exception {
305         separateTestJournal();
306 
307         // Launch an activity that prints applied config.
308         launchActivity(TEST_ACTIVITY);
309         final int assetSeq = getAssetSeqNumber(TEST_ACTIVITY);
310 
311         separateTestJournal();
312         // Update package info.
313         updateApplicationInfo(Arrays.asList(TEST_ACTIVITY.getPackageName()));
314         mAmWmState.waitForWithAmState((amState) -> {
315             // Wait for activity to be resumed and asset seq number to be updated.
316             try {
317                 return getAssetSeqNumber(TEST_ACTIVITY) == assetSeq + 1
318                         && amState.hasActivityState(TEST_ACTIVITY, STATE_RESUMED);
319             } catch (Exception e) {
320                 logE("Error waiting for valid state: " + e.getMessage());
321                 return false;
322             }
323         }, "Waiting asset sequence number to be updated and for activity to be resumed.");
324 
325         // Check if activity is relaunched and asset seq is updated.
326         assertRelaunchOrConfigChanged(TEST_ACTIVITY, 1 /* numRelaunch */,
327                 0 /* numConfigChange */);
328         final int newAssetSeq = getAssetSeqNumber(TEST_ACTIVITY);
329         assertEquals("Asset sequence number must be incremented.", assetSeq + 1, newAssetSeq);
330     }
331 
getAssetSeqNumber(ComponentName activityName)332     private static int getAssetSeqNumber(ComponentName activityName) {
333         return TestJournalContainer.get(activityName).extras.getInt(EXTRA_CONFIG_ASSETS_SEQ);
334     }
335 
336     // Calculate the scaled pixel size just like the device is supposed to.
scaledPixelsToPixels(float sp, float fontScale, int densityDpi)337     private static int scaledPixelsToPixels(float sp, float fontScale, int densityDpi) {
338         final int DEFAULT_DENSITY = 160;
339         float f = densityDpi * (1.0f / DEFAULT_DENSITY) * fontScale * sp;
340         return (int) ((f >= 0) ? (f + 0.5f) : (f - 0.5f));
341     }
342 
getActivityDensityDpi(ComponentName activityName)343     private static int getActivityDensityDpi(ComponentName activityName)
344             throws Exception {
345         final Bundle extras = TestJournalContainer.get(activityName).extras;
346         if (!extras.containsKey(EXTRA_FONT_ACTIVITY_DPI)) {
347             fail("No fontActivityDpi reported from activity " + activityName);
348             return -1;
349         }
350         return extras.getInt(EXTRA_FONT_ACTIVITY_DPI);
351     }
352 
assertExpectedFontPixelSize(ComponentName activityName, int fontPixelSize)353     private void assertExpectedFontPixelSize(ComponentName activityName, int fontPixelSize)
354             throws Exception {
355         final Bundle extras = TestJournalContainer.get(activityName).extras;
356         if (!extras.containsKey(EXTRA_FONT_PIXEL_SIZE)) {
357             fail("No fontPixelSize reported from activity " + activityName);
358         }
359         assertEquals("Expected font pixel size does not match", fontPixelSize,
360                 extras.getInt(EXTRA_FONT_PIXEL_SIZE));
361     }
362 
updateApplicationInfo(List<String> packages)363     private void updateApplicationInfo(List<String> packages) {
364         SystemUtil.runWithShellPermissionIdentity(
365                 () -> mAm.scheduleApplicationInfoChanged(packages,
366                         android.os.Process.myUserHandle().getIdentifier())
367         );
368     }
369 }
370