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