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