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