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.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_SHOWN;
23 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_ACTION_TAPPED;
24 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISSED_OTHER;
25 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_DISMISS_TAPPED;
26 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EDIT_TAPPED;
27 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_EXPANDED_FROM_MINIMIZED;
28 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED;
29 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED;
30 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_EXPANDED;
31 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHOWN_MINIMIZED;
32 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
33 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
34 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
35 import static com.android.systemui.flags.Flags.CLIPBOARD_IMAGE_TIMEOUT;
36 import static com.android.systemui.flags.Flags.CLIPBOARD_SHARED_TRANSITIONS;
37 
38 import android.animation.Animator;
39 import android.animation.AnimatorListenerAdapter;
40 import android.app.RemoteAction;
41 import android.content.BroadcastReceiver;
42 import android.content.ClipData;
43 import android.content.Context;
44 import android.content.Intent;
45 import android.content.IntentFilter;
46 import android.content.pm.PackageManager;
47 import android.hardware.input.InputManager;
48 import android.net.Uri;
49 import android.os.Looper;
50 import android.provider.DeviceConfig;
51 import android.util.Log;
52 import android.view.InputEvent;
53 import android.view.InputEventReceiver;
54 import android.view.InputMonitor;
55 import android.view.MotionEvent;
56 import android.view.WindowInsets;
57 
58 import androidx.annotation.NonNull;
59 import androidx.annotation.Nullable;
60 
61 import com.android.internal.annotations.VisibleForTesting;
62 import com.android.internal.logging.UiEventLogger;
63 import com.android.systemui.broadcast.BroadcastDispatcher;
64 import com.android.systemui.broadcast.BroadcastSender;
65 import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule.OverlayWindowContext;
66 import com.android.systemui.dagger.qualifiers.Background;
67 import com.android.systemui.flags.FeatureFlags;
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 FeatureFlags mFeatureFlags;
97     private final Executor mBgExecutor;
98     private final ClipboardImageLoader mClipboardImageLoader;
99     private final ClipboardTransitionExecutor mTransitionExecutor;
100 
101     private final ClipboardOverlayView mView;
102 
103     private Runnable mOnSessionCompleteListener;
104     private Runnable mOnRemoteCopyTapped;
105     private Runnable mOnShareTapped;
106     private Runnable mOnPreviewTapped;
107 
108     private InputMonitor mInputMonitor;
109     private InputEventReceiver mInputEventReceiver;
110 
111     private BroadcastReceiver mCloseDialogsReceiver;
112     private BroadcastReceiver mScreenshotReceiver;
113 
114     private Animator mExitAnimator;
115     private Animator mEnterAnimator;
116 
117     private Runnable mOnUiUpdate;
118 
119     private boolean mShowingUi;
120     private boolean mIsMinimized;
121     private ClipboardModel mClipboardModel;
122 
123     private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks =
124             new ClipboardOverlayView.ClipboardOverlayCallbacks() {
125                 @Override
126                 public void onInteraction() {
127                     if (mOnUiUpdate != null) {
128                         mOnUiUpdate.run();
129                     }
130                 }
131 
132                 @Override
133                 public void onSwipeDismissInitiated(Animator animator) {
134                     mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
135                     mExitAnimator = animator;
136                 }
137 
138                 @Override
139                 public void onDismissComplete() {
140                     hideImmediate();
141                 }
142 
143                 @Override
144                 public void onPreviewTapped() {
145                     if (mOnPreviewTapped != null) {
146                         mOnPreviewTapped.run();
147                     }
148                 }
149 
150                 @Override
151                 public void onShareButtonTapped() {
152                     if (mOnShareTapped != null) {
153                         mOnShareTapped.run();
154                     }
155                 }
156 
157                 @Override
158                 public void onRemoteCopyButtonTapped() {
159                     if (mOnRemoteCopyTapped != null) {
160                         mOnRemoteCopyTapped.run();
161                     }
162                 }
163 
164                 @Override
165                 public void onDismissButtonTapped() {
166                     mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
167                     animateOut();
168                 }
169 
170                 @Override
171                 public void onMinimizedViewTapped() {
172                     animateFromMinimized();
173                 }
174             };
175 
176     @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)177     public ClipboardOverlayController(@OverlayWindowContext Context context,
178             ClipboardOverlayView clipboardOverlayView,
179             ClipboardOverlayWindow clipboardOverlayWindow,
180             BroadcastDispatcher broadcastDispatcher,
181             BroadcastSender broadcastSender,
182             TimeoutHandler timeoutHandler,
183             FeatureFlags featureFlags,
184             ClipboardOverlayUtils clipboardUtils,
185             @Background Executor bgExecutor,
186             ClipboardImageLoader clipboardImageLoader,
187             ClipboardTransitionExecutor transitionExecutor,
188             UiEventLogger uiEventLogger) {
189         mContext = context;
190         mBroadcastDispatcher = broadcastDispatcher;
191         mClipboardImageLoader = clipboardImageLoader;
192         mTransitionExecutor = transitionExecutor;
193 
194         mClipboardLogger = new ClipboardLogger(uiEventLogger);
195 
196         mView = clipboardOverlayView;
197         mWindow = clipboardOverlayWindow;
198         mWindow.init(this::onInsetsChanged, () -> {
199             mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
200             hideImmediate();
201         });
202 
203         mFeatureFlags = featureFlags;
204         mTimeoutHandler = timeoutHandler;
205         mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
206 
207         mClipboardUtils = clipboardUtils;
208         mBgExecutor = bgExecutor;
209 
210         if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
211             mView.setCallbacks(this);
212         } else {
213             mView.setCallbacks(mClipboardCallbacks);
214         }
215 
216         mWindow.withWindowAttached(() -> {
217             mWindow.setContentView(mView);
218             mView.setInsets(mWindow.getWindowInsets(),
219                     mContext.getResources().getConfiguration().orientation);
220         });
221 
222         mTimeoutHandler.setOnTimeoutRunnable(() -> {
223             if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
224                 finish(CLIPBOARD_OVERLAY_TIMED_OUT);
225             } else {
226                 mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT);
227                 animateOut();
228             }
229         });
230 
231         mCloseDialogsReceiver = new BroadcastReceiver() {
232             @Override
233             public void onReceive(Context context, Intent intent) {
234                 if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
235                     if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
236                         finish(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
237                     } else {
238                         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
239                         animateOut();
240                     }
241                 }
242             }
243         };
244 
245         mBroadcastDispatcher.registerReceiver(mCloseDialogsReceiver,
246                 new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS));
247         mScreenshotReceiver = new BroadcastReceiver() {
248             @Override
249             public void onReceive(Context context, Intent intent) {
250                 if (SCREENSHOT_ACTION.equals(intent.getAction())) {
251                     if (mFeatureFlags.isEnabled(CLIPBOARD_SHARED_TRANSITIONS)) {
252                         finish(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
253                     } else {
254                         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
255                         animateOut();
256                     }
257                 }
258             }
259         };
260 
261         mBroadcastDispatcher.registerReceiver(mScreenshotReceiver,
262                 new IntentFilter(SCREENSHOT_ACTION), null, null, Context.RECEIVER_EXPORTED,
263                 SELF_PERMISSION);
264         monitorOutsideTouches();
265 
266         Intent copyIntent = new Intent(COPY_OVERLAY_ACTION);
267         // Set package name so the system knows it's safe
268         copyIntent.setPackage(mContext.getPackageName());
269         broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
270     }
271 
272     @VisibleForTesting
onInsetsChanged(WindowInsets insets, int orientation)273     void onInsetsChanged(WindowInsets insets, int orientation) {
274         mView.setInsets(insets, orientation);
275         if (shouldShowMinimized(insets) && !mIsMinimized) {
276             mIsMinimized = true;
277             mView.setMinimized(true);
278         }
279     }
280 
281     @Override // ClipboardListener.ClipboardOverlay
setClipData(ClipData data, String source)282     public void setClipData(ClipData data, String source) {
283         ClipboardModel model = ClipboardModel.fromClipData(mContext, mClipboardUtils, data, source);
284         boolean wasExiting = (mExitAnimator != null && mExitAnimator.isRunning());
285         if (wasExiting) {
286             mExitAnimator.cancel();
287         }
288         boolean shouldAnimate = !model.dataMatches(mClipboardModel) || wasExiting;
289         mClipboardModel = model;
290         mClipboardLogger.setClipSource(mClipboardModel.getSource());
291         if (mFeatureFlags.isEnabled(CLIPBOARD_IMAGE_TIMEOUT)) {
292             if (shouldAnimate) {
293                 reset();
294                 mClipboardLogger.setClipSource(mClipboardModel.getSource());
295                 if (shouldShowMinimized(mWindow.getWindowInsets())) {
296                     mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_SHOWN_MINIMIZED);
297                     mIsMinimized = true;
298                     mView.setMinimized(true);
299                     animateIn();
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                 }
322                 animateIn();
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