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