• 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.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_REMOTE_COPY_TAPPED;
28 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SHARE_TAPPED;
29 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_SWIPE_DISMISSED;
30 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TAP_OUTSIDE;
31 import static com.android.systemui.clipboardoverlay.ClipboardOverlayEvent.CLIPBOARD_OVERLAY_TIMED_OUT;
32 import static com.android.systemui.flags.Flags.CLIPBOARD_MINIMIZED_LAYOUT;
33 import static com.android.systemui.flags.Flags.CLIPBOARD_REMOTE_BEHAVIOR;
34 
35 import android.animation.Animator;
36 import android.animation.AnimatorListenerAdapter;
37 import android.app.RemoteAction;
38 import android.content.BroadcastReceiver;
39 import android.content.ClipData;
40 import android.content.ClipDescription;
41 import android.content.ContentResolver;
42 import android.content.Context;
43 import android.content.Intent;
44 import android.content.IntentFilter;
45 import android.content.pm.PackageManager;
46 import android.graphics.Bitmap;
47 import android.hardware.input.InputManager;
48 import android.net.Uri;
49 import android.os.Looper;
50 import android.provider.DeviceConfig;
51 import android.text.TextUtils;
52 import android.util.Log;
53 import android.util.Size;
54 import android.view.InputEvent;
55 import android.view.InputEventReceiver;
56 import android.view.InputMonitor;
57 import android.view.MotionEvent;
58 import android.view.WindowInsets;
59 
60 import androidx.annotation.NonNull;
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.io.IOException;
73 import java.util.Optional;
74 import java.util.concurrent.Executor;
75 
76 import javax.inject.Inject;
77 
78 /**
79  * Controls state and UI for the overlay that appears when something is added to the clipboard
80  */
81 public class ClipboardOverlayController implements ClipboardListener.ClipboardOverlay {
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 
100     private final ClipboardOverlayView mView;
101 
102     private Runnable mOnSessionCompleteListener;
103     private Runnable mOnRemoteCopyTapped;
104     private Runnable mOnShareTapped;
105     private Runnable mOnPreviewTapped;
106 
107     private InputMonitor mInputMonitor;
108     private InputEventReceiver mInputEventReceiver;
109 
110     private BroadcastReceiver mCloseDialogsReceiver;
111     private BroadcastReceiver mScreenshotReceiver;
112 
113     private Animator mExitAnimator;
114     private Animator mEnterAnimator;
115 
116     private Runnable mOnUiUpdate;
117 
118     private boolean mIsMinimized;
119     private ClipboardModel mClipboardModel;
120 
121     private final ClipboardOverlayView.ClipboardOverlayCallbacks mClipboardCallbacks =
122             new ClipboardOverlayView.ClipboardOverlayCallbacks() {
123                 @Override
124                 public void onInteraction() {
125                     if (mOnUiUpdate != null) {
126                         mOnUiUpdate.run();
127                     }
128                 }
129 
130                 @Override
131                 public void onSwipeDismissInitiated(Animator animator) {
132                     mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SWIPE_DISMISSED);
133                     mExitAnimator = animator;
134                 }
135 
136                 @Override
137                 public void onDismissComplete() {
138                     hideImmediate();
139                 }
140 
141                 @Override
142                 public void onPreviewTapped() {
143                     if (mOnPreviewTapped != null) {
144                         mOnPreviewTapped.run();
145                     }
146                 }
147 
148                 @Override
149                 public void onShareButtonTapped() {
150                     if (mOnShareTapped != null) {
151                         mOnShareTapped.run();
152                     }
153                 }
154 
155                 @Override
156                 public void onRemoteCopyButtonTapped() {
157                     if (mOnRemoteCopyTapped != null) {
158                         mOnRemoteCopyTapped.run();
159                     }
160                 }
161 
162                 @Override
163                 public void onDismissButtonTapped() {
164                     mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISS_TAPPED);
165                     animateOut();
166                 }
167 
168                 @Override
169                 public void onMinimizedViewTapped() {
170                     if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
171                         animateFromMinimized();
172                     }
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, 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             UiEventLogger uiEventLogger) {
187         mContext = context;
188         mBroadcastDispatcher = broadcastDispatcher;
189 
190         mClipboardLogger = new ClipboardLogger(uiEventLogger);
191 
192         mView = clipboardOverlayView;
193         mWindow = clipboardOverlayWindow;
194         mWindow.init(this::onInsetsChanged, () -> {
195             mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
196             hideImmediate();
197         });
198 
199         mFeatureFlags = featureFlags;
200         mTimeoutHandler = timeoutHandler;
201         mTimeoutHandler.setDefaultTimeoutMillis(CLIPBOARD_DEFAULT_TIMEOUT_MILLIS);
202 
203         mClipboardUtils = clipboardUtils;
204         mBgExecutor = bgExecutor;
205 
206         mView.setCallbacks(mClipboardCallbacks);
207 
208         mWindow.withWindowAttached(() -> {
209             mWindow.setContentView(mView);
210             mView.setInsets(mWindow.getWindowInsets(),
211                     mContext.getResources().getConfiguration().orientation);
212         });
213 
214         mTimeoutHandler.setOnTimeoutRunnable(() -> {
215             mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TIMED_OUT);
216             animateOut();
217         });
218 
219         mCloseDialogsReceiver = new BroadcastReceiver() {
220             @Override
221             public void onReceive(Context context, Intent intent) {
222                 if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
223                     mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
224                     animateOut();
225                 }
226             }
227         };
228 
229         mBroadcastDispatcher.registerReceiver(mCloseDialogsReceiver,
230                 new IntentFilter(ACTION_CLOSE_SYSTEM_DIALOGS));
231         mScreenshotReceiver = new BroadcastReceiver() {
232             @Override
233             public void onReceive(Context context, Intent intent) {
234                 if (SCREENSHOT_ACTION.equals(intent.getAction())) {
235                     mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_DISMISSED_OTHER);
236                     animateOut();
237                 }
238             }
239         };
240 
241         mBroadcastDispatcher.registerReceiver(mScreenshotReceiver,
242                 new IntentFilter(SCREENSHOT_ACTION), null, null, Context.RECEIVER_EXPORTED,
243                 SELF_PERMISSION);
244         monitorOutsideTouches();
245 
246         Intent copyIntent = new Intent(COPY_OVERLAY_ACTION);
247         // Set package name so the system knows it's safe
248         copyIntent.setPackage(mContext.getPackageName());
249         broadcastSender.sendBroadcast(copyIntent, SELF_PERMISSION);
250     }
251 
252     @VisibleForTesting
onInsetsChanged(WindowInsets insets, int orientation)253     void onInsetsChanged(WindowInsets insets, int orientation) {
254         mView.setInsets(insets, orientation);
255         if (mFeatureFlags.isEnabled(CLIPBOARD_MINIMIZED_LAYOUT)) {
256             if (shouldShowMinimized(insets) && !mIsMinimized) {
257                 mIsMinimized = true;
258                 mView.setMinimized(true);
259             }
260         }
261     }
262 
263     @Override // ClipboardListener.ClipboardOverlay
setClipData(ClipData data, String source)264     public void setClipData(ClipData data, String source) {
265         ClipboardModel model = ClipboardModel.fromClipData(mContext, mClipboardUtils, data, source);
266         boolean wasExiting = (mExitAnimator != null && mExitAnimator.isRunning());
267         if (wasExiting) {
268             mExitAnimator.cancel();
269         }
270         boolean shouldAnimate = !model.dataMatches(mClipboardModel) || wasExiting;
271         mClipboardModel = model;
272         mClipboardLogger.setClipSource(mClipboardModel.getSource());
273         if (shouldAnimate) {
274             reset();
275             mClipboardLogger.setClipSource(mClipboardModel.getSource());
276             if (shouldShowMinimized(mWindow.getWindowInsets())) {
277                 mIsMinimized = true;
278                 mView.setMinimized(true);
279             } else {
280                 setExpandedView();
281             }
282             animateIn();
283             mView.announceForAccessibility(getAccessibilityAnnouncement(mClipboardModel.getType()));
284         } else if (!mIsMinimized) {
285             setExpandedView();
286         }
287         if (mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR) && mClipboardModel.isRemote()) {
288             mTimeoutHandler.cancelTimeout();
289             mOnUiUpdate = null;
290         } else {
291             mOnUiUpdate = mTimeoutHandler::resetTimeout;
292             mOnUiUpdate.run();
293         }
294     }
295 
setExpandedView()296     private void setExpandedView() {
297         final ClipboardModel model = mClipboardModel;
298         mView.setMinimized(false);
299         switch (model.getType()) {
300             case TEXT:
301                 if ((mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR) && model.isRemote())
302                         || DeviceConfig.getBoolean(
303                         DeviceConfig.NAMESPACE_SYSTEMUI, CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
304                     if (model.getTextLinks() != null) {
305                         classifyText(model);
306                     }
307                 }
308                 if (model.isSensitive()) {
309                     mView.showTextPreview(mContext.getString(R.string.clipboard_asterisks), true);
310                 } else {
311                     mView.showTextPreview(model.getText(), false);
312                 }
313                 mView.setEditAccessibilityAction(true);
314                 mOnPreviewTapped = this::editText;
315                 break;
316             case IMAGE:
317                 mBgExecutor.execute(() -> {
318                     if (model.isSensitive() || model.loadThumbnail(mContext) != null) {
319                         mView.post(() -> {
320                             mView.showImagePreview(
321                                     model.isSensitive() ? null : model.loadThumbnail(mContext));
322                             mView.setEditAccessibilityAction(true);
323                         });
324                         mOnPreviewTapped = () -> editImage(model.getUri());
325                     } else {
326                         // image loading failed
327                         mView.post(mView::showDefaultTextPreview);
328                     }
329                 });
330                 break;
331             case URI:
332             case OTHER:
333                 mView.showDefaultTextPreview();
334                 break;
335         }
336         if (mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR)) {
337             if (!model.isRemote()) {
338                 maybeShowRemoteCopy(model.getClipData());
339             }
340         } else {
341             maybeShowRemoteCopy(model.getClipData());
342         }
343         if (model.getType() != ClipboardModel.Type.OTHER) {
344             mOnShareTapped = () -> shareContent(model.getClipData());
345             mView.showShareChip();
346         }
347     }
348 
shouldShowMinimized(WindowInsets insets)349     private boolean shouldShowMinimized(WindowInsets insets) {
350         return insets.getInsets(WindowInsets.Type.ime()).bottom > 0;
351     }
352 
animateFromMinimized()353     private void animateFromMinimized() {
354         if (mEnterAnimator != null && mEnterAnimator.isRunning()) {
355             mEnterAnimator.cancel();
356         }
357         mEnterAnimator = mView.getMinimizedFadeoutAnimation();
358         mEnterAnimator.addListener(new AnimatorListenerAdapter() {
359             @Override
360             public void onAnimationEnd(Animator animation) {
361                 super.onAnimationEnd(animation);
362                 mIsMinimized = false;
363                 setExpandedView();
364                 animateIn();
365             }
366         });
367         mEnterAnimator.start();
368     }
369 
getAccessibilityAnnouncement(ClipboardModel.Type type)370     private String getAccessibilityAnnouncement(ClipboardModel.Type type) {
371         if (type == ClipboardModel.Type.TEXT) {
372             return mContext.getString(R.string.clipboard_text_copied);
373         } else if (type == ClipboardModel.Type.IMAGE) {
374             return mContext.getString(R.string.clipboard_image_copied);
375         } else {
376             return mContext.getString(R.string.clipboard_content_copied);
377         }
378     }
379 
classifyText(ClipboardModel model)380     private void classifyText(ClipboardModel model) {
381         mBgExecutor.execute(() -> {
382             Optional<RemoteAction> remoteAction =
383                     mClipboardUtils.getAction(model.getTextLinks(), model.getSource());
384             if (model.equals(mClipboardModel)) {
385                 remoteAction.ifPresent(action -> {
386                     mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN);
387                     mView.post(() -> mView.setActionChip(action, () -> {
388                         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
389                         animateOut();
390                     }));
391                 });
392             }
393         });
394     }
395 
396     @Override // ClipboardListener.ClipboardOverlay
setClipDataLegacy(ClipData clipData, String clipSource)397     public void setClipDataLegacy(ClipData clipData, String clipSource) {
398         if (mExitAnimator != null && mExitAnimator.isRunning()) {
399             mExitAnimator.cancel();
400         }
401         reset();
402         mClipboardLogger.setClipSource(clipSource);
403         String accessibilityAnnouncement = mContext.getString(R.string.clipboard_content_copied);
404 
405         boolean isSensitive = clipData != null && clipData.getDescription().getExtras() != null
406                 && clipData.getDescription().getExtras()
407                 .getBoolean(ClipDescription.EXTRA_IS_SENSITIVE);
408         boolean isRemote = mFeatureFlags.isEnabled(CLIPBOARD_REMOTE_BEHAVIOR)
409                 && mClipboardUtils.isRemoteCopy(mContext, clipData, clipSource);
410         if (clipData == null || clipData.getItemCount() == 0) {
411             mView.showDefaultTextPreview();
412         } else if (!TextUtils.isEmpty(clipData.getItemAt(0).getText())) {
413             ClipData.Item item = clipData.getItemAt(0);
414             if (isRemote || DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
415                     CLIPBOARD_OVERLAY_SHOW_ACTIONS, false)) {
416                 if (item.getTextLinks() != null) {
417                     classifyText(clipData.getItemAt(0), clipSource);
418                 }
419             }
420             if (isSensitive) {
421                 showEditableText(mContext.getString(R.string.clipboard_asterisks), true);
422             } else {
423                 showEditableText(item.getText(), false);
424             }
425             mOnShareTapped = () -> shareContent(clipData);
426             mView.showShareChip();
427             accessibilityAnnouncement = mContext.getString(R.string.clipboard_text_copied);
428         } else if (clipData.getItemAt(0).getUri() != null) {
429             if (tryShowEditableImage(clipData.getItemAt(0).getUri(), isSensitive)) {
430                 accessibilityAnnouncement = mContext.getString(R.string.clipboard_image_copied);
431             }
432             mOnShareTapped = () -> shareContent(clipData);
433             mView.showShareChip();
434         } else {
435             mView.showDefaultTextPreview();
436         }
437         if (!isRemote) {
438             maybeShowRemoteCopy(clipData);
439         }
440         animateIn();
441         mView.announceForAccessibility(accessibilityAnnouncement);
442         if (isRemote) {
443             mTimeoutHandler.cancelTimeout();
444             mOnUiUpdate = null;
445         } else {
446             mOnUiUpdate = mTimeoutHandler::resetTimeout;
447             mOnUiUpdate.run();
448         }
449     }
450 
maybeShowRemoteCopy(ClipData clipData)451     private void maybeShowRemoteCopy(ClipData clipData) {
452         Intent remoteCopyIntent = IntentCreator.getRemoteCopyIntent(clipData, mContext);
453         // Only show remote copy if it's available.
454         PackageManager packageManager = mContext.getPackageManager();
455         if (packageManager.resolveActivity(
456                 remoteCopyIntent, PackageManager.ResolveInfoFlags.of(0)) != null) {
457             mView.setRemoteCopyVisibility(true);
458             mOnRemoteCopyTapped = () -> {
459                 mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_REMOTE_COPY_TAPPED);
460                 mContext.startActivity(remoteCopyIntent);
461                 animateOut();
462             };
463         } else {
464             mView.setRemoteCopyVisibility(false);
465         }
466     }
467 
468     @Override // ClipboardListener.ClipboardOverlay
setOnSessionCompleteListener(Runnable runnable)469     public void setOnSessionCompleteListener(Runnable runnable) {
470         mOnSessionCompleteListener = runnable;
471     }
472 
classifyText(ClipData.Item item, String source)473     private void classifyText(ClipData.Item item, String source) {
474         mBgExecutor.execute(() -> {
475             Optional<RemoteAction> action = mClipboardUtils.getAction(item, source);
476             mView.post(() -> {
477                 mView.resetActionChips();
478                 action.ifPresent(remoteAction -> {
479                     mView.setActionChip(remoteAction, () -> {
480                         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_ACTION_TAPPED);
481                         animateOut();
482                     });
483                     mClipboardLogger.logUnguarded(CLIPBOARD_OVERLAY_ACTION_SHOWN);
484                 });
485             });
486         });
487     }
488 
monitorOutsideTouches()489     private void monitorOutsideTouches() {
490         InputManager inputManager = mContext.getSystemService(InputManager.class);
491         mInputMonitor = inputManager.monitorGestureInput("clipboard overlay", 0);
492         mInputEventReceiver = new InputEventReceiver(
493                 mInputMonitor.getInputChannel(), Looper.getMainLooper()) {
494             @Override
495             public void onInputEvent(InputEvent event) {
496                 if (event instanceof MotionEvent) {
497                     MotionEvent motionEvent = (MotionEvent) event;
498                     if (motionEvent.getActionMasked() == MotionEvent.ACTION_DOWN) {
499                         if (!mView.isInTouchRegion(
500                                 (int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
501                             mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_TAP_OUTSIDE);
502                             animateOut();
503                         }
504                     }
505                 }
506                 finishInputEvent(event, true /* handled */);
507             }
508         };
509     }
510 
editImage(Uri uri)511     private void editImage(Uri uri) {
512         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
513         mContext.startActivity(IntentCreator.getImageEditIntent(uri, mContext));
514         animateOut();
515     }
516 
editText()517     private void editText() {
518         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_EDIT_TAPPED);
519         mContext.startActivity(IntentCreator.getTextEditorIntent(mContext));
520         animateOut();
521     }
522 
shareContent(ClipData clip)523     private void shareContent(ClipData clip) {
524         mClipboardLogger.logSessionComplete(CLIPBOARD_OVERLAY_SHARE_TAPPED);
525         mContext.startActivity(IntentCreator.getShareIntent(clip, mContext));
526         animateOut();
527     }
528 
showEditableText(CharSequence text, boolean hidden)529     private void showEditableText(CharSequence text, boolean hidden) {
530         mView.showTextPreview(text, hidden);
531         mView.setEditAccessibilityAction(true);
532         mOnPreviewTapped = this::editText;
533     }
534 
tryShowEditableImage(Uri uri, boolean isSensitive)535     private boolean tryShowEditableImage(Uri uri, boolean isSensitive) {
536         Runnable listener = () -> editImage(uri);
537         ContentResolver resolver = mContext.getContentResolver();
538         String mimeType = resolver.getType(uri);
539         boolean isEditableImage = mimeType != null && mimeType.startsWith("image");
540         if (isSensitive) {
541             mView.showImagePreview(null);
542             if (isEditableImage) {
543                 mOnPreviewTapped = listener;
544                 mView.setEditAccessibilityAction(true);
545             }
546         } else if (isEditableImage) { // if the MIMEtype is image, try to load
547             try {
548                 int size = mContext.getResources().getDimensionPixelSize(R.dimen.overlay_x_scale);
549                 // The width of the view is capped, height maintains aspect ratio, so allow it to be
550                 // taller if needed.
551                 Bitmap thumbnail = resolver.loadThumbnail(uri, new Size(size, size * 4), null);
552                 mView.showImagePreview(thumbnail);
553                 mView.setEditAccessibilityAction(true);
554                 mOnPreviewTapped = listener;
555             } catch (IOException e) {
556                 Log.e(TAG, "Thumbnail loading failed", e);
557                 mView.showDefaultTextPreview();
558                 isEditableImage = false;
559             }
560         } else {
561             mView.showDefaultTextPreview();
562         }
563         return isEditableImage;
564     }
565 
animateIn()566     private void animateIn() {
567         if (mEnterAnimator != null && mEnterAnimator.isRunning()) {
568             return;
569         }
570         mEnterAnimator = mView.getEnterAnimation();
571         mEnterAnimator.addListener(new AnimatorListenerAdapter() {
572             @Override
573             public void onAnimationEnd(Animator animation) {
574                 super.onAnimationEnd(animation);
575                 if (mOnUiUpdate != null) {
576                     mOnUiUpdate.run();
577                 }
578             }
579         });
580         mEnterAnimator.start();
581     }
582 
animateOut()583     private void animateOut() {
584         if (mExitAnimator != null && mExitAnimator.isRunning()) {
585             return;
586         }
587         Animator anim = mView.getExitAnimation();
588         anim.addListener(new AnimatorListenerAdapter() {
589             private boolean mCancelled;
590 
591             @Override
592             public void onAnimationCancel(Animator animation) {
593                 super.onAnimationCancel(animation);
594                 mCancelled = true;
595             }
596 
597             @Override
598             public void onAnimationEnd(Animator animation) {
599                 super.onAnimationEnd(animation);
600                 if (!mCancelled) {
601                     hideImmediate();
602                 }
603             }
604         });
605         mExitAnimator = anim;
606         anim.start();
607     }
608 
hideImmediate()609     void hideImmediate() {
610         // Note this may be called multiple times if multiple dismissal events happen at the same
611         // time.
612         mTimeoutHandler.cancelTimeout();
613         mWindow.remove();
614         if (mCloseDialogsReceiver != null) {
615             mBroadcastDispatcher.unregisterReceiver(mCloseDialogsReceiver);
616             mCloseDialogsReceiver = null;
617         }
618         if (mScreenshotReceiver != null) {
619             mBroadcastDispatcher.unregisterReceiver(mScreenshotReceiver);
620             mScreenshotReceiver = null;
621         }
622         if (mInputEventReceiver != null) {
623             mInputEventReceiver.dispose();
624             mInputEventReceiver = null;
625         }
626         if (mInputMonitor != null) {
627             mInputMonitor.dispose();
628             mInputMonitor = null;
629         }
630         if (mOnSessionCompleteListener != null) {
631             mOnSessionCompleteListener.run();
632         }
633     }
634 
reset()635     private void reset() {
636         mOnRemoteCopyTapped = null;
637         mOnShareTapped = null;
638         mOnPreviewTapped = null;
639         mView.reset();
640         mTimeoutHandler.cancelTimeout();
641         mClipboardLogger.reset();
642     }
643 
644     static class ClipboardLogger {
645         private final UiEventLogger mUiEventLogger;
646         private String mClipSource;
647         private boolean mGuarded = false;
648 
ClipboardLogger(UiEventLogger uiEventLogger)649         ClipboardLogger(UiEventLogger uiEventLogger) {
650             mUiEventLogger = uiEventLogger;
651         }
652 
setClipSource(String clipSource)653         void setClipSource(String clipSource) {
654             mClipSource = clipSource;
655         }
656 
logUnguarded(@onNull UiEventLogger.UiEventEnum event)657         void logUnguarded(@NonNull UiEventLogger.UiEventEnum event) {
658             mUiEventLogger.log(event, 0, mClipSource);
659         }
660 
logSessionComplete(@onNull UiEventLogger.UiEventEnum event)661         void logSessionComplete(@NonNull UiEventLogger.UiEventEnum event) {
662             if (!mGuarded) {
663                 mGuarded = true;
664                 mUiEventLogger.log(event, 0, mClipSource);
665             }
666         }
667 
reset()668         void reset() {
669             mGuarded = false;
670             mClipSource = null;
671         }
672     }
673 }
674