1 /* 2 * Copyright (C) 2024 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 com.android.systemui.accessibility; 18 19 import static android.os.Build.HW_TIMEOUT_MULTIPLIER; 20 21 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 22 23 import static com.google.common.truth.Truth.assertThat; 24 import static com.google.common.truth.Truth.assertWithMessage; 25 26 import static org.mockito.ArgumentMatchers.any; 27 import static org.mockito.ArgumentMatchers.eq; 28 import static org.mockito.Mockito.spy; 29 import static org.mockito.Mockito.verify; 30 import static org.mockito.Mockito.when; 31 32 import android.animation.Animator; 33 import android.animation.AnimatorListenerAdapter; 34 import android.animation.ObjectAnimator; 35 import android.animation.ValueAnimator; 36 import android.content.Context; 37 import android.content.pm.ActivityInfo; 38 import android.graphics.Rect; 39 import android.os.RemoteException; 40 import android.testing.AndroidTestingRunner; 41 import android.testing.TestableLooper; 42 import android.view.Display; 43 import android.view.IRotationWatcher; 44 import android.view.IWindowManager; 45 import android.view.SurfaceControl; 46 import android.view.SurfaceControlViewHost; 47 import android.view.View; 48 import android.view.WindowManager; 49 import android.view.accessibility.AccessibilityManager; 50 import android.view.animation.DecelerateInterpolator; 51 import android.view.animation.Interpolator; 52 import android.window.InputTransferToken; 53 54 import androidx.annotation.NonNull; 55 import androidx.test.filters.SmallTest; 56 57 import com.android.systemui.SysuiTestCase; 58 import com.android.systemui.res.R; 59 60 import org.junit.After; 61 import org.junit.Before; 62 import org.junit.Test; 63 import org.junit.runner.RunWith; 64 import org.mockito.Mock; 65 import org.mockito.MockitoAnnotations; 66 67 import java.util.concurrent.CountDownLatch; 68 import java.util.concurrent.TimeUnit; 69 import java.util.function.Supplier; 70 71 @SmallTest 72 @TestableLooper.RunWithLooper 73 @RunWith(AndroidTestingRunner.class) 74 public class FullscreenMagnificationControllerTest extends SysuiTestCase { 75 private static final long ANIMATION_DURATION_MS = 100L; 76 private static final long WAIT_TIMEOUT_S = 5L * HW_TIMEOUT_MULTIPLIER; 77 private static final long ANIMATION_TIMEOUT_MS = 78 5L * ANIMATION_DURATION_MS * HW_TIMEOUT_MULTIPLIER; 79 private FullscreenMagnificationController mFullscreenMagnificationController; 80 private SurfaceControlViewHost mSurfaceControlViewHost; 81 private ValueAnimator mShowHideBorderAnimator; 82 private SurfaceControl.Transaction mTransaction; 83 private TestableWindowManager mWindowManager; 84 @Mock 85 private IWindowManager mIWindowManager; 86 87 @Before setUp()88 public void setUp() { 89 MockitoAnnotations.initMocks(this); 90 getInstrumentation().runOnMainSync(() -> mSurfaceControlViewHost = 91 spy(new SurfaceControlViewHost(mContext, mContext.getDisplay(), 92 new InputTransferToken(), "FullscreenMagnification"))); 93 Supplier<SurfaceControlViewHost> scvhSupplier = () -> mSurfaceControlViewHost; 94 final WindowManager wm = mContext.getSystemService(WindowManager.class); 95 mWindowManager = new TestableWindowManager(wm); 96 mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); 97 98 mTransaction = new SurfaceControl.Transaction(); 99 mShowHideBorderAnimator = spy(newNullTargetObjectAnimator()); 100 mFullscreenMagnificationController = new FullscreenMagnificationController( 101 mContext, 102 mContext.getMainThreadHandler(), 103 mContext.getMainExecutor(), 104 mContext.getSystemService(AccessibilityManager.class), 105 mContext.getSystemService(WindowManager.class), 106 mIWindowManager, 107 scvhSupplier, 108 mTransaction, 109 mShowHideBorderAnimator); 110 } 111 112 @After tearDown()113 public void tearDown() { 114 getInstrumentation().runOnMainSync( 115 () -> mFullscreenMagnificationController 116 .onFullscreenMagnificationActivationChanged(false)); 117 } 118 119 @Test enableFullscreenMagnification_visibleBorder()120 public void enableFullscreenMagnification_visibleBorder() 121 throws InterruptedException, RemoteException { 122 CountDownLatch transactionCommittedLatch = new CountDownLatch(1); 123 CountDownLatch animationEndLatch = new CountDownLatch(1); 124 mTransaction.addTransactionCommittedListener( 125 Runnable::run, transactionCommittedLatch::countDown); 126 mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { 127 @Override 128 public void onAnimationEnd(Animator animation) { 129 animationEndLatch.countDown(); 130 } 131 }); 132 getInstrumentation().runOnMainSync(() -> 133 //Enable fullscreen magnification 134 mFullscreenMagnificationController 135 .onFullscreenMagnificationActivationChanged(true)); 136 assertWithMessage("Failed to wait for transaction committed") 137 .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) 138 .isTrue(); 139 assertWithMessage("Failed to wait for animation to be finished") 140 .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) 141 .isTrue(); 142 verify(mShowHideBorderAnimator).start(); 143 verify(mIWindowManager) 144 .watchRotation(any(IRotationWatcher.class), eq(Display.DEFAULT_DISPLAY)); 145 assertThat(mSurfaceControlViewHost.getView().isVisibleToUser()).isTrue(); 146 } 147 148 @Test disableFullscreenMagnification_reverseAnimationAndReleaseScvh()149 public void disableFullscreenMagnification_reverseAnimationAndReleaseScvh() 150 throws InterruptedException, RemoteException { 151 CountDownLatch transactionCommittedLatch = new CountDownLatch(1); 152 CountDownLatch enableAnimationEndLatch = new CountDownLatch(1); 153 CountDownLatch disableAnimationEndLatch = new CountDownLatch(1); 154 mTransaction.addTransactionCommittedListener( 155 Runnable::run, transactionCommittedLatch::countDown); 156 mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { 157 @Override 158 public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { 159 if (isReverse) { 160 disableAnimationEndLatch.countDown(); 161 } else { 162 enableAnimationEndLatch.countDown(); 163 } 164 } 165 }); 166 getInstrumentation().runOnMainSync(() -> 167 //Enable fullscreen magnification 168 mFullscreenMagnificationController 169 .onFullscreenMagnificationActivationChanged(true)); 170 assertWithMessage("Failed to wait for transaction committed") 171 .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) 172 .isTrue(); 173 assertWithMessage("Failed to wait for enabling animation to be finished") 174 .that(enableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) 175 .isTrue(); 176 verify(mShowHideBorderAnimator).start(); 177 178 getInstrumentation().runOnMainSync(() -> 179 // Disable fullscreen magnification 180 mFullscreenMagnificationController 181 .onFullscreenMagnificationActivationChanged(false)); 182 183 assertWithMessage("Failed to wait for disabling animation to be finished") 184 .that(disableAnimationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) 185 .isTrue(); 186 verify(mShowHideBorderAnimator).reverse(); 187 verify(mSurfaceControlViewHost).release(); 188 verify(mIWindowManager).removeRotationWatcher(any(IRotationWatcher.class)); 189 } 190 191 @Test onFullscreenMagnificationActivationChangeTrue_deactivating_reverseAnimator()192 public void onFullscreenMagnificationActivationChangeTrue_deactivating_reverseAnimator() 193 throws InterruptedException { 194 // Simulate the hiding border animation is running 195 when(mShowHideBorderAnimator.isRunning()).thenReturn(true); 196 CountDownLatch transactionCommittedLatch = new CountDownLatch(1); 197 CountDownLatch animationEndLatch = new CountDownLatch(1); 198 mTransaction.addTransactionCommittedListener( 199 Runnable::run, transactionCommittedLatch::countDown); 200 mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { 201 @Override 202 public void onAnimationEnd(Animator animation) { 203 animationEndLatch.countDown(); 204 } 205 }); 206 207 getInstrumentation().runOnMainSync( 208 () -> mFullscreenMagnificationController 209 .onFullscreenMagnificationActivationChanged(true)); 210 211 assertWithMessage("Failed to wait for transaction committed") 212 .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) 213 .isTrue(); 214 assertWithMessage("Failed to wait for animation to be finished") 215 .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) 216 .isTrue(); 217 verify(mShowHideBorderAnimator).reverse(); 218 } 219 220 @Test onScreenSizeChanged_activated_borderChangedToExpectedSize()221 public void onScreenSizeChanged_activated_borderChangedToExpectedSize() 222 throws InterruptedException { 223 CountDownLatch transactionCommittedLatch = new CountDownLatch(1); 224 CountDownLatch animationEndLatch = new CountDownLatch(1); 225 mTransaction.addTransactionCommittedListener( 226 Runnable::run, transactionCommittedLatch::countDown); 227 mShowHideBorderAnimator.addListener(new AnimatorListenerAdapter() { 228 @Override 229 public void onAnimationEnd(Animator animation) { 230 animationEndLatch.countDown(); 231 } 232 }); 233 getInstrumentation().runOnMainSync(() -> 234 //Enable fullscreen magnification 235 mFullscreenMagnificationController 236 .onFullscreenMagnificationActivationChanged(true)); 237 assertWithMessage("Failed to wait for transaction committed") 238 .that(transactionCommittedLatch.await(WAIT_TIMEOUT_S, TimeUnit.SECONDS)) 239 .isTrue(); 240 assertWithMessage("Failed to wait for animation to be finished") 241 .that(animationEndLatch.await(ANIMATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) 242 .isTrue(); 243 final Rect testWindowBounds = new Rect( 244 mWindowManager.getCurrentWindowMetrics().getBounds()); 245 testWindowBounds.set(testWindowBounds.left, testWindowBounds.top, 246 testWindowBounds.right + 100, testWindowBounds.bottom + 100); 247 mWindowManager.setWindowBounds(testWindowBounds); 248 249 getInstrumentation().runOnMainSync(() -> 250 mFullscreenMagnificationController.onConfigurationChanged( 251 ActivityInfo.CONFIG_SCREEN_SIZE)); 252 253 int borderOffset = mContext.getResources().getDimensionPixelSize( 254 R.dimen.magnifier_border_width_fullscreen_with_offset) 255 - mContext.getResources().getDimensionPixelSize( 256 R.dimen.magnifier_border_width_fullscreen); 257 final int newWidth = testWindowBounds.width() + 2 * borderOffset; 258 final int newHeight = testWindowBounds.height() + 2 * borderOffset; 259 verify(mSurfaceControlViewHost).relayout(newWidth, newHeight); 260 } 261 newNullTargetObjectAnimator()262 private ValueAnimator newNullTargetObjectAnimator() { 263 final ValueAnimator animator = 264 ObjectAnimator.ofFloat(/* target= */ null, View.ALPHA, 0f, 1f); 265 Interpolator interpolator = new DecelerateInterpolator(2.5f); 266 animator.setInterpolator(interpolator); 267 animator.setDuration(ANIMATION_DURATION_MS); 268 return animator; 269 } 270 } 271