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