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