Home | History | Annotate | Download | only in wm
      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
     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
     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
    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
    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")
    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")
    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")
    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 
    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 
    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 
    215         StateCount(int configChangeCount, int relaunchCount, boolean resize) {
    216             mConfigChangeCount = configChangeCount;
    217             mRelaunchCount = relaunchCount;
    218             mResize = resize;
    219         }
    220     }
    221 
    222     @Test
    223     public void testChangeFontScaleRelaunch() throws Exception {
    224         // Should relaunch and receive no onConfigurationChanged()
    225         testChangeFontScale(FONT_SCALE_ACTIVITY, true /* relaunch */);
    226     }
    227 
    228     @Test
    229     public void testChangeFontScaleNoRelaunch() throws Exception {
    230         // Should receive onConfigurationChanged() and no relaunch
    231         testChangeFontScale(FONT_SCALE_NO_RELAUNCH_ACTIVITY, false /* relaunch */);
    232     }
    233 
    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> {
    268         FontScaleSession() {
    269             super(Settings.System.getUriFor(Settings.System.FONT_SCALE),
    270                     Settings.System::getFloat,
    271                     Settings.System::putFloat);
    272         }
    273     }
    274 
    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
    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 
    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.
    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 
    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 
    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 
    363     private void updateApplicationInfo(List<String> packages) {
    364         SystemUtil.runWithShellPermissionIdentity(
    365                 () -> mAm.scheduleApplicationInfoChanged(packages,
    366                         android.os.Process.myUserHandle().getIdentifier())
    367         );
    368     }
    369 }
    370