1 /* 2 * Copyright (C) 2023 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.accessibilityservice.cts; 18 19 import static android.accessibilityservice.MagnificationConfig.MAGNIFICATION_MODE_FULLSCREEN; 20 import static android.accessibilityservice.cts.utils.ActivityLaunchUtils.homeScreenOrBust; 21 import static android.accessibilityservice.cts.utils.CtsTestUtils.DEFAULT_GLOBAL_TIMEOUT_MS; 22 import static android.accessibilityservice.cts.utils.CtsTestUtils.DEFAULT_IDLE_TIMEOUT_MS; 23 import static android.accessibilityservice.cts.utils.CtsTestUtils.isAutomotive; 24 25 import static com.google.common.truth.Truth.assertThat; 26 27 import static org.junit.Assert.assertTrue; 28 import static org.junit.Assume.assumeFalse; 29 import static org.junit.Assume.assumeTrue; 30 31 import android.accessibility.cts.common.AccessibilityDumpOnFailureRule; 32 import android.accessibility.cts.common.InstrumentedAccessibilityServiceTestRule; 33 import android.accessibilityservice.AccessibilityService; 34 import android.accessibilityservice.AccessibilityService.MagnificationController; 35 import android.accessibilityservice.AccessibilityServiceInfo; 36 import android.accessibilityservice.MagnificationConfig; 37 import android.accessibilityservice.cts.activities.AccessibilityWindowQueryActivity; 38 import android.accessibilityservice.cts.utils.SettingsSession; 39 import android.app.Instrumentation; 40 import android.app.UiAutomation; 41 import android.content.res.Resources; 42 import android.graphics.Rect; 43 import android.platform.test.annotations.AppModeFull; 44 import android.platform.test.annotations.Presubmit; 45 46 import androidx.lifecycle.Lifecycle; 47 import androidx.test.core.app.ActivityScenario; 48 import androidx.test.ext.junit.runners.AndroidJUnit4; 49 import androidx.test.platform.app.InstrumentationRegistry; 50 51 import com.android.compatibility.common.util.CddTest; 52 import com.android.compatibility.common.util.DeviceConfigStateChangerRule; 53 import com.android.compatibility.common.util.TestUtils; 54 import com.android.compatibility.common.util.UserSettings; 55 56 import org.junit.After; 57 import org.junit.AfterClass; 58 import org.junit.Before; 59 import org.junit.BeforeClass; 60 import org.junit.Rule; 61 import org.junit.Test; 62 import org.junit.rules.RuleChain; 63 import org.junit.runner.RunWith; 64 65 import java.io.IOException; 66 import java.util.concurrent.atomic.AtomicBoolean; 67 68 /** 69 * Class for testing {@See FullScreenMagnificationController}. 70 */ 71 @AppModeFull 72 @RunWith(AndroidJUnit4.class) 73 @CddTest(requirements = {"3.10/C-1-1,C-1-2"}) 74 @Presubmit 75 public class FullScreenMagnificationControllerTest { 76 77 /** Maximum timeout while waiting for a config to be updated */ 78 private static final int TIMEOUT_CONFIG_SECONDS = 15; 79 80 private static final int BOUNDS_TOLERANCE = 1; 81 82 private static final String DEVICE_CONFIG_NAMESPACE_WM = "window_manager"; 83 private static final String DEVICE_CONFIG_KEY_ALWAYS_ON_MAGNIFIER = 84 "AlwaysOnMagnifier__enable_always_on_magnifier"; 85 private static final String SETTING_KEY_MAGNIFICATION_ALWAYS_ON = 86 "accessibility_magnification_always_on_enabled"; 87 88 private static Instrumentation sInstrumentation; 89 private static UiAutomation sUiAutomation; 90 private StubMagnificationAccessibilityService mService; 91 92 private ActivityScenario<AccessibilityWindowQueryActivity> mActivityScenario = null; 93 94 private final InstrumentedAccessibilityServiceTestRule<StubMagnificationAccessibilityService> 95 mMagnificationAccessibilityServiceRule = 96 new InstrumentedAccessibilityServiceTestRule<>( 97 StubMagnificationAccessibilityService.class, false); 98 99 // StateChangerRules starts UiAutomation without FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES. 100 // They have to be outer rule than other accessibility related rules. 101 private final DeviceConfigStateChangerRule mDeviceConfigStateChangerRule = 102 new DeviceConfigStateChangerRule( 103 sInstrumentation.getContext(), 104 DEVICE_CONFIG_NAMESPACE_WM, 105 DEVICE_CONFIG_KEY_ALWAYS_ON_MAGNIFIER, 106 "true"); 107 108 @Rule 109 public final RuleChain mRuleChain = 110 RuleChain.outerRule(mDeviceConfigStateChangerRule) 111 .around(mMagnificationAccessibilityServiceRule) 112 .around(new AccessibilityDumpOnFailureRule()); 113 114 @BeforeClass oneTimeSetup()115 public static void oneTimeSetup() { 116 sInstrumentation = InstrumentationRegistry.getInstrumentation(); 117 sUiAutomation = sInstrumentation.getUiAutomation(); 118 AccessibilityServiceInfo info = sUiAutomation.getServiceInfo(); 119 info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; 120 sUiAutomation.setServiceInfo(info); 121 } 122 123 @AfterClass postTestTearDown()124 public static void postTestTearDown() { 125 sUiAutomation.destroy(); 126 } 127 128 @Before setUp()129 public void setUp() throws Exception { 130 assumeFalse("Magnification is not supported on Automotive.", 131 isAutomotive(sInstrumentation.getTargetContext())); 132 mService = mMagnificationAccessibilityServiceRule.enableService(); 133 134 // `setServiceInfo` resets magnification unless there's any magnification listener. 135 // In `homeScreenOrBust`, `uiAutomation.setServiceInfo` is done to ensure uiAutomation can 136 // listen the window events. 137 // Although we don't need to listen magnification here, adding an empty listener makes sure 138 // that magnification won't be reset by calling `setServiceInfo`. 139 // See also b/401998908. 140 final MagnificationController controller = mService.getMagnificationController(); 141 controller.addListener( 142 (controllerInner, region, scale1, centerX, centerY) -> { 143 // Do nothing. 144 }); 145 } 146 147 @After cleanUp()148 public void cleanUp() { 149 if (mActivityScenario != null) { 150 mActivityScenario.close(); 151 } 152 } 153 154 @Test testActivityTransitions_alwaysOnEnabled_keepMagnifiedDisabled_zoomOut()155 public void testActivityTransitions_alwaysOnEnabled_keepMagnifiedDisabled_zoomOut() 156 throws Exception { 157 assumeFalse(isKeepMagnifiedOnContextChangeEnabled()); 158 159 try (var session = getAlwaysOnSettingsSession(true)) { 160 mActivityScenario = launchActivityAndWait(); 161 162 zoomIn(/* scale= */ 2.0f); 163 // transition to home screen 164 homeScreenOrBust(sInstrumentation.getContext(), sUiAutomation); 165 166 assertThat(currentScale()).isEqualTo(1f); 167 assertThat(isActivated()).isTrue(); 168 } 169 } 170 171 @Test testActivityTransitions_alwaysOnEnabled_keepMagnifiedEnabled_keepZoom()172 public void testActivityTransitions_alwaysOnEnabled_keepMagnifiedEnabled_keepZoom() 173 throws Exception { 174 assumeTrue(isKeepMagnifiedOnContextChangeEnabled()); 175 176 try (var session = getAlwaysOnSettingsSession(true)) { 177 mActivityScenario = launchActivityAndWait(); 178 179 zoomIn(/* scale= */ 2.0f); 180 // transition to home screen 181 homeScreenOrBust(sInstrumentation.getContext(), sUiAutomation); 182 183 assertThat(currentScale()).isEqualTo(2f); 184 assertThat(isActivated()).isTrue(); 185 } 186 } 187 188 @Test testActivityTransitions_alwaysOnDisabled_disableMagnification()189 public void testActivityTransitions_alwaysOnDisabled_disableMagnification() throws Exception { 190 try (var session = getAlwaysOnSettingsSession(false)) { 191 mActivityScenario = launchActivityAndWait(); 192 193 zoomIn(/* scale= */ 2.0f); 194 // transition to home screen 195 homeScreenOrBust(sInstrumentation.getContext(), sUiAutomation); 196 197 assertThat(currentScale()).isEqualTo(1f); 198 assertThat(isActivated()).isFalse(); 199 } 200 } 201 202 // launch an activity and waits for it to be on screen launchActivityAndWait()203 private ActivityScenario<AccessibilityWindowQueryActivity> launchActivityAndWait() 204 throws Exception { 205 final var activityScenario = 206 ActivityScenario.launch(AccessibilityWindowQueryActivity.class) 207 .moveToState(Lifecycle.State.RESUMED); 208 sUiAutomation.waitForIdle(DEFAULT_IDLE_TIMEOUT_MS, DEFAULT_GLOBAL_TIMEOUT_MS); 209 return activityScenario; 210 } 211 zoomIn(float scale)212 private void zoomIn(float scale) throws Exception { 213 final MagnificationController controller = mService.getMagnificationController(); 214 final Rect rect = controller.getMagnificationRegion().getBounds(); 215 final float x = rect.centerX(); 216 final float y = rect.centerY(); 217 final AtomicBoolean setConfig = new AtomicBoolean(); 218 219 final MagnificationConfig config = new MagnificationConfig.Builder() 220 .setMode(MAGNIFICATION_MODE_FULLSCREEN) 221 .setScale(scale) 222 .setCenterX(x) 223 .setCenterY(y).build(); 224 225 mService.runOnServiceSync( 226 () -> { 227 setConfig.set(controller.setMagnificationConfig(config, false)); 228 }); 229 waitUntilMagnificationConfigEquals(controller, config); 230 231 assertTrue("Failed to set config", setConfig.get()); 232 } 233 currentScale()234 private float currentScale() { 235 final MagnificationController controller = mService.getMagnificationController(); 236 final MagnificationConfig config = controller.getMagnificationConfig(); 237 238 assertThat(config).isNotNull(); 239 240 return config.getScale(); 241 } 242 isActivated()243 private boolean isActivated() { 244 final MagnificationController controller = mService.getMagnificationController(); 245 final MagnificationConfig config = controller.getMagnificationConfig(); 246 247 assertThat(config).isNotNull(); 248 249 return config.isActivated(); 250 } 251 waitUntilMagnificationConfigEquals( AccessibilityService.MagnificationController controller, MagnificationConfig config)252 private void waitUntilMagnificationConfigEquals( 253 AccessibilityService.MagnificationController controller, 254 MagnificationConfig config) throws Exception { 255 TestUtils.waitUntil( 256 "Failed to apply the config. expected: " + config + " , actual: " 257 + controller.getMagnificationConfig(), TIMEOUT_CONFIG_SECONDS, 258 () -> { 259 final MagnificationConfig actualConfig = controller.getMagnificationConfig(); 260 // If expected config activated is false, we just need to verify the activated 261 // value is the same. Otherwise, we need to check all the actual values are 262 // equal to the expected values. 263 if (config.isActivated()) { 264 return actualConfig.getMode() == config.getMode() 265 && actualConfig.isActivated() == config.isActivated() 266 && Float.compare(actualConfig.getScale(), config.getScale()) == 0 267 && (Math.abs(actualConfig.getCenterX() - config.getCenterX()) 268 <= BOUNDS_TOLERANCE) 269 && (Math.abs(actualConfig.getCenterY() - config.getCenterY()) 270 <= BOUNDS_TOLERANCE); 271 } else { 272 return actualConfig.isActivated() == config.isActivated(); 273 } 274 }); 275 } 276 isKeepMagnifiedOnContextChangeEnabled()277 private boolean isKeepMagnifiedOnContextChangeEnabled() { 278 try { 279 return sInstrumentation.getTargetContext().getResources().getBoolean( 280 Resources.getSystem().getIdentifier( 281 "config_magnification_keep_zoom_level_when_context_changed", "bool", 282 "android")); 283 } catch (Resources.NotFoundException ignore) { 284 return false; 285 } 286 } 287 getAlwaysOnSettingsSession(boolean enabled)288 private static SettingsSession getAlwaysOnSettingsSession(boolean enabled) throws IOException { 289 return new SettingsSession( 290 sInstrumentation, 291 UserSettings.Namespace.SECURE, 292 SETTING_KEY_MAGNIFICATION_ALWAYS_ON, 293 enabled ? "1" : "0"); 294 } 295 } 296