1 /* 2 * Copyright (C) 2022 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.clipboardoverlay; 18 19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 20 21 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN; 22 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED; 23 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EXPANDED_FROM_MINIMIZED; 24 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED; 25 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_EXPANDED; 26 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_MINIMIZED; 27 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED; 28 import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT; 29 import static com.android.systemui.flags.Flags.CLIPBOARD_SHARED_TRANSITIONS; 30 31 import static org.mockito.ArgumentMatchers.any; 32 import static org.mockito.ArgumentMatchers.anyBoolean; 33 import static org.mockito.ArgumentMatchers.anyString; 34 import static org.mockito.Mockito.never; 35 import static org.mockito.Mockito.times; 36 import static org.mockito.Mockito.verify; 37 import static org.mockito.Mockito.verifyNoMoreInteractions; 38 import static org.mockito.Mockito.when; 39 40 import android.animation.Animator; 41 import android.animation.AnimatorListenerAdapter; 42 import android.app.RemoteAction; 43 import android.content.ClipData; 44 import android.content.ClipDescription; 45 import android.content.Context; 46 import android.graphics.Insets; 47 import android.graphics.Rect; 48 import android.net.Uri; 49 import android.os.PersistableBundle; 50 import android.view.WindowInsets; 51 import android.view.textclassifier.TextLinks; 52 53 import androidx.test.filters.SmallTest; 54 import androidx.test.runner.AndroidJUnit4; 55 56 import com.android.internal.logging.UiEventLogger; 57 import com.android.systemui.SysuiTestCase; 58 import com.android.systemui.broadcast.BroadcastSender; 59 import com.android.systemui.flags.FakeFeatureFlags; 60 import com.android.systemui.screenshot.TimeoutHandler; 61 import com.android.systemui.settings.FakeDisplayTracker; 62 import com.android.systemui.util.concurrency.FakeExecutor; 63 import com.android.systemui.util.time.FakeSystemClock; 64 65 import org.junit.After; 66 import org.junit.Before; 67 import org.junit.Test; 68 import org.junit.runner.RunWith; 69 import org.mockito.ArgumentCaptor; 70 import org.mockito.Captor; 71 import org.mockito.Mock; 72 import org.mockito.Mockito; 73 import org.mockito.MockitoAnnotations; 74 import org.mockito.invocation.InvocationOnMock; 75 import org.mockito.stubbing.Answer; 76 77 import java.util.Optional; 78 79 @SmallTest 80 @RunWith(AndroidJUnit4.class) 81 public class ClipboardOverlayControllerTest extends SysuiTestCase { 82 83 private ClipboardOverlayController mOverlayController; 84 @Mock 85 private ClipboardOverlayView mClipboardOverlayView; 86 @Mock 87 private ClipboardOverlayWindow mClipboardOverlayWindow; 88 @Mock 89 private BroadcastSender mBroadcastSender; 90 @Mock 91 private TimeoutHandler mTimeoutHandler; 92 @Mock 93 private ClipboardOverlayUtils mClipboardUtils; 94 @Mock 95 private ClipboardImageLoader mClipboardImageLoader; 96 @Mock 97 private ClipboardTransitionExecutor mClipboardTransitionExecutor; 98 @Mock 99 private UiEventLogger mUiEventLogger; 100 private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); 101 private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); 102 103 @Mock 104 private Animator mAnimator; 105 private ArgumentCaptor<Animator.AnimatorListener> mAnimatorListenerCaptor = 106 ArgumentCaptor.forClass(Animator.AnimatorListener.class); 107 108 private ClipData mSampleClipData; 109 110 @Captor 111 private ArgumentCaptor<ClipboardOverlayView.ClipboardOverlayCallbacks> mOverlayCallbacksCaptor; 112 private ClipboardOverlayView.ClipboardOverlayCallbacks mCallbacks; 113 114 @Captor 115 private ArgumentCaptor<AnimatorListenerAdapter> mAnimatorArgumentCaptor; 116 117 private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); 118 119 @Before setup()120 public void setup() { 121 MockitoAnnotations.initMocks(this); 122 123 when(mClipboardOverlayView.getEnterAnimation()).thenReturn(mAnimator); 124 when(mClipboardOverlayView.getExitAnimation()).thenReturn(mAnimator); 125 when(mClipboardOverlayView.getFadeOutAnimation()).thenReturn(mAnimator); 126 when(mClipboardOverlayWindow.getWindowInsets()).thenReturn( 127 getImeInsets(new Rect(0, 0, 0, 0))); 128 129 mSampleClipData = new ClipData("Test", new String[]{"text/plain"}, 130 new ClipData.Item("Test Item")); 131 132 mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, true); // turned off for legacy tests 133 mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, true); // turned off for old tests 134 } 135 136 /** 137 * Needs to be done after setting flags for legacy tests, since the value of 138 * CLIPBOARD_SHARED_TRANSITIONS is checked during construction. This can be moved back into 139 * the setup method once CLIPBOARD_SHARED_TRANSITIONS is fully released and the tests where it 140 * is false are removed.[ 141 */ initController()142 private void initController() { 143 mOverlayController = new ClipboardOverlayController( 144 mContext, 145 mClipboardOverlayView, 146 mClipboardOverlayWindow, 147 getFakeBroadcastDispatcher(), 148 mBroadcastSender, 149 mTimeoutHandler, 150 mFeatureFlags, 151 mClipboardUtils, 152 mExecutor, 153 mClipboardImageLoader, 154 mClipboardTransitionExecutor, 155 mUiEventLogger); 156 verify(mClipboardOverlayView).setCallbacks(mOverlayCallbacksCaptor.capture()); 157 mCallbacks = mOverlayCallbacksCaptor.getValue(); 158 } 159 160 @After tearDown()161 public void tearDown() { 162 mOverlayController.hideImmediate(); 163 } 164 165 @Test test_setClipData_invalidImageData_legacy()166 public void test_setClipData_invalidImageData_legacy() { 167 initController(); 168 169 ClipData clipData = new ClipData("", new String[]{"image/png"}, 170 new ClipData.Item(Uri.parse(""))); 171 mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); 172 173 mOverlayController.setClipData(clipData, ""); 174 175 verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); 176 verify(mClipboardOverlayView, times(1)).showShareChip(); 177 verify(mClipboardOverlayView, times(1)).getEnterAnimation(); 178 } 179 180 @Test test_setClipData_nonImageUri_legacy()181 public void test_setClipData_nonImageUri_legacy() { 182 initController(); 183 184 ClipData clipData = new ClipData("", new String[]{"resource/png"}, 185 new ClipData.Item(Uri.parse(""))); 186 mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); 187 188 mOverlayController.setClipData(clipData, ""); 189 190 verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); 191 verify(mClipboardOverlayView, times(1)).showShareChip(); 192 verify(mClipboardOverlayView, times(1)).getEnterAnimation(); 193 } 194 195 @Test test_setClipData_textData_legacy()196 public void test_setClipData_textData_legacy() { 197 mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); 198 initController(); 199 200 mOverlayController.setClipData(mSampleClipData, "abc"); 201 202 verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false); 203 verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "abc"); 204 verify(mClipboardOverlayView, times(1)).showShareChip(); 205 verify(mClipboardOverlayView, times(1)).getEnterAnimation(); 206 } 207 208 @Test test_setClipData_sensitiveTextData_legacy()209 public void test_setClipData_sensitiveTextData_legacy() { 210 mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); 211 initController(); 212 213 ClipDescription description = mSampleClipData.getDescription(); 214 PersistableBundle b = new PersistableBundle(); 215 b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true); 216 description.setExtras(b); 217 ClipData data = new ClipData(description, mSampleClipData.getItemAt(0)); 218 mOverlayController.setClipData(data, ""); 219 220 verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true); 221 verify(mClipboardOverlayView, times(1)).showShareChip(); 222 verify(mClipboardOverlayView, times(1)).getEnterAnimation(); 223 } 224 225 @Test test_setClipData_repeatedCalls_legacy()226 public void test_setClipData_repeatedCalls_legacy() { 227 when(mAnimator.isRunning()).thenReturn(true); 228 mFeatureFlags.set(CLIPBOARD_IMAGE_TIMEOUT, false); 229 initController(); 230 231 mOverlayController.setClipData(mSampleClipData, ""); 232 mOverlayController.setClipData(mSampleClipData, ""); 233 234 verify(mClipboardOverlayView, times(1)).getEnterAnimation(); 235 } 236 237 @Test test_setClipData_invalidImageData()238 public void test_setClipData_invalidImageData() { 239 initController(); 240 241 ClipData clipData = new ClipData("", new String[]{"image/png"}, 242 new ClipData.Item(Uri.parse(""))); 243 244 mOverlayController.setClipData(clipData, ""); 245 246 verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); 247 verify(mClipboardOverlayView, times(1)).showShareChip(); 248 verify(mClipboardOverlayView, times(1)).getEnterAnimation(); 249 } 250 251 @Test test_setClipData_nonImageUri()252 public void test_setClipData_nonImageUri() { 253 initController(); 254 ClipData clipData = new ClipData("", new String[]{"resource/png"}, 255 new ClipData.Item(Uri.parse(""))); 256 257 mOverlayController.setClipData(clipData, ""); 258 259 verify(mClipboardOverlayView, times(1)).showDefaultTextPreview(); 260 verify(mClipboardOverlayView, times(1)).showShareChip(); 261 verify(mClipboardOverlayView, times(1)).getEnterAnimation(); 262 } 263 264 @Test test_setClipData_textData()265 public void test_setClipData_textData() { 266 initController(); 267 mOverlayController.setClipData(mSampleClipData, "abc"); 268 269 verify(mClipboardOverlayView, times(1)).showTextPreview("Test Item", false); 270 verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "abc"); 271 verify(mClipboardOverlayView, times(1)).showShareChip(); 272 verify(mClipboardOverlayView, times(1)).getEnterAnimation(); 273 } 274 275 @Test test_setClipData_sensitiveTextData()276 public void test_setClipData_sensitiveTextData() { 277 initController(); 278 ClipDescription description = mSampleClipData.getDescription(); 279 PersistableBundle b = new PersistableBundle(); 280 b.putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true); 281 description.setExtras(b); 282 ClipData data = new ClipData(description, mSampleClipData.getItemAt(0)); 283 mOverlayController.setClipData(data, ""); 284 285 verify(mClipboardOverlayView, times(1)).showTextPreview("••••••", true); 286 verify(mClipboardOverlayView, times(1)).showShareChip(); 287 verify(mClipboardOverlayView, times(1)).getEnterAnimation(); 288 } 289 290 @Test test_setClipData_repeatedCalls()291 public void test_setClipData_repeatedCalls() { 292 initController(); 293 when(mAnimator.isRunning()).thenReturn(true); 294 295 mOverlayController.setClipData(mSampleClipData, ""); 296 mOverlayController.setClipData(mSampleClipData, ""); 297 298 verify(mClipboardOverlayView, times(1)).getEnterAnimation(); 299 } 300 301 @Test test_viewCallbacks_onShareTapped_sharedTransitionsOff()302 public void test_viewCallbacks_onShareTapped_sharedTransitionsOff() { 303 mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, false); 304 initController(); 305 mOverlayController.setClipData(mSampleClipData, ""); 306 307 mCallbacks.onShareButtonTapped(); 308 309 verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, ""); 310 verify(mClipboardOverlayView, times(1)).getExitAnimation(); 311 } 312 313 @Test test_viewCallbacks_onShareTapped()314 public void test_viewCallbacks_onShareTapped() { 315 initController(); 316 mOverlayController.setClipData(mSampleClipData, ""); 317 318 mCallbacks.onShareButtonTapped(); 319 verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()); 320 mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator); 321 322 verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, ""); 323 verify(mClipboardOverlayView, times(1)).getFadeOutAnimation(); 324 } 325 326 @Test test_viewCallbacks_onDismissTapped_sharedTransitionsOff()327 public void test_viewCallbacks_onDismissTapped_sharedTransitionsOff() { 328 mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, false); 329 initController(); 330 mOverlayController.setClipData(mSampleClipData, ""); 331 332 mCallbacks.onDismissButtonTapped(); 333 334 verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, ""); 335 verify(mClipboardOverlayView, times(1)).getExitAnimation(); 336 } 337 338 @Test test_viewCallbacks_onDismissTapped()339 public void test_viewCallbacks_onDismissTapped() { 340 initController(); 341 342 mCallbacks.onDismissButtonTapped(); 343 verify(mAnimator).addListener(mAnimatorListenerCaptor.capture()); 344 mAnimatorListenerCaptor.getValue().onAnimationEnd(mAnimator); 345 346 // package name is null since we haven't actually set a source for this test 347 verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED, 0, null); 348 verify(mClipboardOverlayView, times(1)).getExitAnimation(); 349 } 350 351 @Test test_multipleDismissals_dismissesOnce_sharedTransitionsOff()352 public void test_multipleDismissals_dismissesOnce_sharedTransitionsOff() { 353 mFeatureFlags.set(CLIPBOARD_SHARED_TRANSITIONS, false); 354 initController(); 355 mCallbacks.onSwipeDismissInitiated(mAnimator); 356 mCallbacks.onDismissButtonTapped(); 357 mCallbacks.onSwipeDismissInitiated(mAnimator); 358 mCallbacks.onDismissButtonTapped(); 359 360 verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED, 0, null); 361 verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED); 362 } 363 364 @Test test_multipleDismissals_dismissesOnce()365 public void test_multipleDismissals_dismissesOnce() { 366 initController(); 367 368 mCallbacks.onSwipeDismissInitiated(mAnimator); 369 mCallbacks.onDismissButtonTapped(); 370 mCallbacks.onSwipeDismissInitiated(mAnimator); 371 mCallbacks.onDismissButtonTapped(); 372 373 verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SWIPE_DISMISSED, 0, null); 374 verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_DISMISS_TAPPED); 375 } 376 377 @Test test_remoteCopy_withFlagOn()378 public void test_remoteCopy_withFlagOn() { 379 initController(); 380 when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(true); 381 382 mOverlayController.setClipData(mSampleClipData, ""); 383 384 verify(mTimeoutHandler, never()).resetTimeout(); 385 } 386 387 @Test test_nonRemoteCopy()388 public void test_nonRemoteCopy() { 389 initController(); 390 when(mClipboardUtils.isRemoteCopy(any(), any(), any())).thenReturn(false); 391 392 mOverlayController.setClipData(mSampleClipData, ""); 393 394 verify(mTimeoutHandler).resetTimeout(); 395 } 396 397 @Test test_logsUseLastClipSource()398 public void test_logsUseLastClipSource() { 399 initController(); 400 401 mOverlayController.setClipData(mSampleClipData, "first.package"); 402 mCallbacks.onShareButtonTapped(); 403 404 mOverlayController.setClipData(mSampleClipData, "second.package"); 405 mCallbacks.onShareButtonTapped(); 406 407 verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "first.package"); 408 verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHARE_TAPPED, 0, "second.package"); 409 verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "first.package"); 410 verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "second.package"); 411 verifyNoMoreInteractions(mUiEventLogger); 412 } 413 414 @Test test_logOnClipboardActionsShown()415 public void test_logOnClipboardActionsShown() { 416 initController(); 417 ClipData.Item item = mSampleClipData.getItemAt(0); 418 item.setTextLinks(Mockito.mock(TextLinks.class)); 419 when(mClipboardUtils.isRemoteCopy(any(Context.class), any(ClipData.class), anyString())) 420 .thenReturn(true); 421 when(mClipboardUtils.getAction(any(TextLinks.class), anyString())) 422 .thenReturn(Optional.of(Mockito.mock(RemoteAction.class))); 423 when(mClipboardOverlayView.post(any(Runnable.class))).thenAnswer(new Answer<Object>() { 424 @Override 425 public Object answer(InvocationOnMock invocation) throws Throwable { 426 ((Runnable) invocation.getArgument(0)).run(); 427 return null; 428 } 429 }); 430 431 mOverlayController.setClipData( 432 new ClipData(mSampleClipData.getDescription(), item), "actionShownSource"); 433 mExecutor.runAllReady(); 434 435 verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_ACTION_SHOWN, 0, "actionShownSource"); 436 verify(mUiEventLogger).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "actionShownSource"); 437 verifyNoMoreInteractions(mUiEventLogger); 438 } 439 440 @Test test_noInsets_showsExpanded()441 public void test_noInsets_showsExpanded() { 442 initController(); 443 mOverlayController.setClipData(mSampleClipData, ""); 444 445 verify(mClipboardOverlayView, never()).setMinimized(true); 446 verify(mClipboardOverlayView).setMinimized(false); 447 verify(mClipboardOverlayView).showTextPreview("Test Item", false); 448 } 449 450 @Test test_insets_showsMinimized()451 public void test_insets_showsMinimized() { 452 initController(); 453 when(mClipboardOverlayWindow.getWindowInsets()).thenReturn( 454 getImeInsets(new Rect(0, 0, 0, 1))); 455 mOverlayController.setClipData(mSampleClipData, "abc"); 456 Animator mockFadeoutAnimator = Mockito.mock(Animator.class); 457 when(mClipboardOverlayView.getMinimizedFadeoutAnimation()).thenReturn(mockFadeoutAnimator); 458 459 verify(mClipboardOverlayView).setMinimized(true); 460 verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED, 0, "abc"); 461 verify(mClipboardOverlayView, never()).setMinimized(false); 462 verify(mClipboardOverlayView, never()).showTextPreview(any(), anyBoolean()); 463 464 mCallbacks.onMinimizedViewTapped(); 465 verify(mockFadeoutAnimator).addListener(mAnimatorArgumentCaptor.capture()); 466 mAnimatorArgumentCaptor.getValue().onAnimationEnd(mockFadeoutAnimator); 467 468 verify(mClipboardOverlayView).setMinimized(false); 469 verify(mClipboardOverlayView).showTextPreview("Test Item", false); 470 verify(mUiEventLogger, times(1)).log(CLIPBOARD_OVERLAY_EXPANDED_FROM_MINIMIZED, 0, "abc"); 471 verify(mUiEventLogger, never()).log(CLIPBOARD_OVERLAY_SHOWN_EXPANDED, 0, "abc"); 472 } 473 474 @Test test_insetsChanged_minimizes()475 public void test_insetsChanged_minimizes() { 476 initController(); 477 mOverlayController.setClipData(mSampleClipData, ""); 478 verify(mClipboardOverlayView, never()).setMinimized(true); 479 480 WindowInsets insetsWithKeyboard = getImeInsets(new Rect(0, 0, 0, 1)); 481 mOverlayController.onInsetsChanged(insetsWithKeyboard, ORIENTATION_PORTRAIT); 482 verify(mClipboardOverlayView).setMinimized(true); 483 } 484 getImeInsets(Rect r)485 private static WindowInsets getImeInsets(Rect r) { 486 return new WindowInsets.Builder().setInsets(WindowInsets.Type.ime(), Insets.of(r)).build(); 487 } 488 } 489