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