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