1 /* 2 * Copyright (C) 2020 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.screenshot; 18 19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 20 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; 21 22 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; 23 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; 24 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS; 25 import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; 26 import static com.android.systemui.screenshot.LogConfig.DEBUG_UI; 27 import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW; 28 import static com.android.systemui.screenshot.LogConfig.logTag; 29 import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER; 30 import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT; 31 32 import static java.util.Objects.requireNonNull; 33 34 import android.animation.Animator; 35 import android.animation.AnimatorListenerAdapter; 36 import android.annotation.MainThread; 37 import android.annotation.NonNull; 38 import android.annotation.Nullable; 39 import android.app.ActivityManager; 40 import android.app.ActivityOptions; 41 import android.app.ExitTransitionCoordinator; 42 import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks; 43 import android.app.ICompatCameraControlCallback; 44 import android.app.Notification; 45 import android.app.assist.AssistContent; 46 import android.content.BroadcastReceiver; 47 import android.content.Context; 48 import android.content.Intent; 49 import android.content.IntentFilter; 50 import android.content.pm.ActivityInfo; 51 import android.content.res.Configuration; 52 import android.graphics.Bitmap; 53 import android.graphics.Insets; 54 import android.graphics.Rect; 55 import android.hardware.display.DisplayManager; 56 import android.media.AudioAttributes; 57 import android.media.AudioSystem; 58 import android.media.MediaPlayer; 59 import android.net.Uri; 60 import android.os.Bundle; 61 import android.os.Process; 62 import android.os.RemoteException; 63 import android.os.UserHandle; 64 import android.os.UserManager; 65 import android.provider.Settings; 66 import android.util.DisplayMetrics; 67 import android.util.Log; 68 import android.util.Pair; 69 import android.view.Display; 70 import android.view.IRemoteAnimationFinishedCallback; 71 import android.view.IRemoteAnimationRunner; 72 import android.view.KeyEvent; 73 import android.view.LayoutInflater; 74 import android.view.RemoteAnimationAdapter; 75 import android.view.RemoteAnimationTarget; 76 import android.view.ScrollCaptureResponse; 77 import android.view.View; 78 import android.view.ViewRootImpl; 79 import android.view.ViewTreeObserver; 80 import android.view.WindowInsets; 81 import android.view.WindowManager; 82 import android.view.WindowManagerGlobal; 83 import android.view.accessibility.AccessibilityManager; 84 import android.widget.Toast; 85 import android.window.OnBackInvokedCallback; 86 import android.window.OnBackInvokedDispatcher; 87 import android.window.WindowContext; 88 89 import androidx.concurrent.futures.CallbackToFutureAdapter; 90 91 import com.android.internal.app.ChooserActivity; 92 import com.android.internal.logging.UiEventLogger; 93 import com.android.internal.policy.PhoneWindow; 94 import com.android.settingslib.applications.InterestingConfigChanges; 95 import com.android.systemui.R; 96 import com.android.systemui.broadcast.BroadcastSender; 97 import com.android.systemui.clipboardoverlay.ClipboardOverlayController; 98 import com.android.systemui.dagger.qualifiers.Main; 99 import com.android.systemui.flags.FeatureFlags; 100 import com.android.systemui.flags.Flags; 101 import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition; 102 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback; 103 import com.android.systemui.settings.DisplayTracker; 104 import com.android.systemui.util.Assert; 105 106 import com.google.common.util.concurrent.ListenableFuture; 107 108 import java.io.File; 109 import java.util.List; 110 import java.util.concurrent.CancellationException; 111 import java.util.concurrent.ExecutionException; 112 import java.util.concurrent.Executor; 113 import java.util.concurrent.ExecutorService; 114 import java.util.concurrent.Executors; 115 import java.util.concurrent.Future; 116 import java.util.concurrent.TimeUnit; 117 import java.util.concurrent.TimeoutException; 118 import java.util.function.Consumer; 119 import java.util.function.Supplier; 120 121 import javax.inject.Inject; 122 123 /** 124 * Controls the state and flow for screenshots. 125 */ 126 public class ScreenshotController { 127 private static final String TAG = logTag(ScreenshotController.class); 128 129 private ScrollCaptureResponse mLastScrollCaptureResponse; 130 private ListenableFuture<ScrollCaptureResponse> mLastScrollCaptureRequest; 131 132 /** 133 * This is effectively a no-op, but we need something non-null to pass in, in order to 134 * successfully override the pending activity entrance animation. 135 */ 136 static final IRemoteAnimationRunner.Stub SCREENSHOT_REMOTE_RUNNER = 137 new IRemoteAnimationRunner.Stub() { 138 @Override 139 public void onAnimationStart( 140 @WindowManager.TransitionOldType int transit, 141 RemoteAnimationTarget[] apps, 142 RemoteAnimationTarget[] wallpapers, 143 RemoteAnimationTarget[] nonApps, 144 final IRemoteAnimationFinishedCallback finishedCallback) { 145 try { 146 finishedCallback.onAnimationFinished(); 147 } catch (RemoteException e) { 148 Log.e(TAG, "Error finishing screenshot remote animation", e); 149 } 150 } 151 152 @Override 153 public void onAnimationCancelled() { 154 } 155 }; 156 157 /** 158 * POD used in the AsyncTask which saves an image in the background. 159 */ 160 static class SaveImageInBackgroundData { 161 public Bitmap image; 162 public Consumer<Uri> finisher; 163 public ScreenshotController.ActionsReadyListener mActionsReadyListener; 164 public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener; 165 public UserHandle owner; 166 clearImage()167 void clearImage() { 168 image = null; 169 } 170 } 171 172 /** 173 * Structure returned by the SaveImageInBackgroundTask 174 */ 175 static class SavedImageData { 176 public Uri uri; 177 public Supplier<ActionTransition> shareTransition; 178 public Supplier<ActionTransition> editTransition; 179 public Notification.Action deleteAction; 180 public List<Notification.Action> smartActions; 181 public Notification.Action quickShareAction; 182 public UserHandle owner; 183 public String subject; // Title for sharing 184 185 /** 186 * POD for shared element transition. 187 */ 188 static class ActionTransition { 189 public Bundle bundle; 190 public Notification.Action action; 191 public Runnable onCancelRunnable; 192 } 193 194 /** 195 * Used to reset the return data on error 196 */ reset()197 public void reset() { 198 uri = null; 199 shareTransition = null; 200 editTransition = null; 201 deleteAction = null; 202 smartActions = null; 203 quickShareAction = null; 204 subject = null; 205 } 206 } 207 208 /** 209 * Structure returned by the QueryQuickShareInBackgroundTask 210 */ 211 static class QuickShareData { 212 public Notification.Action quickShareAction; 213 214 /** 215 * Used to reset the return data on error 216 */ reset()217 public void reset() { 218 quickShareAction = null; 219 } 220 } 221 222 interface ActionsReadyListener { onActionsReady(ScreenshotController.SavedImageData imageData)223 void onActionsReady(ScreenshotController.SavedImageData imageData); 224 } 225 226 interface QuickShareActionReadyListener { onActionsReady(ScreenshotController.QuickShareData quickShareData)227 void onActionsReady(ScreenshotController.QuickShareData quickShareData); 228 } 229 230 interface TransitionDestination { 231 /** 232 * Allows the long screenshot activity to call back with a destination location (the bounds 233 * on screen of the destination for the transitioning view) and a Runnable to be run once 234 * the transition animation is complete. 235 */ setTransitionDestination(Rect transitionDestination, Runnable onTransitionEnd)236 void setTransitionDestination(Rect transitionDestination, Runnable onTransitionEnd); 237 } 238 239 // These strings are used for communicating the action invoked to 240 // ScreenshotNotificationSmartActionsProvider. 241 static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type"; 242 static final String EXTRA_ID = "android:screenshot_id"; 243 static final String ACTION_TYPE_DELETE = "Delete"; 244 static final String ACTION_TYPE_SHARE = "Share"; 245 static final String ACTION_TYPE_EDIT = "Edit"; 246 static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled"; 247 static final String EXTRA_OVERRIDE_TRANSITION = "android:screenshot_override_transition"; 248 static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent"; 249 static final String EXTRA_ACTION_INTENT_FILLIN = "android:screenshot_action_intent_fillin"; 250 251 static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; 252 static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification"; 253 static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip"; 254 255 // From WizardManagerHelper.java 256 private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; 257 258 private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; 259 260 private final WindowContext mContext; 261 private final FeatureFlags mFlags; 262 private final ScreenshotNotificationsController mNotificationsController; 263 private final ScreenshotSmartActions mScreenshotSmartActions; 264 private final UiEventLogger mUiEventLogger; 265 private final ImageExporter mImageExporter; 266 private final ImageCapture mImageCapture; 267 private final Executor mMainExecutor; 268 private final ExecutorService mBgExecutor; 269 private final BroadcastSender mBroadcastSender; 270 271 private final WindowManager mWindowManager; 272 private final WindowManager.LayoutParams mWindowLayoutParams; 273 private final AccessibilityManager mAccessibilityManager; 274 private final ListenableFuture<MediaPlayer> mCameraSound; 275 private final ScrollCaptureClient mScrollCaptureClient; 276 private final PhoneWindow mWindow; 277 private final DisplayManager mDisplayManager; 278 private final DisplayTracker mDisplayTracker; 279 private final ScrollCaptureController mScrollCaptureController; 280 private final LongScreenshotData mLongScreenshotHolder; 281 private final boolean mIsLowRamDevice; 282 private final ScreenshotNotificationSmartActionsProvider 283 mScreenshotNotificationSmartActionsProvider; 284 private final TimeoutHandler mScreenshotHandler; 285 private final ActionIntentExecutor mActionExecutor; 286 private final UserManager mUserManager; 287 private final AssistContentRequester mAssistContentRequester; 288 289 private final OnBackInvokedCallback mOnBackInvokedCallback = () -> { 290 if (DEBUG_INPUT) { 291 Log.d(TAG, "Predictive Back callback dispatched"); 292 } 293 respondToKeyDismissal(); 294 }; 295 296 private ScreenshotView mScreenshotView; 297 private final MessageContainerController mMessageContainerController; 298 private Bitmap mScreenBitmap; 299 private SaveImageInBackgroundTask mSaveInBgTask; 300 private boolean mScreenshotTakenInPortrait; 301 private boolean mBlockAttach; 302 303 private Animator mScreenshotAnimation; 304 private RequestCallback mCurrentRequestCallback; 305 private String mPackageName = ""; 306 private BroadcastReceiver mCopyBroadcastReceiver; 307 308 /** Tracks config changes that require re-creating UI */ 309 private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges( 310 ActivityInfo.CONFIG_ORIENTATION 311 | ActivityInfo.CONFIG_LAYOUT_DIRECTION 312 | ActivityInfo.CONFIG_LOCALE 313 | ActivityInfo.CONFIG_UI_MODE 314 | ActivityInfo.CONFIG_SCREEN_LAYOUT 315 | ActivityInfo.CONFIG_ASSETS_PATHS); 316 317 @Inject ScreenshotController( Context context, FeatureFlags flags, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController screenshotNotificationsController, ScrollCaptureClient scrollCaptureClient, UiEventLogger uiEventLogger, ImageExporter imageExporter, ImageCapture imageCapture, @Main Executor mainExecutor, ScrollCaptureController scrollCaptureController, LongScreenshotData longScreenshotHolder, ActivityManager activityManager, TimeoutHandler timeoutHandler, BroadcastSender broadcastSender, ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, ActionIntentExecutor actionExecutor, UserManager userManager, AssistContentRequester assistContentRequester, MessageContainerController messageContainerController, DisplayTracker displayTracker )318 ScreenshotController( 319 Context context, 320 FeatureFlags flags, 321 ScreenshotSmartActions screenshotSmartActions, 322 ScreenshotNotificationsController screenshotNotificationsController, 323 ScrollCaptureClient scrollCaptureClient, 324 UiEventLogger uiEventLogger, 325 ImageExporter imageExporter, 326 ImageCapture imageCapture, 327 @Main Executor mainExecutor, 328 ScrollCaptureController scrollCaptureController, 329 LongScreenshotData longScreenshotHolder, 330 ActivityManager activityManager, 331 TimeoutHandler timeoutHandler, 332 BroadcastSender broadcastSender, 333 ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, 334 ActionIntentExecutor actionExecutor, 335 UserManager userManager, 336 AssistContentRequester assistContentRequester, 337 MessageContainerController messageContainerController, 338 DisplayTracker displayTracker 339 ) { 340 mScreenshotSmartActions = screenshotSmartActions; 341 mNotificationsController = screenshotNotificationsController; 342 mScrollCaptureClient = scrollCaptureClient; 343 mUiEventLogger = uiEventLogger; 344 mImageExporter = imageExporter; 345 mImageCapture = imageCapture; 346 mMainExecutor = mainExecutor; 347 mScrollCaptureController = scrollCaptureController; 348 mLongScreenshotHolder = longScreenshotHolder; 349 mIsLowRamDevice = activityManager.isLowRamDevice(); 350 mScreenshotNotificationSmartActionsProvider = screenshotNotificationSmartActionsProvider; 351 mBgExecutor = Executors.newSingleThreadExecutor(); 352 mBroadcastSender = broadcastSender; 353 354 mScreenshotHandler = timeoutHandler; 355 mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS); 356 mScreenshotHandler.setOnTimeoutRunnable(() -> { 357 if (DEBUG_UI) { 358 Log.d(TAG, "Corner timeout hit"); 359 } 360 dismissScreenshot(SCREENSHOT_INTERACTION_TIMEOUT); 361 }); 362 363 mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class)); 364 mDisplayTracker = displayTracker; 365 final Context displayContext = context.createDisplayContext(getDefaultDisplay()); 366 mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); 367 mWindowManager = mContext.getSystemService(WindowManager.class); 368 mFlags = flags; 369 mActionExecutor = actionExecutor; 370 mUserManager = userManager; 371 mMessageContainerController = messageContainerController; 372 mAssistContentRequester = assistContentRequester; 373 374 mAccessibilityManager = AccessibilityManager.getInstance(mContext); 375 376 // Setup the window that we are going to use 377 mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); 378 mWindowLayoutParams.setTitle("ScreenshotAnimation"); 379 380 mWindow = FloatingWindowUtil.getFloatingWindow(mContext); 381 mWindow.setWindowManager(mWindowManager, null, null); 382 383 mConfigChanges.applyNewConfig(context.getResources()); 384 reloadAssets(); 385 386 // Setup the Camera shutter sound 387 mCameraSound = loadCameraSound(); 388 389 mCopyBroadcastReceiver = new BroadcastReceiver() { 390 @Override 391 public void onReceive(Context context, Intent intent) { 392 if (ClipboardOverlayController.COPY_OVERLAY_ACTION.equals(intent.getAction())) { 393 dismissScreenshot(SCREENSHOT_DISMISSED_OTHER); 394 } 395 } 396 }; 397 mContext.registerReceiver(mCopyBroadcastReceiver, new IntentFilter( 398 ClipboardOverlayController.COPY_OVERLAY_ACTION), 399 ClipboardOverlayController.SELF_PERMISSION, null, Context.RECEIVER_NOT_EXPORTED); 400 } 401 handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, RequestCallback requestCallback)402 void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, 403 RequestCallback requestCallback) { 404 Assert.isMainThread(); 405 mCurrentRequestCallback = requestCallback; 406 if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) { 407 Rect bounds = getFullScreenRect(); 408 screenshot.setBitmap( 409 mImageCapture.captureDisplay(mDisplayTracker.getDefaultDisplayId(), bounds)); 410 screenshot.setScreenBounds(bounds); 411 } 412 413 if (screenshot.getBitmap() == null) { 414 Log.e(TAG, "handleScreenshot: Screenshot bitmap was null"); 415 mNotificationsController.notifyScreenshotError( 416 R.string.screenshot_failed_to_capture_text); 417 if (mCurrentRequestCallback != null) { 418 mCurrentRequestCallback.reportError(); 419 } 420 return; 421 } 422 423 mScreenBitmap = screenshot.getBitmap(); 424 String oldPackageName = mPackageName; 425 mPackageName = screenshot.getPackageNameString(); 426 427 if (!isUserSetupComplete(Process.myUserHandle())) { 428 Log.w(TAG, "User setup not complete, displaying toast only"); 429 // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing 430 // and sharing shouldn't be exposed to the user. 431 saveScreenshotAndToast(screenshot.getUserHandle(), finisher); 432 return; 433 } 434 435 mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION), 436 ClipboardOverlayController.SELF_PERMISSION); 437 438 mScreenshotTakenInPortrait = 439 mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; 440 441 // Optimizations 442 mScreenBitmap.setHasAlpha(false); 443 mScreenBitmap.prepareToDraw(); 444 445 prepareViewForNewScreenshot(screenshot, oldPackageName); 446 447 saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher, 448 this::showUiOnActionsReady, this::showUiOnQuickShareActionReady); 449 450 // The window is focusable by default 451 setWindowFocusable(true); 452 mScreenshotView.requestFocus(); 453 454 enqueueScrollCaptureRequest(screenshot.getUserHandle()); 455 456 attachWindow(); 457 458 boolean showFlash = true; 459 if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) { 460 if (screenshot.getScreenBounds() != null 461 && aspectRatiosMatch(screenshot.getBitmap(), screenshot.getInsets(), 462 screenshot.getScreenBounds())) { 463 showFlash = false; 464 } else { 465 showFlash = true; 466 screenshot.setInsets(Insets.NONE); 467 screenshot.setScreenBounds(new Rect(0, 0, screenshot.getBitmap().getWidth(), 468 screenshot.getBitmap().getHeight())); 469 } 470 } 471 472 prepareAnimation(screenshot.getScreenBounds(), showFlash, () -> { 473 mMessageContainerController.onScreenshotTaken(screenshot); 474 }); 475 476 mScreenshotView.badgeScreenshot(mContext.getPackageManager().getUserBadgedIcon( 477 mContext.getDrawable(R.drawable.overlay_badge_background), 478 screenshot.getUserHandle())); 479 mScreenshotView.setScreenshot(screenshot); 480 481 if (mFlags.isEnabled(Flags.SCREENSHOT_METADATA) && screenshot.getTaskId() >= 0) { 482 mAssistContentRequester.requestAssistContent(screenshot.getTaskId(), 483 new AssistContentRequester.Callback() { 484 @Override 485 public void onAssistContentAvailable(AssistContent assistContent) { 486 screenshot.setContextUrl(assistContent.getWebUri()); 487 } 488 }); 489 } 490 491 // ignore system bar insets for the purpose of window layout 492 mWindow.getDecorView().setOnApplyWindowInsetsListener( 493 (v, insets) -> WindowInsets.CONSUMED); 494 mScreenshotHandler.cancelTimeout(); // restarted after animation 495 } 496 prepareViewForNewScreenshot(ScreenshotData screenshot, String oldPackageName)497 void prepareViewForNewScreenshot(ScreenshotData screenshot, String oldPackageName) { 498 withWindowAttached(() -> { 499 if (mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) { 500 mScreenshotView.announceForAccessibility(mContext.getResources().getString( 501 R.string.screenshot_saving_work_profile_title)); 502 } else { 503 mScreenshotView.announceForAccessibility( 504 mContext.getResources().getString(R.string.screenshot_saving_title)); 505 } 506 }); 507 508 mScreenshotView.reset(); 509 510 if (mScreenshotView.isAttachedToWindow()) { 511 // if we didn't already dismiss for another reason 512 if (!mScreenshotView.isDismissing()) { 513 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0, 514 oldPackageName); 515 } 516 if (DEBUG_WINDOW) { 517 Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. " 518 + "(dismissing=" + mScreenshotView.isDismissing() + ")"); 519 } 520 } 521 522 mScreenshotView.setPackageName(mPackageName); 523 524 mScreenshotView.updateOrientation( 525 mWindowManager.getCurrentWindowMetrics().getWindowInsets()); 526 } 527 528 /** 529 * Clears current screenshot 530 */ dismissScreenshot(ScreenshotEvent event)531 void dismissScreenshot(ScreenshotEvent event) { 532 if (DEBUG_DISMISS) { 533 Log.d(TAG, "dismissScreenshot"); 534 } 535 // If we're already animating out, don't restart the animation 536 if (mScreenshotView.isDismissing()) { 537 if (DEBUG_DISMISS) { 538 Log.v(TAG, "Already dismissing, ignoring duplicate command"); 539 } 540 return; 541 } 542 mUiEventLogger.log(event, 0, mPackageName); 543 mScreenshotHandler.cancelTimeout(); 544 mScreenshotView.animateDismissal(); 545 } 546 isPendingSharedTransition()547 boolean isPendingSharedTransition() { 548 return mScreenshotView.isPendingSharedTransition(); 549 } 550 551 // Any cleanup needed when the service is being destroyed. onDestroy()552 void onDestroy() { 553 if (mSaveInBgTask != null) { 554 // just log success/failure for the pre-existing screenshot 555 mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); 556 } 557 removeWindow(); 558 releaseMediaPlayer(); 559 releaseContext(); 560 mBgExecutor.shutdownNow(); 561 } 562 563 /** 564 * Release the constructed window context. 565 */ releaseContext()566 private void releaseContext() { 567 mContext.unregisterReceiver(mCopyBroadcastReceiver); 568 mContext.release(); 569 } 570 releaseMediaPlayer()571 private void releaseMediaPlayer() { 572 // Note that this may block if the sound is still being loaded (very unlikely) but we can't 573 // reliably release in the background because the service is being destroyed. 574 try { 575 MediaPlayer player = mCameraSound.get(1, TimeUnit.SECONDS); 576 if (player != null) { 577 player.release(); 578 } 579 } catch (InterruptedException | ExecutionException | TimeoutException e) { 580 mCameraSound.cancel(true); 581 Log.w(TAG, "Error releasing shutter sound", e); 582 } 583 } 584 respondToKeyDismissal()585 private void respondToKeyDismissal() { 586 dismissScreenshot(SCREENSHOT_DISMISSED_OTHER); 587 } 588 589 /** 590 * Update resources on configuration change. Reinflate for theme/color changes. 591 */ reloadAssets()592 private void reloadAssets() { 593 if (DEBUG_UI) { 594 Log.d(TAG, "reloadAssets()"); 595 } 596 597 // Inflate the screenshot layout 598 mScreenshotView = (ScreenshotView) 599 LayoutInflater.from(mContext).inflate(R.layout.screenshot, null); 600 mMessageContainerController.setView(mScreenshotView); 601 mScreenshotView.addOnAttachStateChangeListener( 602 new View.OnAttachStateChangeListener() { 603 @Override 604 public void onViewAttachedToWindow(@NonNull View v) { 605 if (DEBUG_INPUT) { 606 Log.d(TAG, "Registering Predictive Back callback"); 607 } 608 mScreenshotView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback( 609 OnBackInvokedDispatcher.PRIORITY_DEFAULT, mOnBackInvokedCallback); 610 } 611 612 @Override 613 public void onViewDetachedFromWindow(@NonNull View v) { 614 if (DEBUG_INPUT) { 615 Log.d(TAG, "Unregistering Predictive Back callback"); 616 } 617 mScreenshotView.findOnBackInvokedDispatcher() 618 .unregisterOnBackInvokedCallback(mOnBackInvokedCallback); 619 } 620 }); 621 mScreenshotView.init(mUiEventLogger, new ScreenshotView.ScreenshotViewCallback() { 622 @Override 623 public void onUserInteraction() { 624 if (DEBUG_INPUT) { 625 Log.d(TAG, "onUserInteraction"); 626 } 627 mScreenshotHandler.resetTimeout(); 628 } 629 630 @Override 631 public void onDismiss() { 632 finishDismiss(); 633 } 634 635 @Override 636 public void onTouchOutside() { 637 // TODO(159460485): Remove this when focus is handled properly in the system 638 setWindowFocusable(false); 639 } 640 }, mActionExecutor, mFlags); 641 mScreenshotView.setDefaultDisplay(mDisplayTracker.getDefaultDisplayId()); 642 mScreenshotView.setDefaultTimeoutMillis(mScreenshotHandler.getDefaultTimeoutMillis()); 643 644 mScreenshotView.setOnKeyListener((v, keyCode, event) -> { 645 if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { 646 if (DEBUG_INPUT) { 647 Log.d(TAG, "onKeyEvent: " + keyCode); 648 } 649 respondToKeyDismissal(); 650 return true; 651 } 652 return false; 653 }); 654 655 if (DEBUG_WINDOW) { 656 Log.d(TAG, "adding OnComputeInternalInsetsListener"); 657 } 658 mScreenshotView.getViewTreeObserver().addOnComputeInternalInsetsListener(mScreenshotView); 659 if (DEBUG_WINDOW) { 660 Log.d(TAG, "setContentView: " + mScreenshotView); 661 } 662 setContentView(mScreenshotView); 663 } 664 prepareAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete)665 private void prepareAnimation(Rect screenRect, boolean showFlash, 666 Runnable onAnimationComplete) { 667 mScreenshotView.getViewTreeObserver().addOnPreDrawListener( 668 new ViewTreeObserver.OnPreDrawListener() { 669 @Override 670 public boolean onPreDraw() { 671 if (DEBUG_WINDOW) { 672 Log.d(TAG, "onPreDraw: startAnimation"); 673 } 674 mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this); 675 startAnimation(screenRect, showFlash, onAnimationComplete); 676 return true; 677 } 678 }); 679 } 680 enqueueScrollCaptureRequest(UserHandle owner)681 private void enqueueScrollCaptureRequest(UserHandle owner) { 682 // Wait until this window is attached to request because it is 683 // the reference used to locate the target window (below). 684 withWindowAttached(() -> { 685 requestScrollCapture(owner); 686 mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( 687 new ViewRootImpl.ActivityConfigCallback() { 688 @Override 689 public void onConfigurationChanged(Configuration overrideConfig, 690 int newDisplayId) { 691 if (mConfigChanges.applyNewConfig(mContext.getResources())) { 692 // Hide the scroll chip until we know it's available in this 693 // orientation 694 mScreenshotView.hideScrollChip(); 695 // Delay scroll capture eval a bit to allow the underlying activity 696 // to set up in the new orientation. 697 mScreenshotHandler.postDelayed(() -> { 698 requestScrollCapture(owner); 699 }, 150); 700 mScreenshotView.updateInsets( 701 mWindowManager.getCurrentWindowMetrics().getWindowInsets()); 702 // Screenshot animation calculations won't be valid anymore, 703 // so just end 704 if (mScreenshotAnimation != null 705 && mScreenshotAnimation.isRunning()) { 706 mScreenshotAnimation.end(); 707 } 708 } 709 } 710 711 @Override 712 public void requestCompatCameraControl(boolean showControl, 713 boolean transformationApplied, 714 ICompatCameraControlCallback callback) { 715 Log.w(TAG, "Unexpected requestCompatCameraControl callback"); 716 } 717 }); 718 }); 719 } 720 requestScrollCapture(UserHandle owner)721 private void requestScrollCapture(UserHandle owner) { 722 if (!allowLongScreenshots()) { 723 Log.d(TAG, "Long screenshots not supported on this device"); 724 return; 725 } 726 mScrollCaptureClient.setHostWindowToken(mWindow.getDecorView().getWindowToken()); 727 if (mLastScrollCaptureRequest != null) { 728 mLastScrollCaptureRequest.cancel(true); 729 } 730 final ListenableFuture<ScrollCaptureResponse> future = 731 mScrollCaptureClient.request(mDisplayTracker.getDefaultDisplayId()); 732 mLastScrollCaptureRequest = future; 733 mLastScrollCaptureRequest.addListener(() -> 734 onScrollCaptureResponseReady(future, owner), mMainExecutor); 735 } 736 onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture, UserHandle owner)737 private void onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture, 738 UserHandle owner) { 739 try { 740 if (mLastScrollCaptureResponse != null) { 741 mLastScrollCaptureResponse.close(); 742 mLastScrollCaptureResponse = null; 743 } 744 if (responseFuture.isCancelled()) { 745 return; 746 } 747 mLastScrollCaptureResponse = responseFuture.get(); 748 if (!mLastScrollCaptureResponse.isConnected()) { 749 // No connection means that the target window wasn't found 750 // or that it cannot support scroll capture. 751 Log.d(TAG, "ScrollCapture: " + mLastScrollCaptureResponse.getDescription() + " [" 752 + mLastScrollCaptureResponse.getWindowTitle() + "]"); 753 return; 754 } 755 Log.d(TAG, "ScrollCapture: connected to window [" 756 + mLastScrollCaptureResponse.getWindowTitle() + "]"); 757 758 final ScrollCaptureResponse response = mLastScrollCaptureResponse; 759 mScreenshotView.showScrollChip(response.getPackageName(), /* onClick */ () -> { 760 DisplayMetrics displayMetrics = new DisplayMetrics(); 761 getDefaultDisplay().getRealMetrics(displayMetrics); 762 Bitmap newScreenshot = mImageCapture.captureDisplay( 763 mDisplayTracker.getDefaultDisplayId(), 764 new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)); 765 766 mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot, 767 mScreenshotTakenInPortrait); 768 // delay starting scroll capture to make sure the scrim is up before the app moves 769 mScreenshotView.post(() -> runBatchScrollCapture(response, owner)); 770 }); 771 } catch (InterruptedException | ExecutionException e) { 772 Log.e(TAG, "requestScrollCapture failed", e); 773 } 774 } 775 776 ListenableFuture<ScrollCaptureController.LongScreenshot> mLongScreenshotFuture; 777 runBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner)778 private void runBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) { 779 // Clear the reference to prevent close() in dismissScreenshot 780 mLastScrollCaptureResponse = null; 781 782 if (mLongScreenshotFuture != null) { 783 mLongScreenshotFuture.cancel(true); 784 } 785 mLongScreenshotFuture = mScrollCaptureController.run(response); 786 mLongScreenshotFuture.addListener(() -> { 787 ScrollCaptureController.LongScreenshot longScreenshot; 788 try { 789 longScreenshot = mLongScreenshotFuture.get(); 790 } catch (CancellationException e) { 791 Log.e(TAG, "Long screenshot cancelled"); 792 return; 793 } catch (InterruptedException | ExecutionException e) { 794 Log.e(TAG, "Exception", e); 795 mScreenshotView.restoreNonScrollingUi(); 796 return; 797 } 798 799 if (longScreenshot.getHeight() == 0) { 800 mScreenshotView.restoreNonScrollingUi(); 801 return; 802 } 803 804 mLongScreenshotHolder.setLongScreenshot(longScreenshot); 805 mLongScreenshotHolder.setTransitionDestinationCallback( 806 (transitionDestination, onTransitionEnd) -> { 807 mScreenshotView.startLongScreenshotTransition( 808 transitionDestination, onTransitionEnd, 809 longScreenshot); 810 // TODO: Do this via ActionIntentExecutor instead. 811 mContext.closeSystemDialogs(); 812 } 813 ); 814 815 final Intent intent = new Intent(mContext, LongScreenshotActivity.class); 816 intent.putExtra(LongScreenshotActivity.EXTRA_SCREENSHOT_USER_HANDLE, 817 owner); 818 intent.setFlags( 819 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 820 821 mContext.startActivity(intent, 822 ActivityOptions.makeCustomAnimation(mContext, 0, 0).toBundle()); 823 RemoteAnimationAdapter runner = new RemoteAnimationAdapter( 824 SCREENSHOT_REMOTE_RUNNER, 0, 0); 825 try { 826 WindowManagerGlobal.getWindowManagerService() 827 .overridePendingAppTransitionRemote(runner, 828 mDisplayTracker.getDefaultDisplayId()); 829 } catch (Exception e) { 830 Log.e(TAG, "Error overriding screenshot app transition", e); 831 } 832 }, mMainExecutor); 833 } 834 withWindowAttached(Runnable action)835 private void withWindowAttached(Runnable action) { 836 View decorView = mWindow.getDecorView(); 837 if (decorView.isAttachedToWindow()) { 838 action.run(); 839 } else { 840 decorView.getViewTreeObserver().addOnWindowAttachListener( 841 new ViewTreeObserver.OnWindowAttachListener() { 842 @Override 843 public void onWindowAttached() { 844 mBlockAttach = false; 845 decorView.getViewTreeObserver().removeOnWindowAttachListener(this); 846 action.run(); 847 } 848 849 @Override 850 public void onWindowDetached() { 851 } 852 }); 853 854 } 855 } 856 setContentView(View contentView)857 private void setContentView(View contentView) { 858 mWindow.setContentView(contentView); 859 } 860 861 @MainThread attachWindow()862 private void attachWindow() { 863 View decorView = mWindow.getDecorView(); 864 if (decorView.isAttachedToWindow() || mBlockAttach) { 865 return; 866 } 867 if (DEBUG_WINDOW) { 868 Log.d(TAG, "attachWindow"); 869 } 870 mBlockAttach = true; 871 mWindowManager.addView(decorView, mWindowLayoutParams); 872 decorView.requestApplyInsets(); 873 } 874 removeWindow()875 void removeWindow() { 876 final View decorView = mWindow.peekDecorView(); 877 if (decorView != null && decorView.isAttachedToWindow()) { 878 if (DEBUG_WINDOW) { 879 Log.d(TAG, "Removing screenshot window"); 880 } 881 mWindowManager.removeViewImmediate(decorView); 882 } 883 // Ensure that we remove the input monitor 884 if (mScreenshotView != null) { 885 mScreenshotView.stopInputListening(); 886 } 887 } 888 loadCameraSound()889 private ListenableFuture<MediaPlayer> loadCameraSound() { 890 // The media player creation is slow and needs on the background thread. 891 return CallbackToFutureAdapter.getFuture((completer) -> { 892 mBgExecutor.execute(() -> { 893 try { 894 MediaPlayer player = MediaPlayer.create(mContext, 895 Uri.fromFile(new File(mContext.getResources().getString( 896 com.android.internal.R.string.config_cameraShutterSound))), 897 null, 898 new AudioAttributes.Builder() 899 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) 900 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 901 .build(), AudioSystem.newAudioSessionId()); 902 completer.set(player); 903 } catch (IllegalStateException e) { 904 Log.w(TAG, "Screenshot sound initialization failed", e); 905 completer.set(null); 906 } 907 }); 908 return "ScreenshotController#loadCameraSound"; 909 }); 910 } 911 912 private void playCameraSound() { 913 mCameraSound.addListener(() -> { 914 try { 915 MediaPlayer player = mCameraSound.get(); 916 if (player != null) { 917 player.start(); 918 } 919 } catch (InterruptedException | ExecutionException e) { 920 } 921 }, mBgExecutor); 922 } 923 924 /** 925 * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on 926 * failure). 927 */ 928 private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) { 929 // Play the shutter sound to notify that we've taken a screenshot 930 playCameraSound(); 931 932 saveScreenshotInWorkerThread( 933 owner, 934 /* onComplete */ finisher, 935 /* actionsReadyListener */ imageData -> { 936 if (DEBUG_CALLBACK) { 937 Log.d(TAG, "returning URI to finisher (Consumer<URI>): " + imageData.uri); 938 } 939 finisher.accept(imageData.uri); 940 if (imageData.uri == null) { 941 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName); 942 mNotificationsController.notifyScreenshotError( 943 R.string.screenshot_failed_to_save_text); 944 } else { 945 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName); 946 mScreenshotHandler.post(() -> Toast.makeText(mContext, 947 R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); 948 } 949 }, 950 null); 951 } 952 953 /** 954 * Starts the animation after taking the screenshot 955 */ 956 private void startAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete) { 957 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 958 mScreenshotAnimation.cancel(); 959 } 960 961 mScreenshotAnimation = 962 mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash); 963 if (onAnimationComplete != null) { 964 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 965 @Override 966 public void onAnimationEnd(Animator animation) { 967 super.onAnimationEnd(animation); 968 onAnimationComplete.run(); 969 } 970 }); 971 } 972 973 // Play the shutter sound to notify that we've taken a screenshot 974 playCameraSound(); 975 976 if (DEBUG_ANIM) { 977 Log.d(TAG, "starting post-screenshot animation"); 978 } 979 mScreenshotAnimation.start(); 980 } 981 982 /** Reset screenshot view and then call onCompleteRunnable */ 983 private void finishDismiss() { 984 Log.d(TAG, "finishDismiss"); 985 if (mLastScrollCaptureRequest != null) { 986 mLastScrollCaptureRequest.cancel(true); 987 mLastScrollCaptureRequest = null; 988 } 989 if (mLastScrollCaptureResponse != null) { 990 mLastScrollCaptureResponse.close(); 991 mLastScrollCaptureResponse = null; 992 } 993 if (mLongScreenshotFuture != null) { 994 mLongScreenshotFuture.cancel(true); 995 } 996 if (mCurrentRequestCallback != null) { 997 mCurrentRequestCallback.onFinish(); 998 mCurrentRequestCallback = null; 999 } 1000 mScreenshotView.reset(); 1001 removeWindow(); 1002 mScreenshotHandler.cancelTimeout(); 1003 } 1004 1005 /** 1006 * Creates a new worker thread and saves the screenshot to the media store. 1007 */ 1008 private void saveScreenshotInWorkerThread( 1009 UserHandle owner, 1010 @NonNull Consumer<Uri> finisher, 1011 @Nullable ActionsReadyListener actionsReadyListener, 1012 @Nullable QuickShareActionReadyListener 1013 quickShareActionsReadyListener) { 1014 ScreenshotController.SaveImageInBackgroundData 1015 data = new ScreenshotController.SaveImageInBackgroundData(); 1016 data.image = mScreenBitmap; 1017 data.finisher = finisher; 1018 data.mActionsReadyListener = actionsReadyListener; 1019 data.mQuickShareActionsReadyListener = quickShareActionsReadyListener; 1020 data.owner = owner; 1021 1022 if (mSaveInBgTask != null) { 1023 // just log success/failure for the pre-existing screenshot 1024 mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); 1025 } 1026 1027 mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter, 1028 mScreenshotSmartActions, data, getActionTransitionSupplier(), 1029 mScreenshotNotificationSmartActionsProvider); 1030 mSaveInBgTask.execute(); 1031 } 1032 1033 1034 /** 1035 * Sets up the action shade and its entrance animation, once we get the screenshot URI. 1036 */ 1037 private void showUiOnActionsReady(ScreenshotController.SavedImageData imageData) { 1038 logSuccessOnActionsReady(imageData); 1039 if (DEBUG_UI) { 1040 Log.d(TAG, "Showing UI actions"); 1041 } 1042 1043 mScreenshotHandler.resetTimeout(); 1044 1045 if (imageData.uri != null) { 1046 if (!imageData.owner.equals(Process.myUserHandle())) { 1047 Log.d(TAG, "Screenshot saved to user " + imageData.owner + " as " 1048 + imageData.uri); 1049 } 1050 mScreenshotHandler.post(() -> { 1051 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 1052 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 1053 @Override 1054 public void onAnimationEnd(Animator animation) { 1055 super.onAnimationEnd(animation); 1056 doPostAnimation(imageData); 1057 } 1058 }); 1059 } else { 1060 doPostAnimation(imageData); 1061 } 1062 }); 1063 } 1064 } 1065 1066 private void doPostAnimation(ScreenshotController.SavedImageData imageData) { 1067 mScreenshotView.setChipIntents(imageData); 1068 } 1069 1070 /** 1071 * Sets up the action shade and its entrance animation, once we get the Quick Share action data. 1072 */ 1073 private void showUiOnQuickShareActionReady(ScreenshotController.QuickShareData quickShareData) { 1074 if (DEBUG_UI) { 1075 Log.d(TAG, "Showing UI for Quick Share action"); 1076 } 1077 if (quickShareData.quickShareAction != null) { 1078 mScreenshotHandler.post(() -> { 1079 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 1080 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 1081 @Override 1082 public void onAnimationEnd(Animator animation) { 1083 super.onAnimationEnd(animation); 1084 mScreenshotView.addQuickShareChip(quickShareData.quickShareAction); 1085 } 1086 }); 1087 } else { 1088 mScreenshotView.addQuickShareChip(quickShareData.quickShareAction); 1089 } 1090 }); 1091 } 1092 } 1093 1094 /** 1095 * Supplies the necessary bits for the shared element transition to share sheet. 1096 * Note that once supplied, the action intent to share must be sent immediately after. 1097 */ 1098 private Supplier<ActionTransition> getActionTransitionSupplier() { 1099 return () -> { 1100 Pair<ActivityOptions, ExitTransitionCoordinator> transition = 1101 ActivityOptions.startSharedElementAnimation( 1102 mWindow, new ScreenshotExitTransitionCallbacksSupplier(true).get(), 1103 null, Pair.create(mScreenshotView.getScreenshotPreview(), 1104 ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)); 1105 transition.second.startExit(); 1106 1107 ActionTransition supply = new ActionTransition(); 1108 supply.bundle = transition.first.toBundle(); 1109 supply.onCancelRunnable = () -> ActivityOptions.stopSharedElementAnimation(mWindow); 1110 return supply; 1111 }; 1112 } 1113 1114 /** 1115 * Logs success/failure of the screenshot saving task, and shows an error if it failed. 1116 */ 1117 private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) { 1118 if (imageData.uri == null) { 1119 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName); 1120 mNotificationsController.notifyScreenshotError( 1121 R.string.screenshot_failed_to_save_text); 1122 } else { 1123 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName); 1124 if (mUserManager.isManagedProfile(imageData.owner.getIdentifier())) { 1125 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE, 0, 1126 mPackageName); 1127 } 1128 } 1129 } 1130 1131 private boolean isUserSetupComplete(UserHandle owner) { 1132 return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0) 1133 .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; 1134 } 1135 1136 /** 1137 * Updates the window focusability. If the window is already showing, then it updates the 1138 * window immediately, otherwise the layout params will be applied when the window is next 1139 * shown. 1140 */ 1141 private void setWindowFocusable(boolean focusable) { 1142 if (DEBUG_WINDOW) { 1143 Log.d(TAG, "setWindowFocusable: " + focusable); 1144 } 1145 int flags = mWindowLayoutParams.flags; 1146 if (focusable) { 1147 mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 1148 } else { 1149 mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 1150 } 1151 if (mWindowLayoutParams.flags == flags) { 1152 if (DEBUG_WINDOW) { 1153 Log.d(TAG, "setWindowFocusable: skipping, already " + focusable); 1154 } 1155 return; 1156 } 1157 final View decorView = mWindow.peekDecorView(); 1158 if (decorView != null && decorView.isAttachedToWindow()) { 1159 mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); 1160 } 1161 } 1162 1163 private Display getDefaultDisplay() { 1164 return mDisplayManager.getDisplay(mDisplayTracker.getDefaultDisplayId()); 1165 } 1166 1167 private boolean allowLongScreenshots() { 1168 return !mIsLowRamDevice; 1169 } 1170 1171 private Rect getFullScreenRect() { 1172 DisplayMetrics displayMetrics = new DisplayMetrics(); 1173 getDefaultDisplay().getRealMetrics(displayMetrics); 1174 return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels); 1175 } 1176 1177 /** Does the aspect ratio of the bitmap with insets removed match the bounds. */ 1178 private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, 1179 Rect screenBounds) { 1180 int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right; 1181 int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom; 1182 1183 if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 1184 || bitmap.getHeight() == 0) { 1185 if (DEBUG_UI) { 1186 Log.e(TAG, "Provided bitmap and insets create degenerate region: " 1187 + bitmap.getWidth() + "x" + bitmap.getHeight() + " " + bitmapInsets); 1188 } 1189 return false; 1190 } 1191 1192 float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight; 1193 float boundsAspect = ((float) screenBounds.width()) / screenBounds.height(); 1194 1195 boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f; 1196 if (DEBUG_UI) { 1197 Log.d(TAG, "aspectRatiosMatch: don't match bitmap: " + insettedBitmapAspect 1198 + ", bounds: " + boundsAspect); 1199 } 1200 return matchWithinTolerance; 1201 } 1202 1203 private class ScreenshotExitTransitionCallbacksSupplier implements 1204 Supplier<ExitTransitionCallbacks> { 1205 final boolean mDismissOnHideSharedElements; 1206 1207 ScreenshotExitTransitionCallbacksSupplier(boolean dismissOnHideSharedElements) { 1208 mDismissOnHideSharedElements = dismissOnHideSharedElements; 1209 } 1210 1211 @Override 1212 public ExitTransitionCallbacks get() { 1213 return new ExitTransitionCallbacks() { 1214 @Override 1215 public boolean isReturnTransitionAllowed() { 1216 return false; 1217 } 1218 1219 @Override 1220 public void hideSharedElements() { 1221 if (mDismissOnHideSharedElements) { 1222 finishDismiss(); 1223 } 1224 } 1225 1226 @Override 1227 public void onFinish() { 1228 } 1229 }; 1230 } 1231 } 1232 } 1233