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