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