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 assumeRunNotOnVisibleBackgroundNonProfileUser( 251 "Font scale cannot be modified by visible background users"); 252 final FontScaleSession fontScaleSession = createManagedFontScaleSession(); 253 fontScaleSession.set(1.0f); 254 separateTestJournal(); 255 launchActivity(activityName); 256 mWmState.computeState(activityName); 257 258 final Bundle extras = TestJournalContainer.get(activityName).extras; 259 if (!extras.containsKey(EXTRA_FONT_ACTIVITY_DPI)) { 260 fail("No fontActivityDpi reported from activity " + activityName); 261 } 262 final int densityDpi = extras.getInt(EXTRA_FONT_ACTIVITY_DPI); 263 264 final float fontScale = 0.85f; 265 separateTestJournal(); 266 fontScaleSession.set(fontScale); 267 mWmState.computeState(activityName); 268 // The number of config changes could be greater than expected as there may have 269 // other configuration change events triggered after font scale changed, such as 270 // NavigationBar recreated. 271 new ActivityLifecycleCounts(activityName).assertCountWithRetry( 272 "relaunch or config changed", 273 countSpec(ActivityCallback.ON_DESTROY, CountSpec.EQUALS, relaunch ? 1 : 0), 274 countSpec(ActivityCallback.ON_CREATE, CountSpec.EQUALS, relaunch ? 1 : 0), 275 countSpec(ActivityCallback.ON_RESUME, CountSpec.EQUALS, relaunch ? 1 : 0), 276 countSpec(ActivityCallback.ON_CONFIGURATION_CHANGED, 277 CountSpec.GREATER_THAN_OR_EQUALS, relaunch ? 0 : 1)); 278 279 // Verify that the display metrics are updated, and therefore the text size is also 280 // updated accordingly. 281 waitForOrFail("reported fontPixelSize from " + activityName, 282 () -> scaledPixelsToPixels(EXPECTED_FONT_SIZE_SP, fontScale, densityDpi) 283 == TestJournalContainer.get(activityName).extras.getInt( 284 EXTRA_FONT_PIXEL_SIZE)); 285 } 286 287 /** 288 * Test updating application info when app is running. An activity with matching package name 289 * must be recreated and its asset sequence number must be incremented. 290 */ 291 @Test testUpdateApplicationInfo()292 public void testUpdateApplicationInfo() throws Exception { 293 separateTestJournal(); 294 295 // Launch an activity that prints applied config. 296 launchActivity(TEST_ACTIVITY); 297 final int assetSeq = getAssetSeqNumber(TEST_ACTIVITY); 298 299 separateTestJournal(); 300 // Update package info. 301 updateApplicationInfo(Arrays.asList(TEST_ACTIVITY.getPackageName())); 302 mWmState.waitForWithAmState((amState) -> { 303 // Wait for activity to be resumed and asset seq number to be updated. 304 try { 305 return getAssetSeqNumber(TEST_ACTIVITY) == assetSeq + 1 306 && amState.hasActivityState(TEST_ACTIVITY, STATE_RESUMED); 307 } catch (Exception e) { 308 logE("Error waiting for valid state: " + e.getMessage()); 309 return false; 310 } 311 }, "asset sequence number to be updated and for activity to be resumed."); 312 313 // Check if activity is relaunched and asset seq is updated. 314 assertRelaunchOrConfigChanged(TEST_ACTIVITY, 1 /* numRelaunch */, 315 0 /* numConfigChange */); 316 final int newAssetSeq = getAssetSeqNumber(TEST_ACTIVITY); 317 assertTrue("Asset sequence number must be incremented.", assetSeq < newAssetSeq); 318 } 319 getAssetSeqNumber(ComponentName activityName)320 private static int getAssetSeqNumber(ComponentName activityName) { 321 return TestJournalContainer.get(activityName).extras.getInt(EXTRA_CONFIG_ASSETS_SEQ); 322 } 323 324 // Calculate the scaled pixel size just like the device is supposed to. scaledPixelsToPixels(float sp, float fontScale, int densityDpi)325 private static int scaledPixelsToPixels(float sp, float fontScale, int densityDpi) { 326 final int DEFAULT_DENSITY = 160; 327 float f = densityDpi * (1.0f / DEFAULT_DENSITY) * fontScale * sp; 328 logAlways("scaledPixelsToPixels, f=" + f + ", densityDpi=" + densityDpi 329 + ", fontScale=" + fontScale + ", sp=" + sp 330 + ", Math.nextUp(f)=" + Math.nextUp(f)); 331 // Use the next up adjacent number to prevent precision loss of the float number. 332 f = Math.nextUp(f); 333 return (int) ((f >= 0) ? (f + 0.5f) : (f - 0.5f)); 334 } 335 updateApplicationInfo(List<String> packages)336 private void updateApplicationInfo(List<String> packages) { 337 SystemUtil.runWithShellPermissionIdentity( 338 () -> mAm.scheduleApplicationInfoChanged(packages, 339 android.os.Process.myUserHandle().getIdentifier()) 340 ); 341 } 342 343 /** 344 * Verifies if Activity receives {@link Activity#onConfigurationChanged(Configuration)} even if 345 * the size change is small. 346 */ 347 @Test testResizeWithoutCrossingSizeBucket()348 public void testResizeWithoutCrossingSizeBucket() { 349 assumeTrue(supportsSplitScreenMultiWindow()); 350 351 launchActivity(NO_RELAUNCH_ACTIVITY); 352 353 waitAndAssertResumedActivity(NO_RELAUNCH_ACTIVITY, "Activity must be resumed"); 354 final int taskId = mWmState.getTaskByActivity(NO_RELAUNCH_ACTIVITY).getTaskId(); 355 356 separateTestJournal(); 357 mTaskOrganizer.putTaskInSplitPrimary(taskId); 358 359 // It is expected a config change callback because the Activity goes to split mode. 360 assertRelaunchOrConfigChanged(NO_RELAUNCH_ACTIVITY, 0 /* numRelaunch */, 361 1 /* numConfigChange */); 362 363 // Resize task a little and verify if the Activity still receive config changes. 364 separateTestJournal(); 365 final Rect taskBounds = mTaskOrganizer.getPrimaryTaskBounds(); 366 taskBounds.set(taskBounds.left, taskBounds.top, taskBounds.right, taskBounds.bottom + 10); 367 mTaskOrganizer.setRootPrimaryTaskBounds(taskBounds); 368 369 mWmState.waitForValidState(NO_RELAUNCH_ACTIVITY); 370 371 assertRelaunchOrConfigChanged(NO_RELAUNCH_ACTIVITY, 0 /* numRelaunch */, 372 1 /* numConfigChange */); 373 } 374 } 375