1 /*
2  * Copyright (C) 2021 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.Intent.ACTION_CLOSE_SYSTEM_DIALOGS;
20 
21 
22 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.CLIPBOARD_OVERLAY_SHOW_ACTIONS;
23 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
24 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED;
25 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER;
26 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
27 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EDIT_TAPPED;
28 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EXPANDED_FROM_MINIMIZED;
29 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED;
30 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED;
31 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_EXPANDED;
32 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_MINIMIZED;
33 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
34 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
35 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
36 import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT;
37 import static com.android.systemui.flags.Flags.CLIPBOARD_SHARED_TRANSITIONS;
38 
39 import android.animation.Animator;
40 import android.animation.AnimatorListenerAdapter;
41 import android.app.RemoteAction;
42 import android.content.BroadcastReceiver;
43 import android.content.ClipData;
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.IntentFilter;
47 import android.content.pm.PackageManager;
48 import android.hardware.input.InputManager;
49 import android.net.Uri;
50 import android.os.Looper;
51 import android.provider.DeviceConfig;
52 import android.util.Log;
53 import android.view.InputEvent;
54 import android.view.InputEventReceiver;
55 import android.view.InputMonitor;
56 import android.view.MotionEvent;
57 import android.view.WindowInsets;
58 
59 import androidx.annotation.NonNull;
60 import androidx.annotation.Nullable;
61 
62 import com.android.internal.annotations.VisibleForTesting;
63 import com.android.internal.logging.UiEventLogger;
64 import com.android.systemui.R;
65 import com.android.systemui.broadcast.BroadcastDispatcher;
66 import com.android.systemui.broadcast.BroadcastSender;
67 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext;
68 import com.android.systemui.dagger.qualifiers.Background;
69 import com.android.systemui.flags.FeatureFlags;
70 import com.android.systemui.screenshot.TimeoutHandler;
71 
72 import java.util.Optional;
73 import java.util.concurrent.Executor;
74 
75 import javax.inject.Inject;
76 
77 /**
78  * Controls state and UI for the overlay that appears when something is added to the clipboard
79  */
80 public class ClipboardOverlayController implements ClipboardListener.ClipboardOverlay,
81         ClipboardOverlayView.ClipboardOverlayCallbacks {
82     private static final String TAG = "ClipboardOverlayCtrlr";
83 
84     /** Constants for screenshot/copy deconflicting */
85     public static final String SCREENSHOT_ACTION = "com.android.systemui.SCREENSHOT";
86     public static final String SELF_PERMISSION = "com.android.systemui.permission.SELF";
87     public static final String COPY_OVERLAY_ACTION = "com.android.systemui.COPY";
88 
89     private static final int CLIPBOARD_DEFAULT_TIMEOUT_MILLIS = 6000;
90 
91     private final Context mContext;
92     private final ClipboardLogger mClipboardLogger;
93     private final BroadcastDispatcher mBroadcastDispatcher;
94     private final ClipboardOverlayWindow mWindow;
95     private final TimeoutHandler mTimeoutHandler;
96     private final ClipboardOverlayUtils mClipboardUtils;
97     private final FeatureFlags mFeatureFlags;
98     private final Executor mBgExecutor;
99     private final ClipboardImageLoader mClipboardImageLoader;
100     private final ClipboardTransitionExecutor mTransitionExecutor;
101 
102     private final ClipboardOverlayView mView;
103 
104     private Runnable mOnSessionCompleteListener;
105     private Runnable mOnRemoteCopyTapped;
106     private Runnable mOnShareTapped;
107     private Runnable mOnPreviewTapped;
108 
109     private InputMonitor mInputMonitor;
110     private InputEventReceiver mInputEventReceiver;
111 
112     private BroadcastReceiver mCloseDialogsReceiver;
113     private BroadcastReceiver mScreenshotReceiver;
114 
115     private Animator mExitAnimator;
116     private Animator mEnterAnimator;
117 
118     private Runnable mOnUiUpdate;
119 
120     private boolean mShowingUi;
121     private boolean mIsMinimized;
122     private ClipboardModel mClipboardModel;
123 
124     private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks =
125             new ClipboardOverlayView.ClipboardOverlayCallbacks() {
126                 @Override
127                 public void onInteraction() {
128                     if (mOnUiUpdate != null) {
129                         mOnUiUpdate.run();
130                     }
131                 }
132 
133                 @Override
134                 public void onSwipeDismissInitiated(Animator animator) {
135                     mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
136                     mExitAnimator = animator;
137                 }
138 
139                 @Override
140                 public void onDismissComplete() {
141                     hideImmediate();
142                 }
143 
144                 @Override
145                 public void onPreviewTapped() {
146                     if (mOnPreviewTapped != null) {
147                         mOnPreviewTapped.run();
148                     }
149                 }
150 
151                 @Override
152                 public void onShareButtonTapped() {
153                     if (mOnShareTapped != null) {
154                         mOnShareTapped.run();
155                     }
156                 }
157 
158                 @Override
159                 public void onRemoteCopyButtonTapped() {
160                     if (mOnRemoteCopyTapped != null) {
161                         mOnRemoteCopyTapped.run();
162                     }
163                 }
164 
165                 @Override
166                 public void onDismissButtonTapped() {
167                     mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
168                     animateOut();
169                 }
170 
171                 @Override
172                 public void onMinimizedViewTapped() {
173                     animateFromMinimized();
174                 }
175             };
176 
177     @Inject
ClipboardOverlayController(@verlayWindowContext Context context, ClipboardOverlayView clipboardOverlayView, ClipboardOverlayWindow clipboardOverlayWindow, BroadcastDispatcher broadcastDispatcher, BroadcastSender broadcastSender, TimeoutHandler timeoutHandler, FeatureFlags featureFlags, ClipboardOverlayUtils clipboardUtils, @Background Executor bgExecutor, ClipboardImageLoader clipboardImageLoader, ClipboardTransitionExecutor transitionExecutor, UiEventLogger uiEventLogger)178     public ClipboardOverlayController(@OverlayWindowContext Context context,
179             ClipboardOverlayView clipboardOverlayView,
180             ClipboardOverlayWindow clipboardOverlayWindow,
181             BroadcastDispatcher broadcastDispatcher,
182             BroadcastSender broadcastSender,
183             TimeoutHandler timeoutHandler,
184             FeatureFlags featureFlags,
185             ClipboardOverlayUtils clipboardUtils,
186             @Background Executor bgExecutor,
187             ClipboardImageLoader clipboardImageLoader,
188             ClipboardTransitionExecutor transitionExecutor,
189             UiEventLogger uiEventLogger) {
190         mContext = context;
191         mBroadcastDispatcher = broadcastDispatcher;
192         mClipboardImageLoader = clipboardImageLoader;
193         mTransitionExecutor = transitionExecutor;
194 
195         mClipboardLogger = new ClipboardLogger(uiEventLogger);
196 
197         mView = clipboardOverlayView;
198         mWindow = clipboardOverlayWindow;
199         mWindow.init(this::onInsetsChanged, () -> {
200             mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
201             hideImmediate();
202         });
203 
204         mFeatureFlags = featureFlags;
205         mTimeoutHandler = timeoutHandler;
206         mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
207 
208         mClipboardUtils = clipboardUtils;
209         mBgExecutor = bgExecutor;
210 
211         if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
212             mView.setCallbacks(this);
213         } else {
214             mView.setCallbacks(mClipboardCallbacks);
215         }
216 
217         mWindow.withWindowAttached(() -> {
218             mWindow.setContentView(mView);
219             mView.setInsets(mWindow.getWindowInsets(),
220                     mContext.getResources().getConfiguration().orientation);
221         });
222 
223         mTimeoutHandler.setOnTimeoutRunnable(() -> {
224             if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
225                 finish(CLIPBOARD_OVERLAY_TIMED_OUT);
226             } else {
227                 mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT);
228                 animateOut();
229             }
230         });
231 
232         mCloseDialogsReceiver = new BroadcastReceiver() {
233             @Override
234             public void onReceive(Context context, Intent intent) {
235                 if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
236                     if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
237                         finish(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
238                     } else {
239                         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
240                         animateOut();
241                     }
242                 }
243             }
244         };
245 
246         mBroadcastDispatcher.registerReceiver(mCloseDialogsReceiver,
247                 new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS));
248         mScreenshotReceiver = new BroadcastReceiver() {
249             @Override
250             public void onReceive(Context context, Intent intent) {
251                 if (SCREENSHOT_ACTION.equals(intent.getAction())) {
252                     if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
253                         finish(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
254                     } else {
255                         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
256                         animateOut();
257                     }
258                 }
259             }
260         };
261 
262         mBroadcastDispatcher.registerReceiver(mScreenshotReceiver,
263                 new IntentFilter(SCREENSHOT_ACTION), null, null, Context.RECEIVER_EXPORTED,
264                 SELF_PERMISSION);
265         monitorOutsideTouches();
266 
267         Intent copyIntent = new Intent(COPY_OVERLAY_ACTION);
268         // Set package name so the system knows it's safe
269         copyIntent.setPackage(mContext.getPackageName());
270         broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
271     }
272 
273     @VisibleForTesting
onInsetsChanged(WindowInsets insets, int orientation)274     void onInsetsChanged(WindowInsets insets, int orientation) {
275         mView.setInsets(insets, orientation);
276         if (shouldShowMinimized(insets) && !mIsMinimized) {
277             mIsMinimized = true;
278             mView.setMinimized(true);
279         }
280     }
281 
282     @Override // ClipboardListener.ClipboardOverlay
setClipData(ClipData data, String source)283     public void setClipData(ClipData data, String source) {
284         ClipboardModel model = ClipboardModel.fromClipData(mContext, mClipboardUtils, data, source);
285         boolean wasExiting = (mExitAnimator != null && mExitAnimator.isRunning());
286         if (wasExiting) {
287             mExitAnimator.cancel();
288         }
289         boolean shouldAnimate = !model.dataMatches(mClipboardModel) || wasExiting;
290         mClipboardModel = model;
291         mClipboardLogger.setClipSource(mClipboardModel.getSource());
292         if (mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT)) {
293             if (shouldAnimate) {
294                 reset();
295                 mClipboardLogger.setClipSource(mClipboardModel.getSource());
296                 if (shouldShowMinimized(mWindow.getWindowInsets())) {
297                     mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED);
298                     mIsMinimized = true;
299                     mView.setMinimized(true);
300                 } else {
301                     mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED);
302                     setExpandedView(this::animateIn);
303                 }
304                 mView.announceForAccessibility(
305                         getAccessibilityAnnouncement(mClipboardModel.getType()));
306             } else if (!mIsMinimized) {
307                 setExpandedView(() -> {
308                 });
309             }
310         } else {
311             if (shouldAnimate) {
312                 reset();
313                 mClipboardLogger.setClipSource(mClipboardModel.getSource());
314                 if (shouldShowMinimized(mWindow.getWindowInsets())) {
315                     mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED);
316                     mIsMinimized = true;
317                     mView.setMinimized(true);
318                 } else {
319                     mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_EXPANDED);
320                     setExpandedView();
321                     animateIn();
322                 }
323                 mView.announceForAccessibility(
324                         getAccessibilityAnnouncement(mClipboardModel.getType()));
325             } else if (!mIsMinimized) {
326                 setExpandedView();
327             }
328         }
329         if (mClipboardModel.isRemote()) {
330             mTimeoutHandler.cancelTimeout();
331             mOnUiUpdate = null;
332         } else {
333             mOnUiUpdate = mTimeoutHandler::resetTimeout;
334             mOnUiUpdate.run();
335         }
336     }
337 
setExpandedView(Runnable onViewReady)338     private void setExpandedView(Runnable onViewReady) {
339         final ClipboardModel model = mClipboardModel;
340         mView.setMinimized(false);
341         switch (model.getType()) {
342             case TEXT:
343                 if (model.isRemote() || DeviceConfig.getBoolean(
344                         DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
345                     if (model.getTextLinks() != null) {
346                         classifyText(model);
347                     }
348                 }
349                 if (model.isSensitive()) {
350                     mView.showTextPreview(mContext.getString(R.string.clipboard_asterisks), true);
351                 } else {
352                     mView.showTextPreview(model.getText().toString(), false);
353                 }
354                 mView.setEditAccessibilityAction(true);
355                 mOnPreviewTapped = this::editText;
356                 onViewReady.run();
357                 break;
358             case IMAGE:
359                 mView.setEditAccessibilityAction(true);
360                 mOnPreviewTapped = () -> editImage(model.getUri());
361                 if (model.isSensitive()) {
362                     mView.showImagePreview(null);
363                     onViewReady.run();
364                 } else {
365                     mClipboardImageLoader.loadAsync(model.getUri(), (bitmap) -> mView.post(() -> {
366                         if (bitmap == null) {
367                             mView.showDefaultTextPreview();
368                         } else {
369                             mView.showImagePreview(bitmap);
370                         }
371                         onViewReady.run();
372                     }));
373                 }
374                 break;
375             case URI:
376             case OTHER:
377                 mView.showDefaultTextPreview();
378                 onViewReady.run();
379                 break;
380         }
381         if (!model.isRemote()) {
382             maybeShowRemoteCopy(model.getClipData());
383         }
384         if (model.getType() != ClipboardModel.Type.OTHER) {
385             mOnShareTapped = () -> shareContent(model.getClipData());
386             mView.showShareChip();
387         }
388     }
389 
setExpandedView()390     private void setExpandedView() {
391         final ClipboardModel model = mClipboardModel;
392         mView.setMinimized(false);
393         switch (model.getType()) {
394             case TEXT:
395                 if (model.isRemote() || DeviceConfig.getBoolean(
396                         DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
397                     if (model.getTextLinks() != null) {
398                         classifyText(model);
399                     }
400                 }
401                 if (model.isSensitive()) {
402                     mView.showTextPreview(mContext.getString(R.string.clipboard_asterisks), true);
403                 } else {
404                     mView.showTextPreview(model.getText().toString(), false);
405                 }
406                 mView.setEditAccessibilityAction(true);
407                 mOnPreviewTapped = this::editText;
408                 break;
409             case IMAGE:
410                 mBgExecutor.execute(() -> {
411                     if (model.isSensitive() || model.loadThumbnail(mContext) != null) {
412                         mView.post(() -> {
413                             mView.showImagePreview(
414                                     model.isSensitive() ? null : model.loadThumbnail(mContext));
415                             mView.setEditAccessibilityAction(true);
416                         });
417                         mOnPreviewTapped = () -> editImage(model.getUri());
418                     } else {
419                         // image loading failed
420                         mView.post(mView::showDefaultTextPreview);
421                     }
422                 });
423                 break;
424             case URI:
425             case OTHER:
426                 mView.showDefaultTextPreview();
427                 break;
428         }
429         if (!model.isRemote()) {
430             maybeShowRemoteCopy(model.getClipData());
431         }
432         if (model.getType() != ClipboardModel.Type.OTHER) {
433             mOnShareTapped = () -> shareContent(model.getClipData());
434             mView.showShareChip();
435         }
436     }
437 
shouldShowMinimized(WindowInsets insets)438     private boolean shouldShowMinimized(WindowInsets insets) {
439         return insets.getInsets(WindowInsets.Type.ime()).bottom > 0;
440     }
441 
animateFromMinimized()442     private void animateFromMinimized() {
443         if (mEnterAnimator != null && mEnterAnimator.isRunning()) {
444             mEnterAnimator.cancel();
445         }
446         mEnterAnimator = mView.getMinimizedFadeoutAnimation();
447         mEnterAnimator.addListener(new AnimatorListenerAdapter() {
448             @Override
449             public void onAnimationEnd(Animator animation) {
450                 super.onAnimationEnd(animation);
451                 if (mIsMinimized) {
452                     mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_EXPANDED_FROM_MINIMIZED);
453                     mIsMinimized = false;
454                 }
455                 if (mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT)) {
456                     setExpandedView(() -> animateIn());
457                 } else {
458                     setExpandedView();
459                     animateIn();
460                 }
461             }
462         });
463         mEnterAnimator.start();
464     }
465 
getAccessibilityAnnouncement(ClipboardModel.Type type)466     private String getAccessibilityAnnouncement(ClipboardModel.Type type) {
467         if (type == ClipboardModel.Type.TEXT) {
468             return mContext.getString(R.string.clipboard_text_copied);
469         } else if (type == ClipboardModel.Type.IMAGE) {
470             return mContext.getString(R.string.clipboard_image_copied);
471         } else {
472             return mContext.getString(R.string.clipboard_content_copied);
473         }
474     }
475 
classifyText(ClipboardModel model)476     private void classifyText(ClipboardModel model) {
477         mBgExecutor.execute(() -> {
478             Optional<RemoteAction> remoteAction =
479                     mClipboardUtils.getAction(model.getTextLinks(), model.getSource());
480             if (model.equals(mClipboardModel)) {
481                 remoteAction.ifPresent(action -> {
482                     mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN);
483                     mView.post(() -> mView.setActionChip(action, () -> {
484                         if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
485                             finish(CLIPBOARD_OVERLAY_ACTION_TAPPED);
486                         } else {
487                             mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
488                             animateOut();
489                         }
490                     }));
491                 });
492             }
493         });
494     }
495 
maybeShowRemoteCopy(ClipData clipData)496     private void maybeShowRemoteCopy(ClipData clipData) {
497         Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext);
498         // Only show remote copy if it's available.
499         PackageManager packageManager = mContext.getPackageManager();
500         if (packageManager.resolveActivity(
501                 remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) {
502             mView.setRemoteCopyVisibility(true);
503             mOnRemoteCopyTapped = () -> {
504                 mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED);
505                 mContext.startActivity(remoteCopyIntent);
506                 animateOut();
507             };
508         } else {
509             mView.setRemoteCopyVisibility(false);
510         }
511     }
512 
513     @Override // ClipboardListener.ClipboardOverlay
setOnSessionCompleteListener(Runnable runnable)514     public void setOnSessionCompleteListener(Runnable runnable) {
515         mOnSessionCompleteListener = runnable;
516     }
517 
monitorOutsideTouches()518     private void monitorOutsideTouches() {
519         InputManager inputManager = mContext.getSystemService(InputManager.class);
520         mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
521         mInputEventReceiver = new InputEventReceiver(
522                 mInputMonitor.getInputChannel(), Looper.getMainLooper()) {
523             @Override
524             public void onInputEvent(InputEvent event) {
525                 if ((!mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT) || mShowingUi)
526                         && event instanceof MotionEvent) {
527                     MotionEvent motionEvent = (MotionEvent) event;
528                     if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
529                         if (!mView.isInTouchRegion(
530                                 (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
531                             if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
532                                 finish(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
533                             } else {
534                                 mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
535                                 animateOut();
536                             }
537                         }
538                     }
539                 }
540                 finishInputEvent(event, true /* handled */);
541             }
542         };
543     }
544 
editImage(Uri uri)545     private void editImage(Uri uri) {
546         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
547         mContext.startActivity(IntentCreator.getImageEditIntent(uri, mContext));
548         animateOut();
549     }
550 
editText()551     private void editText() {
552         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
553         mContext.startActivity(IntentCreator.getTextEditorIntent(mContext));
554         animateOut();
555     }
556 
shareContent(ClipData clip)557     private void shareContent(ClipData clip) {
558         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SHARE_TAPPED);
559         mContext.startActivity(IntentCreator.getShareIntent(clip, mContext));
560         animateOut();
561     }
562 
animateIn()563     private void animateIn() {
564         if (mEnterAnimator != null && mEnterAnimator.isRunning()) {
565             return;
566         }
567         mEnterAnimator = mView.getEnterAnimation();
568         mEnterAnimator.addListener(new AnimatorListenerAdapter() {
569             @Override
570             public void onAnimationStart(Animator animation) {
571                 super.onAnimationStart(animation);
572                 mShowingUi = true;
573             }
574 
575             @Override
576             public void onAnimationEnd(Animator animation) {
577                 super.onAnimationEnd(animation);
578                 if (mOnUiUpdate != null) {
579                     mOnUiUpdate.run();
580                 }
581             }
582         });
583         mEnterAnimator.start();
584     }
585 
finish(ClipboardOverlayEvent event)586     private void finish(ClipboardOverlayEvent event) {
587         finish(event, null);
588     }
589 
animateOut()590     private void animateOut() {
591         if (mExitAnimator != null && mExitAnimator.isRunning()) {
592             return;
593         }
594         mExitAnimator = mView.getExitAnimation();
595         mExitAnimator.addListener(new AnimatorListenerAdapter() {
596             private boolean mCancelled;
597 
598             @Override
599             public void onAnimationCancel(Animator animation) {
600                 super.onAnimationCancel(animation);
601                 mCancelled = true;
602             }
603 
604             @Override
605             public void onAnimationEnd(Animator animation) {
606                 super.onAnimationEnd(animation);
607                 if (!mCancelled) {
608                     hideImmediate();
609                 }
610             }
611         });
612         mExitAnimator.start();
613     }
614 
finish(ClipboardOverlayEvent event, @Nullable Intent intent)615     private void finish(ClipboardOverlayEvent event, @Nullable Intent intent) {
616         if (mExitAnimator != null && mExitAnimator.isRunning()) {
617             return;
618         }
619         mExitAnimator = mView.getExitAnimation();
620         mExitAnimator.addListener(new AnimatorListenerAdapter() {
621             private boolean mCancelled;
622 
623             @Override
624             public void onAnimationCancel(Animator animation) {
625                 super.onAnimationCancel(animation);
626                 mCancelled = true;
627             }
628 
629             @Override
630             public void onAnimationEnd(Animator animation) {
631                 super.onAnimationEnd(animation);
632                 if (!mCancelled) {
633                     mClipboardLogger.logSessionComplete(event);
634                     if (intent != null) {
635                         mContext.startActivity(intent);
636                     }
637                     hideImmediate();
638                 }
639             }
640         });
641         mExitAnimator.start();
642     }
643 
finishWithSharedTransition(ClipboardOverlayEvent event, Intent intent)644     private void finishWithSharedTransition(ClipboardOverlayEvent event, Intent intent) {
645         if (mExitAnimator != null && mExitAnimator.isRunning()) {
646             return;
647         }
648         mClipboardLogger.logSessionComplete(event);
649         mExitAnimator = mView.getFadeOutAnimation();
650         mExitAnimator.start();
651         mTransitionExecutor.startSharedTransition(
652                 mWindow, mView.getPreview(), intent, this::hideImmediate);
653     }
654 
hideImmediate()655     void hideImmediate() {
656         // Note this may be called multiple times if multiple dismissal events happen at the same
657         // time.
658         mTimeoutHandler.cancelTimeout();
659         mWindow.remove();
660         if (mCloseDialogsReceiver != null) {
661             mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver);
662             mCloseDialogsReceiver = null;
663         }
664         if (mScreenshotReceiver != null) {
665             mBroadcastDispatcher.unregisterReceiver(mScreenshotReceiver);
666             mScreenshotReceiver = null;
667         }
668         if (mInputEventReceiver != null) {
669             mInputEventReceiver.dispose();
670             mInputEventReceiver = null;
671         }
672         if (mInputMonitor != null) {
673             mInputMonitor.dispose();
674             mInputMonitor = null;
675         }
676         if (mOnSessionCompleteListener != null) {
677             mOnSessionCompleteListener.run();
678         }
679     }
680 
reset()681     private void reset() {
682         mOnRemoteCopyTapped = null;
683         mOnShareTapped = null;
684         mOnPreviewTapped = null;
685         mShowingUi = false;
686         mView.reset();
687         mTimeoutHandler.cancelTimeout();
688         mClipboardLogger.reset();
689     }
690 
691     @Override
onDismissButtonTapped()692     public void onDismissButtonTapped() {
693         if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
694             finish(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
695         }
696     }
697 
698     @Override
onRemoteCopyButtonTapped()699     public void onRemoteCopyButtonTapped() {
700         if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
701             finish(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED,
702                     IntentCreator.getRemoteCopyIntent(mClipboardModel.getClipData(), mContext));
703         }
704     }
705 
706     @Override
onShareButtonTapped()707     public void onShareButtonTapped() {
708         if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
709             if (mClipboardModel.getType() != ClipboardModel.Type.OTHER) {
710                 finishWithSharedTransition(CLIPBOARD_OVERLAY_SHARE_TAPPED,
711                         IntentCreator.getShareIntent(mClipboardModel.getClipData(), mContext));
712             }
713         }
714     }
715 
716     @Override
onPreviewTapped()717     public void onPreviewTapped() {
718         if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
719             switch (mClipboardModel.getType()) {
720                 case TEXT:
721                     finish(CLIPBOARD_OVERLAY_EDIT_TAPPED,
722                             IntentCreator.getTextEditorIntent(mContext));
723                     break;
724                 case IMAGE:
725                     finishWithSharedTransition(CLIPBOARD_OVERLAY_EDIT_TAPPED,
726                             IntentCreator.getImageEditIntent(mClipboardModel.getUri(), mContext));
727                     break;
728                 default:
729                     Log.w(TAG, "Got preview tapped callback for non-editable type "
730                             + mClipboardModel.getType());
731             }
732         }
733     }
734 
735     @Override
onMinimizedViewTapped()736     public void onMinimizedViewTapped() {
737         animateFromMinimized();
738     }
739 
740     @Override
onInteraction()741     public void onInteraction() {
742         if (!mClipboardModel.isRemote()) {
743             mTimeoutHandler.resetTimeout();
744         }
745     }
746 
747     @Override
onSwipeDismissInitiated(Animator animator)748     public void onSwipeDismissInitiated(Animator animator) {
749         if (mExitAnimator != null && mExitAnimator.isRunning()) {
750             mExitAnimator.cancel();
751         }
752         mExitAnimator = animator;
753         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
754     }
755 
756     @Override
onDismissComplete()757     public void onDismissComplete() {
758         hideImmediate();
759     }
760 
761     static class ClipboardLogger {
762         private final UiEventLogger mUiEventLogger;
763         private String mClipSource;
764         private boolean mGuarded = false;
765 
ClipboardLogger(UiEventLogger uiEventLogger)766         ClipboardLogger(UiEventLogger uiEventLogger) {
767             mUiEventLogger = uiEventLogger;
768         }
769 
setClipSource(String clipSource)770         void setClipSource(String clipSource) {
771             mClipSource = clipSource;
772         }
773 
logUnguarded(@onNull UiEventLogger.UiEventEnum event)774         void logUnguarded(@NonNull UiEventLogger.UiEventEnum event) {
775             mUiEventLogger.log(event, 0, mClipSource);
776         }
777 
logSessionComplete(@onNull UiEventLogger.UiEventEnum event)778         void logSessionComplete(@NonNull UiEventLogger.UiEventEnum event) {
779             if (!mGuarded) {
780                 mGuarded = true;
781                 mUiEventLogger.log(event, 0, mClipSource);
782             }
783         }
784 
reset()785         void reset() {
786             mGuarded = false;
787             mClipSource = null;
788         }
789     }
790 }
791