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.Display.DEFAULT_DISPLAY; 21 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 22 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; 23 24 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; 25 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; 26 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS; 27 import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; 28 import static com.android.systemui.screenshot.LogConfig.DEBUG_UI; 29 import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW; 30 import static com.android.systemui.screenshot.LogConfig.logTag; 31 32 import static java.util.Objects.requireNonNull; 33 34 import android.animation.Animator; 35 import android.animation.AnimatorListenerAdapter; 36 import android.annotation.Nullable; 37 import android.app.ActivityManager; 38 import android.app.ActivityOptions; 39 import android.app.ExitTransitionCoordinator; 40 import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks; 41 import android.app.Notification; 42 import android.content.ComponentName; 43 import android.content.Context; 44 import android.content.Intent; 45 import android.content.pm.ActivityInfo; 46 import android.graphics.Bitmap; 47 import android.graphics.Insets; 48 import android.graphics.PixelFormat; 49 import android.graphics.Rect; 50 import android.hardware.display.DisplayManager; 51 import android.media.MediaActionSound; 52 import android.net.Uri; 53 import android.os.Bundle; 54 import android.os.Handler; 55 import android.os.IBinder; 56 import android.os.Looper; 57 import android.os.Message; 58 import android.os.RemoteException; 59 import android.provider.Settings; 60 import android.util.DisplayMetrics; 61 import android.util.Log; 62 import android.util.Pair; 63 import android.view.Display; 64 import android.view.DisplayAddress; 65 import android.view.IRemoteAnimationFinishedCallback; 66 import android.view.IRemoteAnimationRunner; 67 import android.view.KeyEvent; 68 import android.view.LayoutInflater; 69 import android.view.RemoteAnimationAdapter; 70 import android.view.RemoteAnimationTarget; 71 import android.view.ScrollCaptureResponse; 72 import android.view.SurfaceControl; 73 import android.view.View; 74 import android.view.ViewTreeObserver; 75 import android.view.Window; 76 import android.view.WindowInsets; 77 import android.view.WindowManager; 78 import android.view.WindowManagerGlobal; 79 import android.view.accessibility.AccessibilityEvent; 80 import android.view.accessibility.AccessibilityManager; 81 import android.widget.Toast; 82 import android.window.WindowContext; 83 84 import com.android.internal.app.ChooserActivity; 85 import com.android.internal.logging.UiEventLogger; 86 import com.android.internal.policy.PhoneWindow; 87 import com.android.settingslib.applications.InterestingConfigChanges; 88 import com.android.systemui.R; 89 import com.android.systemui.dagger.qualifiers.Main; 90 import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition; 91 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback; 92 93 import com.google.common.util.concurrent.ListenableFuture; 94 95 import java.util.List; 96 import java.util.concurrent.CancellationException; 97 import java.util.concurrent.ExecutionException; 98 import java.util.concurrent.Executor; 99 import java.util.concurrent.ExecutorService; 100 import java.util.concurrent.Executors; 101 import java.util.concurrent.Future; 102 import java.util.function.Consumer; 103 import java.util.function.Supplier; 104 105 import javax.inject.Inject; 106 107 /** 108 * Controls the state and flow for screenshots. 109 */ 110 public class ScreenshotController { 111 private static final String TAG = logTag(ScreenshotController.class); 112 113 private ScrollCaptureResponse mLastScrollCaptureResponse; 114 private ListenableFuture<ScrollCaptureResponse> mLastScrollCaptureRequest; 115 116 /** 117 * This is effectively a no-op, but we need something non-null to pass in, in order to 118 * successfully override the pending activity entrance animation. 119 */ 120 static final IRemoteAnimationRunner.Stub SCREENSHOT_REMOTE_RUNNER = 121 new IRemoteAnimationRunner.Stub() { 122 @Override 123 public void onAnimationStart( 124 @WindowManager.TransitionOldType int transit, 125 RemoteAnimationTarget[] apps, 126 RemoteAnimationTarget[] wallpapers, 127 RemoteAnimationTarget[] nonApps, 128 final IRemoteAnimationFinishedCallback finishedCallback) { 129 try { 130 finishedCallback.onAnimationFinished(); 131 } catch (RemoteException e) { 132 Log.e(TAG, "Error finishing screenshot remote animation", e); 133 } 134 } 135 136 @Override 137 public void onAnimationCancelled() { 138 } 139 }; 140 141 /** 142 * POD used in the AsyncTask which saves an image in the background. 143 */ 144 static class SaveImageInBackgroundData { 145 public Bitmap image; 146 public Consumer<Uri> finisher; 147 public ScreenshotController.ActionsReadyListener mActionsReadyListener; 148 public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener; 149 clearImage()150 void clearImage() { 151 image = null; 152 } 153 } 154 155 /** 156 * Structure returned by the SaveImageInBackgroundTask 157 */ 158 static class SavedImageData { 159 public Uri uri; 160 public Supplier<ActionTransition> shareTransition; 161 public Supplier<ActionTransition> editTransition; 162 public Notification.Action deleteAction; 163 public List<Notification.Action> smartActions; 164 public Notification.Action quickShareAction; 165 166 /** 167 * POD for shared element transition. 168 */ 169 static class ActionTransition { 170 public Bundle bundle; 171 public Notification.Action action; 172 public Runnable onCancelRunnable; 173 } 174 175 /** 176 * Used to reset the return data on error 177 */ reset()178 public void reset() { 179 uri = null; 180 shareTransition = null; 181 editTransition = null; 182 deleteAction = null; 183 smartActions = null; 184 quickShareAction = null; 185 } 186 } 187 188 /** 189 * Structure returned by the QueryQuickShareInBackgroundTask 190 */ 191 static class QuickShareData { 192 public Notification.Action quickShareAction; 193 194 /** 195 * Used to reset the return data on error 196 */ reset()197 public void reset() { 198 quickShareAction = null; 199 } 200 } 201 202 interface ActionsReadyListener { onActionsReady(ScreenshotController.SavedImageData imageData)203 void onActionsReady(ScreenshotController.SavedImageData imageData); 204 } 205 206 interface QuickShareActionReadyListener { onActionsReady(ScreenshotController.QuickShareData quickShareData)207 void onActionsReady(ScreenshotController.QuickShareData quickShareData); 208 } 209 210 interface TransitionDestination { 211 /** 212 * Allows the long screenshot activity to call back with a destination location (the bounds 213 * on screen of the destination for the transitioning view) and a Runnable to be run once 214 * the transition animation is complete. 215 */ setTransitionDestination(Rect transitionDestination, Runnable onTransitionEnd)216 void setTransitionDestination(Rect transitionDestination, Runnable onTransitionEnd); 217 } 218 219 // These strings are used for communicating the action invoked to 220 // ScreenshotNotificationSmartActionsProvider. 221 static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type"; 222 static final String EXTRA_ID = "android:screenshot_id"; 223 static final String ACTION_TYPE_DELETE = "Delete"; 224 static final String ACTION_TYPE_SHARE = "Share"; 225 static final String ACTION_TYPE_EDIT = "Edit"; 226 static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled"; 227 static final String EXTRA_OVERRIDE_TRANSITION = "android:screenshot_override_transition"; 228 static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent"; 229 230 static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id"; 231 static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification"; 232 static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip"; 233 234 235 private static final int MESSAGE_CORNER_TIMEOUT = 2; 236 private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; 237 238 // From WizardManagerHelper.java 239 private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; 240 241 private final WindowContext mContext; 242 private final ScreenshotNotificationsController mNotificationsController; 243 private final ScreenshotSmartActions mScreenshotSmartActions; 244 private final UiEventLogger mUiEventLogger; 245 private final ImageExporter mImageExporter; 246 private final Executor mMainExecutor; 247 private final ExecutorService mBgExecutor; 248 249 private final WindowManager mWindowManager; 250 private final WindowManager.LayoutParams mWindowLayoutParams; 251 private final AccessibilityManager mAccessibilityManager; 252 private final MediaActionSound mCameraSound; 253 private final ScrollCaptureClient mScrollCaptureClient; 254 private final PhoneWindow mWindow; 255 private final DisplayManager mDisplayManager; 256 private final ScrollCaptureController mScrollCaptureController; 257 private final LongScreenshotData mLongScreenshotHolder; 258 private final boolean mIsLowRamDevice; 259 260 private ScreenshotView mScreenshotView; 261 private Bitmap mScreenBitmap; 262 private SaveImageInBackgroundTask mSaveInBgTask; 263 private boolean mScreenshotTakenInPortrait; 264 265 private Animator mScreenshotAnimation; 266 private RequestCallback mCurrentRequestCallback; 267 268 private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) { 269 @Override 270 public void handleMessage(Message msg) { 271 switch (msg.what) { 272 case MESSAGE_CORNER_TIMEOUT: 273 if (DEBUG_UI) { 274 Log.d(TAG, "Corner timeout hit"); 275 } 276 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT); 277 ScreenshotController.this.dismissScreenshot(false); 278 break; 279 default: 280 break; 281 } 282 } 283 }; 284 285 /** Tracks config changes that require re-creating UI */ 286 private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges( 287 ActivityInfo.CONFIG_ORIENTATION 288 | ActivityInfo.CONFIG_LAYOUT_DIRECTION 289 | ActivityInfo.CONFIG_LOCALE 290 | ActivityInfo.CONFIG_UI_MODE 291 | ActivityInfo.CONFIG_SCREEN_LAYOUT 292 | ActivityInfo.CONFIG_ASSETS_PATHS); 293 294 @Inject ScreenshotController( Context context, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController screenshotNotificationsController, ScrollCaptureClient scrollCaptureClient, UiEventLogger uiEventLogger, ImageExporter imageExporter, @Main Executor mainExecutor, ScrollCaptureController scrollCaptureController, LongScreenshotData longScreenshotHolder, ActivityManager activityManager)295 ScreenshotController( 296 Context context, 297 ScreenshotSmartActions screenshotSmartActions, 298 ScreenshotNotificationsController screenshotNotificationsController, 299 ScrollCaptureClient scrollCaptureClient, 300 UiEventLogger uiEventLogger, 301 ImageExporter imageExporter, 302 @Main Executor mainExecutor, 303 ScrollCaptureController scrollCaptureController, 304 LongScreenshotData longScreenshotHolder, 305 ActivityManager activityManager) { 306 mScreenshotSmartActions = screenshotSmartActions; 307 mNotificationsController = screenshotNotificationsController; 308 mScrollCaptureClient = scrollCaptureClient; 309 mUiEventLogger = uiEventLogger; 310 mImageExporter = imageExporter; 311 mMainExecutor = mainExecutor; 312 mScrollCaptureController = scrollCaptureController; 313 mLongScreenshotHolder = longScreenshotHolder; 314 mIsLowRamDevice = activityManager.isLowRamDevice(); 315 mBgExecutor = Executors.newSingleThreadExecutor(); 316 317 mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class)); 318 final Context displayContext = context.createDisplayContext(getDefaultDisplay()); 319 mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); 320 mWindowManager = mContext.getSystemService(WindowManager.class); 321 322 mAccessibilityManager = AccessibilityManager.getInstance(mContext); 323 324 // Setup the window that we are going to use 325 mWindowLayoutParams = new WindowManager.LayoutParams( 326 MATCH_PARENT, MATCH_PARENT, /* xpos */ 0, /* ypos */ 0, TYPE_SCREENSHOT, 327 WindowManager.LayoutParams.FLAG_FULLSCREEN 328 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN 329 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 330 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH 331 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 332 | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, 333 PixelFormat.TRANSLUCENT); 334 mWindowLayoutParams.setTitle("ScreenshotAnimation"); 335 mWindowLayoutParams.layoutInDisplayCutoutMode = 336 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 337 mWindowLayoutParams.setFitInsetsTypes(0); 338 // This is needed to let touches pass through outside the touchable areas 339 mWindowLayoutParams.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; 340 341 mWindow = new PhoneWindow(mContext); 342 mWindow.setWindowManager(mWindowManager, null, null); 343 mWindow.requestFeature(Window.FEATURE_NO_TITLE); 344 mWindow.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS); 345 mWindow.setBackgroundDrawableResource(android.R.color.transparent); 346 347 mConfigChanges.applyNewConfig(context.getResources()); 348 reloadAssets(); 349 350 // Setup the Camera shutter sound 351 mCameraSound = new MediaActionSound(); 352 mCameraSound.load(MediaActionSound.SHUTTER_CLICK); 353 } 354 takeScreenshotFullscreen(Consumer<Uri> finisher, RequestCallback requestCallback)355 void takeScreenshotFullscreen(Consumer<Uri> finisher, RequestCallback requestCallback) { 356 mCurrentRequestCallback = requestCallback; 357 DisplayMetrics displayMetrics = new DisplayMetrics(); 358 getDefaultDisplay().getRealMetrics(displayMetrics); 359 takeScreenshotInternal( 360 finisher, 361 new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)); 362 } 363 handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, Insets visibleInsets, int taskId, int userId, ComponentName topComponent, Consumer<Uri> finisher, RequestCallback requestCallback)364 void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, 365 Insets visibleInsets, int taskId, int userId, ComponentName topComponent, 366 Consumer<Uri> finisher, RequestCallback requestCallback) { 367 // TODO: use task Id, userId, topComponent for smart handler 368 369 if (screenshot == null) { 370 Log.e(TAG, "Got null bitmap from screenshot message"); 371 mNotificationsController.notifyScreenshotError( 372 R.string.screenshot_failed_to_capture_text); 373 requestCallback.reportError(); 374 return; 375 } 376 377 boolean showFlash = false; 378 if (!aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) { 379 showFlash = true; 380 visibleInsets = Insets.NONE; 381 screenshotScreenBounds.set(0, 0, screenshot.getWidth(), screenshot.getHeight()); 382 } 383 mCurrentRequestCallback = requestCallback; 384 saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, showFlash); 385 } 386 387 /** 388 * Displays a screenshot selector 389 */ takeScreenshotPartial(final Consumer<Uri> finisher, RequestCallback requestCallback)390 void takeScreenshotPartial(final Consumer<Uri> finisher, RequestCallback requestCallback) { 391 mScreenshotView.reset(); 392 mCurrentRequestCallback = requestCallback; 393 394 attachWindow(); 395 mWindow.setContentView(mScreenshotView); 396 mScreenshotView.requestApplyInsets(); 397 398 mScreenshotView.takePartialScreenshot( 399 rect -> takeScreenshotInternal(finisher, rect)); 400 } 401 402 /** 403 * Clears current screenshot 404 */ dismissScreenshot(boolean immediate)405 void dismissScreenshot(boolean immediate) { 406 if (DEBUG_DISMISS) { 407 Log.d(TAG, "dismissScreenshot(immediate=" + immediate + ")"); 408 } 409 // If we're already animating out, don't restart the animation 410 // (but do obey an immediate dismissal) 411 if (!immediate && mScreenshotView.isDismissing()) { 412 if (DEBUG_DISMISS) { 413 Log.v(TAG, "Already dismissing, ignoring duplicate command"); 414 } 415 return; 416 } 417 cancelTimeout(); 418 if (immediate) { 419 finishDismiss(); 420 } else { 421 mScreenshotView.animateDismissal(); 422 } 423 424 if (mLastScrollCaptureResponse != null) { 425 mLastScrollCaptureResponse.close(); 426 mLastScrollCaptureResponse = null; 427 } 428 } 429 isPendingSharedTransition()430 boolean isPendingSharedTransition() { 431 return mScreenshotView.isPendingSharedTransition(); 432 } 433 434 /** 435 * Release the constructed window context. 436 */ releaseContext()437 void releaseContext() { 438 mContext.release(); 439 mCameraSound.release(); 440 mBgExecutor.shutdownNow(); 441 } 442 443 /** 444 * Update resources on configuration change. Reinflate for theme/color changes. 445 */ reloadAssets()446 private void reloadAssets() { 447 if (DEBUG_UI) { 448 Log.d(TAG, "reloadAssets()"); 449 } 450 451 // Inflate the screenshot layout 452 mScreenshotView = (ScreenshotView) 453 LayoutInflater.from(mContext).inflate(R.layout.global_screenshot, null); 454 mScreenshotView.init(mUiEventLogger, new ScreenshotView.ScreenshotViewCallback() { 455 @Override 456 public void onUserInteraction() { 457 resetTimeout(); 458 } 459 460 @Override 461 public void onDismiss() { 462 finishDismiss(); 463 } 464 465 @Override 466 public void onTouchOutside() { 467 // TODO(159460485): Remove this when focus is handled properly in the system 468 setWindowFocusable(false); 469 } 470 }); 471 472 mScreenshotView.setOnKeyListener((v, keyCode, event) -> { 473 if (keyCode == KeyEvent.KEYCODE_BACK) { 474 if (DEBUG_INPUT) { 475 Log.d(TAG, "onKeyEvent: KeyEvent.KEYCODE_BACK"); 476 } 477 dismissScreenshot(false); 478 return true; 479 } 480 return false; 481 }); 482 483 if (DEBUG_WINDOW) { 484 Log.d(TAG, "adding OnComputeInternalInsetsListener"); 485 } 486 mScreenshotView.getViewTreeObserver().addOnComputeInternalInsetsListener(mScreenshotView); 487 } 488 489 /** 490 * Takes a screenshot of the current display and shows an animation. 491 */ takeScreenshotInternal(Consumer<Uri> finisher, Rect crop)492 private void takeScreenshotInternal(Consumer<Uri> finisher, Rect crop) { 493 mScreenshotTakenInPortrait = 494 mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; 495 496 // copy the input Rect, since SurfaceControl.screenshot can mutate it 497 Rect screenRect = new Rect(crop); 498 Bitmap screenshot = captureScreenshot(crop); 499 500 if (screenshot == null) { 501 Log.e(TAG, "takeScreenshotInternal: Screenshot bitmap was null"); 502 mNotificationsController.notifyScreenshotError( 503 R.string.screenshot_failed_to_capture_text); 504 if (mCurrentRequestCallback != null) { 505 mCurrentRequestCallback.reportError(); 506 } 507 return; 508 } 509 510 saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, true); 511 } 512 captureScreenshot(Rect crop)513 private Bitmap captureScreenshot(Rect crop) { 514 int width = crop.width(); 515 int height = crop.height(); 516 Bitmap screenshot = null; 517 final Display display = getDefaultDisplay(); 518 final DisplayAddress address = display.getAddress(); 519 if (!(address instanceof DisplayAddress.Physical)) { 520 Log.e(TAG, "Skipping Screenshot - Default display does not have a physical address: " 521 + display); 522 } else { 523 final DisplayAddress.Physical physicalAddress = (DisplayAddress.Physical) address; 524 525 final IBinder displayToken = SurfaceControl.getPhysicalDisplayToken( 526 physicalAddress.getPhysicalDisplayId()); 527 final SurfaceControl.DisplayCaptureArgs captureArgs = 528 new SurfaceControl.DisplayCaptureArgs.Builder(displayToken) 529 .setSourceCrop(crop) 530 .setSize(width, height) 531 .build(); 532 final SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer = 533 SurfaceControl.captureDisplay(captureArgs); 534 screenshot = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); 535 } 536 return screenshot; 537 } 538 saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, boolean showFlash)539 private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, 540 Insets screenInsets, boolean showFlash) { 541 if (mAccessibilityManager.isEnabled()) { 542 AccessibilityEvent event = 543 new AccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 544 event.setContentDescription( 545 mContext.getResources().getString(R.string.screenshot_saving_title)); 546 mAccessibilityManager.sendAccessibilityEvent(event); 547 } 548 549 550 if (mScreenshotView.isAttachedToWindow()) { 551 // if we didn't already dismiss for another reason 552 if (!mScreenshotView.isDismissing()) { 553 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED); 554 } 555 if (DEBUG_WINDOW) { 556 Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. " 557 + "(dismissing=" + mScreenshotView.isDismissing() + ")"); 558 } 559 mScreenshotView.reset(); 560 } 561 562 mScreenshotView.updateOrientation(mWindowManager.getCurrentWindowMetrics() 563 .getWindowInsets().getDisplayCutout()); 564 565 mScreenBitmap = screenshot; 566 567 if (!isUserSetupComplete()) { 568 Log.w(TAG, "User setup not complete, displaying toast only"); 569 // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing 570 // and sharing shouldn't be exposed to the user. 571 saveScreenshotAndToast(finisher); 572 return; 573 } 574 575 // Optimizations 576 mScreenBitmap.setHasAlpha(false); 577 mScreenBitmap.prepareToDraw(); 578 579 saveScreenshotInWorkerThread(finisher, this::showUiOnActionsReady, 580 this::showUiOnQuickShareActionReady); 581 582 // The window is focusable by default 583 setWindowFocusable(true); 584 585 // Wait until this window is attached to request because it is 586 // the reference used to locate the target window (below). 587 withWindowAttached(() -> { 588 requestScrollCapture(); 589 mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( 590 (overrideConfig, newDisplayId) -> { 591 if (mConfigChanges.applyNewConfig(mContext.getResources())) { 592 // Hide the scroll chip until we know it's available in this orientation 593 mScreenshotView.hideScrollChip(); 594 // Delay scroll capture eval a bit to allow the underlying activity 595 // to set up in the new orientation. 596 mScreenshotHandler.postDelayed(this::requestScrollCapture, 150); 597 mScreenshotView.updateDisplayCutoutMargins( 598 mWindowManager.getCurrentWindowMetrics().getWindowInsets() 599 .getDisplayCutout()); 600 // screenshot animation calculations won't be valid anymore, so just end 601 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 602 mScreenshotAnimation.end(); 603 } 604 } 605 }); 606 }); 607 608 attachWindow(); 609 mScreenshotView.getViewTreeObserver().addOnPreDrawListener( 610 new ViewTreeObserver.OnPreDrawListener() { 611 @Override 612 public boolean onPreDraw() { 613 if (DEBUG_WINDOW) { 614 Log.d(TAG, "onPreDraw: startAnimation"); 615 } 616 mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this); 617 startAnimation(screenRect, showFlash); 618 return true; 619 } 620 }); 621 mScreenshotView.setScreenshot(mScreenBitmap, screenInsets); 622 if (DEBUG_WINDOW) { 623 Log.d(TAG, "setContentView: " + mScreenshotView); 624 } 625 setContentView(mScreenshotView); 626 // ignore system bar insets for the purpose of window layout 627 mWindow.getDecorView().setOnApplyWindowInsetsListener( 628 (v, insets) -> WindowInsets.CONSUMED); 629 cancelTimeout(); // restarted after animation 630 } 631 requestScrollCapture()632 private void requestScrollCapture() { 633 if (!allowLongScreenshots()) { 634 Log.d(TAG, "Long screenshots not supported on this device"); 635 return; 636 } 637 mScrollCaptureClient.setHostWindowToken(mWindow.getDecorView().getWindowToken()); 638 if (mLastScrollCaptureRequest != null) { 639 mLastScrollCaptureRequest.cancel(true); 640 } 641 mLastScrollCaptureRequest = mScrollCaptureClient.request(DEFAULT_DISPLAY); 642 mLastScrollCaptureRequest.addListener(() -> 643 onScrollCaptureResponseReady(mLastScrollCaptureRequest), mMainExecutor); 644 } 645 onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture)646 private void onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture) { 647 try { 648 if (mLastScrollCaptureResponse != null) { 649 mLastScrollCaptureResponse.close(); 650 } 651 mLastScrollCaptureResponse = responseFuture.get(); 652 if (!mLastScrollCaptureResponse.isConnected()) { 653 // No connection means that the target window wasn't found 654 // or that it cannot support scroll capture. 655 Log.d(TAG, "ScrollCapture: " + mLastScrollCaptureResponse.getDescription() + " [" 656 + mLastScrollCaptureResponse.getWindowTitle() + "]"); 657 return; 658 } 659 Log.d(TAG, "ScrollCapture: connected to window [" 660 + mLastScrollCaptureResponse.getWindowTitle() + "]"); 661 662 final ScrollCaptureResponse response = mLastScrollCaptureResponse; 663 mScreenshotView.showScrollChip(/* onClick */ () -> { 664 DisplayMetrics displayMetrics = new DisplayMetrics(); 665 getDefaultDisplay().getRealMetrics(displayMetrics); 666 Bitmap newScreenshot = captureScreenshot( 667 new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels)); 668 669 mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot, 670 mScreenshotTakenInPortrait); 671 // delay starting scroll capture to make sure the scrim is up before the app moves 672 mScreenshotView.post(() -> { 673 // Clear the reference to prevent close() in dismissScreenshot 674 mLastScrollCaptureResponse = null; 675 final ListenableFuture<ScrollCaptureController.LongScreenshot> future = 676 mScrollCaptureController.run(response); 677 future.addListener(() -> { 678 ScrollCaptureController.LongScreenshot longScreenshot; 679 680 try { 681 longScreenshot = future.get(); 682 } catch (CancellationException 683 | InterruptedException 684 | ExecutionException e) { 685 Log.e(TAG, "Exception", e); 686 mScreenshotView.restoreNonScrollingUi(); 687 return; 688 } 689 690 if (longScreenshot.getHeight() == 0) { 691 mScreenshotView.restoreNonScrollingUi(); 692 return; 693 } 694 695 mLongScreenshotHolder.setLongScreenshot(longScreenshot); 696 mLongScreenshotHolder.setTransitionDestinationCallback( 697 (transitionDestination, onTransitionEnd) -> 698 mScreenshotView.startLongScreenshotTransition( 699 transitionDestination, onTransitionEnd, 700 longScreenshot)); 701 702 final Intent intent = new Intent(mContext, LongScreenshotActivity.class); 703 intent.setFlags( 704 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); 705 706 mContext.startActivity(intent, 707 ActivityOptions.makeCustomAnimation(mContext, 0, 0).toBundle()); 708 RemoteAnimationAdapter runner = new RemoteAnimationAdapter( 709 SCREENSHOT_REMOTE_RUNNER, 0, 0); 710 try { 711 WindowManagerGlobal.getWindowManagerService() 712 .overridePendingAppTransitionRemote(runner, DEFAULT_DISPLAY); 713 } catch (Exception e) { 714 Log.e(TAG, "Error overriding screenshot app transition", e); 715 } 716 }, mMainExecutor); 717 }); 718 }); 719 } catch (CancellationException e) { 720 // Ignore 721 } catch (InterruptedException | ExecutionException e) { 722 Log.e(TAG, "requestScrollCapture failed", e); 723 } 724 } 725 withWindowAttached(Runnable action)726 private void withWindowAttached(Runnable action) { 727 View decorView = mWindow.getDecorView(); 728 if (decorView.isAttachedToWindow()) { 729 action.run(); 730 } else { 731 decorView.getViewTreeObserver().addOnWindowAttachListener( 732 new ViewTreeObserver.OnWindowAttachListener() { 733 @Override 734 public void onWindowAttached() { 735 decorView.getViewTreeObserver().removeOnWindowAttachListener(this); 736 action.run(); 737 } 738 739 @Override 740 public void onWindowDetached() { 741 } 742 }); 743 744 } 745 } 746 setContentView(View contentView)747 private void setContentView(View contentView) { 748 mWindow.setContentView(contentView); 749 } 750 attachWindow()751 private void attachWindow() { 752 View decorView = mWindow.getDecorView(); 753 if (decorView.isAttachedToWindow()) { 754 return; 755 } 756 if (DEBUG_WINDOW) { 757 Log.d(TAG, "attachWindow"); 758 } 759 mWindowManager.addView(decorView, mWindowLayoutParams); 760 decorView.requestApplyInsets(); 761 } 762 removeWindow()763 void removeWindow() { 764 final View decorView = mWindow.peekDecorView(); 765 if (decorView != null && decorView.isAttachedToWindow()) { 766 if (DEBUG_WINDOW) { 767 Log.d(TAG, "Removing screenshot window"); 768 } 769 mWindowManager.removeViewImmediate(decorView); 770 } 771 } 772 773 /** 774 * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on 775 * failure). 776 */ saveScreenshotAndToast(Consumer<Uri> finisher)777 private void saveScreenshotAndToast(Consumer<Uri> finisher) { 778 // Play the shutter sound to notify that we've taken a screenshot 779 mCameraSound.play(MediaActionSound.SHUTTER_CLICK); 780 781 saveScreenshotInWorkerThread( 782 /* onComplete */ finisher, 783 /* actionsReadyListener */ imageData -> { 784 if (DEBUG_CALLBACK) { 785 Log.d(TAG, "returning URI to finisher (Consumer<URI>): " + imageData.uri); 786 } 787 finisher.accept(imageData.uri); 788 if (imageData.uri == null) { 789 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); 790 mNotificationsController.notifyScreenshotError( 791 R.string.screenshot_failed_to_save_text); 792 } else { 793 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); 794 mScreenshotHandler.post(() -> Toast.makeText(mContext, 795 R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); 796 } 797 }, 798 null); 799 } 800 801 /** 802 * Starts the animation after taking the screenshot 803 */ startAnimation(Rect screenRect, boolean showFlash)804 private void startAnimation(Rect screenRect, boolean showFlash) { 805 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 806 mScreenshotAnimation.cancel(); 807 } 808 809 mScreenshotAnimation = 810 mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash); 811 812 // Play the shutter sound to notify that we've taken a screenshot 813 mCameraSound.play(MediaActionSound.SHUTTER_CLICK); 814 815 if (DEBUG_ANIM) { 816 Log.d(TAG, "starting post-screenshot animation"); 817 } 818 mScreenshotAnimation.start(); 819 } 820 821 /** Reset screenshot view and then call onCompleteRunnable */ finishDismiss()822 private void finishDismiss() { 823 if (DEBUG_UI) { 824 Log.d(TAG, "finishDismiss"); 825 } 826 cancelTimeout(); 827 removeWindow(); 828 mScreenshotView.reset(); 829 if (mCurrentRequestCallback != null) { 830 mCurrentRequestCallback.onFinish(); 831 mCurrentRequestCallback = null; 832 } 833 } 834 835 /** 836 * Creates a new worker thread and saves the screenshot to the media store. 837 */ saveScreenshotInWorkerThread(Consumer<Uri> finisher, @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener, @Nullable ScreenshotController.QuickShareActionReadyListener quickShareActionsReadyListener)838 private void saveScreenshotInWorkerThread(Consumer<Uri> finisher, 839 @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener, 840 @Nullable ScreenshotController.QuickShareActionReadyListener 841 quickShareActionsReadyListener) { 842 ScreenshotController.SaveImageInBackgroundData 843 data = new ScreenshotController.SaveImageInBackgroundData(); 844 data.image = mScreenBitmap; 845 data.finisher = finisher; 846 data.mActionsReadyListener = actionsReadyListener; 847 data.mQuickShareActionsReadyListener = quickShareActionsReadyListener; 848 849 if (mSaveInBgTask != null) { 850 // just log success/failure for the pre-existing screenshot 851 mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); 852 } 853 854 mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter, 855 mScreenshotSmartActions, data, getActionTransitionSupplier()); 856 mSaveInBgTask.execute(); 857 } 858 cancelTimeout()859 private void cancelTimeout() { 860 mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT); 861 } 862 resetTimeout()863 private void resetTimeout() { 864 cancelTimeout(); 865 866 AccessibilityManager accessibilityManager = (AccessibilityManager) 867 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 868 long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis( 869 SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS, 870 AccessibilityManager.FLAG_CONTENT_CONTROLS); 871 872 mScreenshotHandler.sendMessageDelayed( 873 mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT), 874 timeoutMs); 875 if (DEBUG_UI) { 876 Log.d(TAG, "dismiss timeout: " + timeoutMs + " ms"); 877 } 878 879 } 880 881 /** 882 * Sets up the action shade and its entrance animation, once we get the screenshot URI. 883 */ showUiOnActionsReady(ScreenshotController.SavedImageData imageData)884 private void showUiOnActionsReady(ScreenshotController.SavedImageData imageData) { 885 logSuccessOnActionsReady(imageData); 886 if (DEBUG_UI) { 887 Log.d(TAG, "Showing UI actions"); 888 } 889 890 resetTimeout(); 891 892 if (imageData.uri != null) { 893 mScreenshotHandler.post(() -> { 894 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 895 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 896 @Override 897 public void onAnimationEnd(Animator animation) { 898 super.onAnimationEnd(animation); 899 mScreenshotView.setChipIntents(imageData); 900 } 901 }); 902 } else { 903 mScreenshotView.setChipIntents(imageData); 904 } 905 }); 906 } 907 } 908 909 /** 910 * Sets up the action shade and its entrance animation, once we get the Quick Share action data. 911 */ showUiOnQuickShareActionReady(ScreenshotController.QuickShareData quickShareData)912 private void showUiOnQuickShareActionReady(ScreenshotController.QuickShareData quickShareData) { 913 if (DEBUG_UI) { 914 Log.d(TAG, "Showing UI for Quick Share action"); 915 } 916 if (quickShareData.quickShareAction != null) { 917 mScreenshotHandler.post(() -> { 918 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 919 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 920 @Override 921 public void onAnimationEnd(Animator animation) { 922 super.onAnimationEnd(animation); 923 mScreenshotView.addQuickShareChip(quickShareData.quickShareAction); 924 } 925 }); 926 } else { 927 mScreenshotView.addQuickShareChip(quickShareData.quickShareAction); 928 } 929 }); 930 } 931 } 932 933 /** 934 * Supplies the necessary bits for the shared element transition to share sheet. 935 * Note that once supplied, the action intent to share must be sent immediately after. 936 */ getActionTransitionSupplier()937 private Supplier<ActionTransition> getActionTransitionSupplier() { 938 return () -> { 939 View preview = mScreenshotView.getTransitionView(); 940 preview.setX(preview.getX() - mScreenshotView.getStaticLeftMargin()); 941 Pair<ActivityOptions, ExitTransitionCoordinator> transition = 942 ActivityOptions.startSharedElementAnimation( 943 mWindow, new ScreenshotExitTransitionCallbacksSupplier(true).get(), 944 null, Pair.create(mScreenshotView.getTransitionView(), 945 ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)); 946 transition.second.startExit(); 947 948 ActionTransition supply = new ActionTransition(); 949 supply.bundle = transition.first.toBundle(); 950 supply.onCancelRunnable = () -> ActivityOptions.stopSharedElementAnimation(mWindow); 951 return supply; 952 }; 953 } 954 955 /** 956 * Logs success/failure of the screenshot saving task, and shows an error if it failed. 957 */ logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData)958 private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) { 959 if (imageData.uri == null) { 960 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED); 961 mNotificationsController.notifyScreenshotError( 962 R.string.screenshot_failed_to_save_text); 963 } else { 964 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED); 965 } 966 } 967 isUserSetupComplete()968 private boolean isUserSetupComplete() { 969 return Settings.Secure.getInt(mContext.getContentResolver(), 970 SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; 971 } 972 973 /** 974 * Updates the window focusability. If the window is already showing, then it updates the 975 * window immediately, otherwise the layout params will be applied when the window is next 976 * shown. 977 */ setWindowFocusable(boolean focusable)978 private void setWindowFocusable(boolean focusable) { 979 if (DEBUG_WINDOW) { 980 Log.d(TAG, "setWindowFocusable: " + focusable); 981 } 982 int flags = mWindowLayoutParams.flags; 983 if (focusable) { 984 mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 985 } else { 986 mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 987 } 988 if (mWindowLayoutParams.flags == flags) { 989 if (DEBUG_WINDOW) { 990 Log.d(TAG, "setWindowFocusable: skipping, already " + focusable); 991 } 992 return; 993 } 994 final View decorView = mWindow.peekDecorView(); 995 if (decorView != null && decorView.isAttachedToWindow()) { 996 mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); 997 } 998 } 999 getDefaultDisplay()1000 private Display getDefaultDisplay() { 1001 return mDisplayManager.getDisplay(DEFAULT_DISPLAY); 1002 } 1003 allowLongScreenshots()1004 private boolean allowLongScreenshots() { 1005 return !mIsLowRamDevice; 1006 } 1007 1008 /** Does the aspect ratio of the bitmap with insets removed match the bounds. */ aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds)1009 private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, 1010 Rect screenBounds) { 1011 int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right; 1012 int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom; 1013 1014 if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 1015 || bitmap.getHeight() == 0) { 1016 if (DEBUG_UI) { 1017 Log.e(TAG, "Provided bitmap and insets create degenerate region: " 1018 + bitmap.getWidth() + "x" + bitmap.getHeight() + " " + bitmapInsets); 1019 } 1020 return false; 1021 } 1022 1023 float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight; 1024 float boundsAspect = ((float) screenBounds.width()) / screenBounds.height(); 1025 1026 boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f; 1027 if (DEBUG_UI) { 1028 Log.d(TAG, "aspectRatiosMatch: don't match bitmap: " + insettedBitmapAspect 1029 + ", bounds: " + boundsAspect); 1030 } 1031 return matchWithinTolerance; 1032 } 1033 1034 private class ScreenshotExitTransitionCallbacksSupplier implements 1035 Supplier<ExitTransitionCallbacks> { 1036 final boolean mDismissOnHideSharedElements; 1037 1038 ScreenshotExitTransitionCallbacksSupplier(boolean dismissOnHideSharedElements) { 1039 mDismissOnHideSharedElements = dismissOnHideSharedElements; 1040 } 1041 1042 @Override 1043 public ExitTransitionCallbacks get() { 1044 return new ExitTransitionCallbacks() { 1045 @Override 1046 public boolean isReturnTransitionAllowed() { 1047 return false; 1048 } 1049 1050 @Override 1051 public void hideSharedElements() { 1052 if (mDismissOnHideSharedElements) { 1053 finishDismiss(); 1054 } 1055 } 1056 1057 @Override 1058 public void onFinish() { 1059 } 1060 }; 1061 } 1062 } 1063 } 1064