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.screenshotPrivateProfileAccessibilityAnnouncementFix; 23 import static com.android.systemui.Flags.screenshotShelfUi2; 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_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 android.animation.Animator; 34 import android.animation.AnimatorListenerAdapter; 35 import android.annotation.MainThread; 36 import android.annotation.NonNull; 37 import android.annotation.Nullable; 38 import android.app.ActivityOptions; 39 import android.app.ExitTransitionCoordinator; 40 import android.app.ICompatCameraControlCallback; 41 import android.app.Notification; 42 import android.content.BroadcastReceiver; 43 import android.content.Context; 44 import android.content.Intent; 45 import android.content.IntentFilter; 46 import android.content.pm.ActivityInfo; 47 import android.content.res.Configuration; 48 import android.graphics.Bitmap; 49 import android.graphics.Insets; 50 import android.graphics.Rect; 51 import android.net.Uri; 52 import android.os.Process; 53 import android.os.UserHandle; 54 import android.os.UserManager; 55 import android.provider.Settings; 56 import android.util.DisplayMetrics; 57 import android.util.Log; 58 import android.util.Pair; 59 import android.view.Display; 60 import android.view.ScrollCaptureResponse; 61 import android.view.View; 62 import android.view.ViewGroup; 63 import android.view.ViewRootImpl; 64 import android.view.ViewTreeObserver; 65 import android.view.WindowInsets; 66 import android.view.WindowManager; 67 import android.widget.Toast; 68 import android.window.WindowContext; 69 70 import com.android.internal.app.ChooserActivity; 71 import com.android.internal.logging.UiEventLogger; 72 import com.android.internal.policy.PhoneWindow; 73 import com.android.settingslib.applications.InterestingConfigChanges; 74 import com.android.systemui.broadcast.BroadcastDispatcher; 75 import com.android.systemui.broadcast.BroadcastSender; 76 import com.android.systemui.clipboardoverlay.ClipboardOverlayController; 77 import com.android.systemui.dagger.qualifiers.Main; 78 import com.android.systemui.flags.FeatureFlags; 79 import com.android.systemui.res.R; 80 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback; 81 import com.android.systemui.screenshot.scroll.ScrollCaptureExecutor; 82 import com.android.systemui.util.Assert; 83 84 import com.google.common.util.concurrent.ListenableFuture; 85 86 import dagger.assisted.Assisted; 87 import dagger.assisted.AssistedFactory; 88 import dagger.assisted.AssistedInject; 89 90 import kotlin.Unit; 91 92 import java.util.List; 93 import java.util.UUID; 94 import java.util.concurrent.Executor; 95 import java.util.concurrent.ExecutorService; 96 import java.util.concurrent.Executors; 97 import java.util.function.Consumer; 98 99 import javax.inject.Provider; 100 101 /** 102 * Controls the state and flow for screenshots. 103 */ 104 public class ScreenshotController implements ScreenshotHandler { 105 private static final String TAG = logTag(ScreenshotController.class); 106 107 /** 108 * POD used in the AsyncTask which saves an image in the background. 109 */ 110 static class SaveImageInBackgroundData { 111 public Bitmap image; 112 public Consumer<Uri> finisher; 113 public ScreenshotController.ActionsReadyListener mActionsReadyListener; 114 public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener; 115 public UserHandle owner; 116 public int displayId; 117 clearImage()118 void clearImage() { 119 image = null; 120 } 121 } 122 123 /** 124 * Structure returned by the SaveImageInBackgroundTask 125 */ 126 public static class SavedImageData { 127 public Uri uri; 128 public List<Notification.Action> smartActions; 129 public Notification.Action quickShareAction; 130 public UserHandle owner; 131 public String subject; // Title for sharing 132 public Long imageTime; // Time at which screenshot was saved 133 134 /** 135 * Used to reset the return data on error 136 */ reset()137 public void reset() { 138 uri = null; 139 smartActions = null; 140 quickShareAction = null; 141 subject = null; 142 imageTime = null; 143 } 144 } 145 146 /** 147 * Structure returned by the QueryQuickShareInBackgroundTask 148 */ 149 static class QuickShareData { 150 public Notification.Action quickShareAction; 151 152 /** 153 * Used to reset the return data on error 154 */ reset()155 public void reset() { 156 quickShareAction = null; 157 } 158 } 159 160 interface ActionsReadyListener { onActionsReady(ScreenshotController.SavedImageData imageData)161 void onActionsReady(ScreenshotController.SavedImageData imageData); 162 } 163 164 interface QuickShareActionReadyListener { onActionsReady(ScreenshotController.QuickShareData quickShareData)165 void onActionsReady(ScreenshotController.QuickShareData quickShareData); 166 } 167 168 public interface TransitionDestination { 169 /** 170 * Allows the long screenshot activity to call back with a destination location (the bounds 171 * on screen of the destination for the transitioning view) and a Runnable to be run once 172 * the transition animation is complete. 173 */ setTransitionDestination(Rect transitionDestination, Runnable onTransitionEnd)174 void setTransitionDestination(Rect transitionDestination, Runnable onTransitionEnd); 175 } 176 177 // These strings are used for communicating the action invoked to 178 // ScreenshotNotificationSmartActionsProvider. 179 public static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type"; 180 public static final String EXTRA_ID = "android:screenshot_id"; 181 public static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled"; 182 public static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent"; 183 public static final String EXTRA_ACTION_INTENT_FILLIN = 184 "android:screenshot_action_intent_fillin"; 185 186 187 // From WizardManagerHelper.java 188 private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; 189 190 static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; 191 192 private final WindowContext mContext; 193 private final FeatureFlags mFlags; 194 private final ScreenshotViewProxy mViewProxy; 195 private final ScreenshotNotificationsController mNotificationsController; 196 private final ScreenshotSmartActions mScreenshotSmartActions; 197 private final UiEventLogger mUiEventLogger; 198 private final ImageExporter mImageExporter; 199 private final ImageCapture mImageCapture; 200 private final Executor mMainExecutor; 201 private final ExecutorService mBgExecutor; 202 private final BroadcastSender mBroadcastSender; 203 private final BroadcastDispatcher mBroadcastDispatcher; 204 private final ScreenshotActionsController mActionsController; 205 206 private final WindowManager mWindowManager; 207 private final WindowManager.LayoutParams mWindowLayoutParams; 208 @Nullable 209 private final ScreenshotSoundController mScreenshotSoundController; 210 private final PhoneWindow mWindow; 211 private final Display mDisplay; 212 private final ScrollCaptureExecutor mScrollCaptureExecutor; 213 private final ScreenshotNotificationSmartActionsProvider 214 mScreenshotNotificationSmartActionsProvider; 215 private final TimeoutHandler mScreenshotHandler; 216 private final ActionIntentExecutor mActionIntentExecutor; 217 private final UserManager mUserManager; 218 private final AssistContentRequester mAssistContentRequester; 219 private final ActionExecutor mActionExecutor; 220 221 222 private final MessageContainerController mMessageContainerController; 223 private final AnnouncementResolver mAnnouncementResolver; 224 private Bitmap mScreenBitmap; 225 private SaveImageInBackgroundTask mSaveInBgTask; 226 private boolean mScreenshotTakenInPortrait; 227 private boolean mAttachRequested; 228 private boolean mDetachRequested; 229 private Animator mScreenshotAnimation; 230 private RequestCallback mCurrentRequestCallback; 231 private String mPackageName = ""; 232 private final BroadcastReceiver mCopyBroadcastReceiver; 233 234 // When false, the screenshot is taken without showing the ui. Note that this only applies to 235 // external displays, as on the default one the UI should **always** be shown. 236 // This is needed in case of screenshot during display mirroring, as adding another window to 237 // the external display makes mirroring stop. 238 // When there is a way to distinguish between displays that are mirroring or extending, this 239 // can be removed and we can directly show the ui only in the extended case. 240 private final Boolean mShowUIOnExternalDisplay; 241 /** Tracks config changes that require re-creating UI */ 242 private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges( 243 ActivityInfo.CONFIG_ORIENTATION 244 | ActivityInfo.CONFIG_LAYOUT_DIRECTION 245 | ActivityInfo.CONFIG_LOCALE 246 | ActivityInfo.CONFIG_UI_MODE 247 | ActivityInfo.CONFIG_SCREEN_LAYOUT 248 | ActivityInfo.CONFIG_ASSETS_PATHS); 249 250 251 @AssistedInject ScreenshotController( Context context, WindowManager windowManager, FeatureFlags flags, ScreenshotViewProxy.Factory viewProxyFactory, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory, UiEventLogger uiEventLogger, ImageExporter imageExporter, ImageCapture imageCapture, @Main Executor mainExecutor, ScrollCaptureExecutor scrollCaptureExecutor, TimeoutHandler timeoutHandler, BroadcastSender broadcastSender, BroadcastDispatcher broadcastDispatcher, ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, ScreenshotActionsController.Factory screenshotActionsControllerFactory, ActionIntentExecutor actionIntentExecutor, ActionExecutor.Factory actionExecutorFactory, UserManager userManager, AssistContentRequester assistContentRequester, MessageContainerController messageContainerController, Provider<ScreenshotSoundController> screenshotSoundController, AnnouncementResolver announcementResolver, @Assisted Display display, @Assisted boolean showUIOnExternalDisplay )252 ScreenshotController( 253 Context context, 254 WindowManager windowManager, 255 FeatureFlags flags, 256 ScreenshotViewProxy.Factory viewProxyFactory, 257 ScreenshotSmartActions screenshotSmartActions, 258 ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory, 259 UiEventLogger uiEventLogger, 260 ImageExporter imageExporter, 261 ImageCapture imageCapture, 262 @Main Executor mainExecutor, 263 ScrollCaptureExecutor scrollCaptureExecutor, 264 TimeoutHandler timeoutHandler, 265 BroadcastSender broadcastSender, 266 BroadcastDispatcher broadcastDispatcher, 267 ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, 268 ScreenshotActionsController.Factory screenshotActionsControllerFactory, 269 ActionIntentExecutor actionIntentExecutor, 270 ActionExecutor.Factory actionExecutorFactory, 271 UserManager userManager, 272 AssistContentRequester assistContentRequester, 273 MessageContainerController messageContainerController, 274 Provider<ScreenshotSoundController> screenshotSoundController, 275 AnnouncementResolver announcementResolver, 276 @Assisted Display display, 277 @Assisted boolean showUIOnExternalDisplay 278 ) { 279 mScreenshotSmartActions = screenshotSmartActions; 280 mNotificationsController = screenshotNotificationsControllerFactory.create( 281 display.getDisplayId()); 282 mUiEventLogger = uiEventLogger; 283 mImageExporter = imageExporter; 284 mImageCapture = imageCapture; 285 mMainExecutor = mainExecutor; 286 mScrollCaptureExecutor = scrollCaptureExecutor; 287 mScreenshotNotificationSmartActionsProvider = screenshotNotificationSmartActionsProvider; 288 mBgExecutor = Executors.newSingleThreadExecutor(); 289 mBroadcastSender = broadcastSender; 290 mBroadcastDispatcher = broadcastDispatcher; 291 292 mScreenshotHandler = timeoutHandler; 293 mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS); 294 295 mDisplay = display; 296 mWindowManager = windowManager; 297 final Context displayContext = context.createDisplayContext(display); 298 mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); 299 mFlags = flags; 300 mActionIntentExecutor = actionIntentExecutor; 301 mUserManager = userManager; 302 mMessageContainerController = messageContainerController; 303 mAssistContentRequester = assistContentRequester; 304 mAnnouncementResolver = announcementResolver; 305 306 mViewProxy = viewProxyFactory.getProxy(mContext, mDisplay.getDisplayId()); 307 308 mScreenshotHandler.setOnTimeoutRunnable(() -> { 309 if (DEBUG_UI) { 310 Log.d(TAG, "Corner timeout hit"); 311 } 312 mViewProxy.requestDismissal(SCREENSHOT_INTERACTION_TIMEOUT); 313 }); 314 315 // Setup the window that we are going to use 316 mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); 317 mWindowLayoutParams.setTitle("ScreenshotAnimation"); 318 319 mWindow = FloatingWindowUtil.getFloatingWindow(mContext); 320 mWindow.setWindowManager(mWindowManager, null, null); 321 322 mConfigChanges.applyNewConfig(context.getResources()); 323 reloadAssets(); 324 325 mActionExecutor = actionExecutorFactory.create(mWindow, mViewProxy, 326 () -> { 327 finishDismiss(); 328 return Unit.INSTANCE; 329 }); 330 mActionsController = screenshotActionsControllerFactory.getController(mActionExecutor); 331 332 333 // Sound is only reproduced from the controller of the default display. 334 if (mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY) { 335 mScreenshotSoundController = screenshotSoundController.get(); 336 } else { 337 mScreenshotSoundController = null; 338 } 339 340 mCopyBroadcastReceiver = new BroadcastReceiver() { 341 @Override 342 public void onReceive(Context context, Intent intent) { 343 if (ClipboardOverlayController.COPY_OVERLAY_ACTION.equals(intent.getAction())) { 344 mViewProxy.requestDismissal(SCREENSHOT_DISMISSED_OTHER); 345 } 346 } 347 }; 348 mBroadcastDispatcher.registerReceiver(mCopyBroadcastReceiver, new IntentFilter( 349 ClipboardOverlayController.COPY_OVERLAY_ACTION), null, null, 350 Context.RECEIVER_NOT_EXPORTED, ClipboardOverlayController.SELF_PERMISSION); 351 mShowUIOnExternalDisplay = showUIOnExternalDisplay; 352 } 353 354 @Override handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, RequestCallback requestCallback)355 public void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, 356 RequestCallback requestCallback) { 357 Assert.isMainThread(); 358 359 mCurrentRequestCallback = requestCallback; 360 if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_FULLSCREEN 361 && screenshot.getBitmap() == null) { 362 Rect bounds = getFullScreenRect(); 363 screenshot.setBitmap(mImageCapture.captureDisplay(mDisplay.getDisplayId(), bounds)); 364 screenshot.setScreenBounds(bounds); 365 } 366 367 if (screenshot.getBitmap() == null) { 368 Log.e(TAG, "handleScreenshot: Screenshot bitmap was null"); 369 mNotificationsController.notifyScreenshotError( 370 R.string.screenshot_failed_to_capture_text); 371 if (mCurrentRequestCallback != null) { 372 mCurrentRequestCallback.reportError(); 373 } 374 return; 375 } 376 377 mScreenBitmap = screenshot.getBitmap(); 378 String oldPackageName = mPackageName; 379 mPackageName = screenshot.getPackageNameString(); 380 381 if (!isUserSetupComplete(Process.myUserHandle())) { 382 Log.w(TAG, "User setup not complete, displaying toast only"); 383 // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing 384 // and sharing shouldn't be exposed to the user. 385 saveScreenshotAndToast(screenshot.getUserHandle(), finisher); 386 return; 387 } 388 389 mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION), 390 ClipboardOverlayController.SELF_PERMISSION); 391 392 mScreenshotTakenInPortrait = 393 mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; 394 395 // Optimizations 396 mScreenBitmap.setHasAlpha(false); 397 mScreenBitmap.prepareToDraw(); 398 399 prepareViewForNewScreenshot(screenshot, oldPackageName); 400 401 if (!shouldShowUi()) { 402 saveScreenshotInWorkerThread( 403 screenshot.getUserHandle(), finisher, this::logSuccessOnActionsReady, 404 (ignored) -> { 405 }); 406 return; 407 } 408 409 final UUID requestId; 410 if (screenshotShelfUi2()) { 411 requestId = mActionsController.setCurrentScreenshot(screenshot); 412 saveScreenshotInBackground(screenshot, requestId, finisher); 413 414 if (screenshot.getTaskId() >= 0) { 415 mAssistContentRequester.requestAssistContent( 416 screenshot.getTaskId(), 417 assistContent -> 418 mActionsController.onAssistContent(requestId, assistContent)); 419 } else { 420 mActionsController.onAssistContent(requestId, null); 421 } 422 } else { 423 requestId = UUID.randomUUID(); // passed through but unused for legacy UI 424 saveScreenshotInWorkerThread(screenshot.getUserHandle(), finisher, 425 this::showUiOnActionsReady, this::showUiOnQuickShareActionReady); 426 } 427 428 // The window is focusable by default 429 setWindowFocusable(true); 430 mViewProxy.requestFocus(); 431 432 enqueueScrollCaptureRequest(requestId, screenshot.getUserHandle()); 433 434 attachWindow(); 435 436 boolean showFlash; 437 if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) { 438 if (screenshot.getScreenBounds() != null 439 && aspectRatiosMatch(screenshot.getBitmap(), screenshot.getInsets(), 440 screenshot.getScreenBounds())) { 441 showFlash = false; 442 } else { 443 showFlash = true; 444 screenshot.setInsets(Insets.NONE); 445 screenshot.setScreenBounds(new Rect(0, 0, screenshot.getBitmap().getWidth(), 446 screenshot.getBitmap().getHeight())); 447 } 448 } else { 449 showFlash = true; 450 } 451 452 mViewProxy.prepareEntranceAnimation( 453 () -> startAnimation(screenshot.getScreenBounds(), showFlash, 454 () -> mMessageContainerController.onScreenshotTaken(screenshot))); 455 456 mViewProxy.setScreenshot(screenshot); 457 458 // ignore system bar insets for the purpose of window layout 459 mWindow.getDecorView().setOnApplyWindowInsetsListener( 460 (v, insets) -> WindowInsets.CONSUMED); 461 if (!screenshotShelfUi2()) { 462 mScreenshotHandler.cancelTimeout(); // restarted after animation 463 } 464 } 465 shouldShowUi()466 private boolean shouldShowUi() { 467 return mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY || mShowUIOnExternalDisplay; 468 } 469 prepareViewForNewScreenshot(@onNull ScreenshotData screenshot, String oldPackageName)470 void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) { 471 withWindowAttached(() -> { 472 if (screenshotPrivateProfileAccessibilityAnnouncementFix()) { 473 mAnnouncementResolver.getScreenshotAnnouncement( 474 screenshot.getUserHandle().getIdentifier(), 475 announcement -> { 476 mViewProxy.announceForAccessibility(announcement); 477 }); 478 } else { 479 if (mUserManager.isManagedProfile(screenshot.getUserHandle().getIdentifier())) { 480 mViewProxy.announceForAccessibility(mContext.getResources().getString( 481 R.string.screenshot_saving_work_profile_title)); 482 } else { 483 mViewProxy.announceForAccessibility( 484 mContext.getResources().getString(R.string.screenshot_saving_title)); 485 } 486 } 487 }); 488 489 mViewProxy.reset(); 490 491 if (mViewProxy.isAttachedToWindow()) { 492 // if we didn't already dismiss for another reason 493 if (!mViewProxy.isDismissing()) { 494 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0, 495 oldPackageName); 496 } 497 if (DEBUG_WINDOW) { 498 Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. " 499 + "(dismissing=" + mViewProxy.isDismissing() + ")"); 500 } 501 } 502 503 mViewProxy.setPackageName(mPackageName); 504 505 mViewProxy.updateOrientation( 506 mWindowManager.getCurrentWindowMetrics().getWindowInsets()); 507 } 508 509 /** 510 * Requests the view to dismiss the current screenshot (may be ignored, if screenshot is already 511 * being dismissed) 512 */ requestDismissal(ScreenshotEvent event)513 void requestDismissal(ScreenshotEvent event) { 514 mViewProxy.requestDismissal(event); 515 } 516 isPendingSharedTransition()517 boolean isPendingSharedTransition() { 518 if (screenshotShelfUi2()) { 519 return mActionExecutor.isPendingSharedTransition(); 520 } else { 521 return mViewProxy.isPendingSharedTransition(); 522 } 523 } 524 525 // Any cleanup needed when the service is being destroyed. onDestroy()526 void onDestroy() { 527 if (mSaveInBgTask != null) { 528 // just log success/failure for the pre-existing screenshot 529 mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); 530 } 531 removeWindow(); 532 releaseMediaPlayer(); 533 releaseContext(); 534 mBgExecutor.shutdown(); 535 } 536 537 /** 538 * Release the constructed window context. 539 */ releaseContext()540 private void releaseContext() { 541 mBroadcastDispatcher.unregisterReceiver(mCopyBroadcastReceiver); 542 mContext.release(); 543 } 544 releaseMediaPlayer()545 private void releaseMediaPlayer() { 546 if (mScreenshotSoundController == null) return; 547 mScreenshotSoundController.releaseScreenshotSoundAsync(); 548 } 549 550 /** 551 * Update resources on configuration change. Reinflate for theme/color changes. 552 */ reloadAssets()553 private void reloadAssets() { 554 if (DEBUG_UI) { 555 Log.d(TAG, "reloadAssets()"); 556 } 557 558 mMessageContainerController.setView(mViewProxy.getView()); 559 mViewProxy.setCallbacks(new ScreenshotView.ScreenshotViewCallback() { 560 @Override 561 public void onUserInteraction() { 562 if (DEBUG_INPUT) { 563 Log.d(TAG, "onUserInteraction"); 564 } 565 mScreenshotHandler.resetTimeout(); 566 } 567 568 @Override 569 public void onAction(Intent intent, UserHandle owner, boolean overrideTransition) { 570 Pair<ActivityOptions, ExitTransitionCoordinator> exit = createWindowTransition(); 571 mActionIntentExecutor.launchIntentAsync( 572 intent, owner, overrideTransition, exit.first, exit.second); 573 } 574 575 @Override 576 public void onDismiss() { 577 finishDismiss(); 578 } 579 580 @Override 581 public void onTouchOutside() { 582 // TODO(159460485): Remove this when focus is handled properly in the system 583 setWindowFocusable(false); 584 } 585 }); 586 587 if (DEBUG_WINDOW) { 588 Log.d(TAG, "setContentView: " + mViewProxy.getView()); 589 } 590 mWindow.setContentView(mViewProxy.getView()); 591 } 592 enqueueScrollCaptureRequest(UUID requestId, UserHandle owner)593 private void enqueueScrollCaptureRequest(UUID requestId, UserHandle owner) { 594 // Wait until this window is attached to request because it is 595 // the reference used to locate the target window (below). 596 withWindowAttached(() -> { 597 requestScrollCapture(requestId, owner); 598 mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( 599 new ViewRootImpl.ActivityConfigCallback() { 600 @Override 601 public void onConfigurationChanged(Configuration overrideConfig, 602 int newDisplayId) { 603 if (mConfigChanges.applyNewConfig(mContext.getResources())) { 604 // Hide the scroll chip until we know it's available in this 605 // orientation 606 if (screenshotShelfUi2()) { 607 mActionsController.onScrollChipInvalidated(); 608 } else { 609 mViewProxy.hideScrollChip(); 610 } 611 // Delay scroll capture eval a bit to allow the underlying activity 612 // to set up in the new orientation. 613 mScreenshotHandler.postDelayed( 614 () -> requestScrollCapture(requestId, owner), 150); 615 mViewProxy.updateInsets( 616 mWindowManager.getCurrentWindowMetrics().getWindowInsets()); 617 // Screenshot animation calculations won't be valid anymore, 618 // so just end 619 if (mScreenshotAnimation != null 620 && mScreenshotAnimation.isRunning()) { 621 mScreenshotAnimation.end(); 622 } 623 } 624 } 625 626 @Override 627 public void requestCompatCameraControl(boolean showControl, 628 boolean transformationApplied, 629 ICompatCameraControlCallback callback) { 630 Log.w(TAG, "Unexpected requestCompatCameraControl callback"); 631 } 632 }); 633 }); 634 } 635 requestScrollCapture(UUID requestId, UserHandle owner)636 private void requestScrollCapture(UUID requestId, UserHandle owner) { 637 mScrollCaptureExecutor.requestScrollCapture( 638 mDisplay.getDisplayId(), 639 mWindow.getDecorView().getWindowToken(), 640 (response) -> { 641 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION, 642 0, response.getPackageName()); 643 if (screenshotShelfUi2()) { 644 mActionsController.onScrollChipReady(requestId, 645 () -> onScrollButtonClicked(owner, response)); 646 } else { 647 mViewProxy.showScrollChip(response.getPackageName(), 648 () -> onScrollButtonClicked(owner, response)); 649 } 650 return Unit.INSTANCE; 651 } 652 ); 653 } 654 onScrollButtonClicked(UserHandle owner, ScrollCaptureResponse response)655 private void onScrollButtonClicked(UserHandle owner, ScrollCaptureResponse response) { 656 if (DEBUG_INPUT) { 657 Log.d(TAG, "scroll chip tapped"); 658 } 659 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED, 0, 660 response.getPackageName()); 661 Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplay.getDisplayId(), 662 getFullScreenRect()); 663 if (newScreenshot == null) { 664 Log.e(TAG, "Failed to capture current screenshot for scroll transition!"); 665 return; 666 } 667 // delay starting scroll capture to make sure scrim is up before the app moves 668 mViewProxy.prepareScrollingTransition(response, mScreenBitmap, newScreenshot, 669 mScreenshotTakenInPortrait, () -> executeBatchScrollCapture(response, owner)); 670 } 671 executeBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner)672 private void executeBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) { 673 mScrollCaptureExecutor.executeBatchScrollCapture(response, 674 () -> { 675 final Intent intent = ActionIntentCreator.INSTANCE.createLongScreenshotIntent( 676 owner, mContext); 677 mContext.startActivity(intent); 678 }, 679 mViewProxy::restoreNonScrollingUi, 680 mViewProxy::startLongScreenshotTransition); 681 } 682 withWindowAttached(Runnable action)683 private void withWindowAttached(Runnable action) { 684 View decorView = mWindow.getDecorView(); 685 if (decorView.isAttachedToWindow()) { 686 action.run(); 687 } else { 688 decorView.getViewTreeObserver().addOnWindowAttachListener( 689 new ViewTreeObserver.OnWindowAttachListener() { 690 @Override 691 public void onWindowAttached() { 692 mAttachRequested = false; 693 decorView.getViewTreeObserver().removeOnWindowAttachListener(this); 694 action.run(); 695 } 696 697 @Override 698 public void onWindowDetached() { 699 } 700 }); 701 702 } 703 } 704 705 @MainThread attachWindow()706 private void attachWindow() { 707 View decorView = mWindow.getDecorView(); 708 if (decorView.isAttachedToWindow() || mAttachRequested) { 709 return; 710 } 711 if (DEBUG_WINDOW) { 712 Log.d(TAG, "attachWindow"); 713 } 714 mAttachRequested = true; 715 mWindowManager.addView(decorView, mWindowLayoutParams); 716 decorView.requestApplyInsets(); 717 718 if (screenshotShelfUi2()) { 719 ViewGroup layout = decorView.requireViewById(android.R.id.content); 720 layout.setClipChildren(false); 721 layout.setClipToPadding(false); 722 } 723 } 724 removeWindow()725 void removeWindow() { 726 final View decorView = mWindow.peekDecorView(); 727 if (decorView != null && decorView.isAttachedToWindow()) { 728 if (DEBUG_WINDOW) { 729 Log.d(TAG, "Removing screenshot window"); 730 } 731 mWindowManager.removeViewImmediate(decorView); 732 mDetachRequested = false; 733 } 734 if (mAttachRequested && !mDetachRequested) { 735 mDetachRequested = true; 736 withWindowAttached(this::removeWindow); 737 } 738 739 mViewProxy.stopInputListening(); 740 } 741 playCameraSoundIfNeeded()742 private void playCameraSoundIfNeeded() { 743 if (mScreenshotSoundController == null) return; 744 // the controller is not-null only on the default display controller 745 mScreenshotSoundController.playScreenshotSoundAsync(); 746 } 747 748 /** 749 * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on 750 * failure). 751 */ saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher)752 private void saveScreenshotAndToast(UserHandle owner, Consumer<Uri> finisher) { 753 // Play the shutter sound to notify that we've taken a screenshot 754 playCameraSoundIfNeeded(); 755 756 saveScreenshotInWorkerThread( 757 owner, 758 /* onComplete */ finisher, 759 /* actionsReadyListener */ imageData -> { 760 if (DEBUG_CALLBACK) { 761 Log.d(TAG, "returning URI to finisher (Consumer<URI>): " + imageData.uri); 762 } 763 finisher.accept(imageData.uri); 764 if (imageData.uri == null) { 765 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName); 766 mNotificationsController.notifyScreenshotError( 767 R.string.screenshot_failed_to_save_text); 768 } else { 769 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName); 770 mScreenshotHandler.post(() -> Toast.makeText(mContext, 771 R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); 772 } 773 }, 774 null); 775 } 776 777 /** 778 * Starts the animation after taking the screenshot 779 */ startAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete)780 private void startAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete) { 781 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 782 mScreenshotAnimation.cancel(); 783 } 784 785 mScreenshotAnimation = 786 mViewProxy.createScreenshotDropInAnimation(screenRect, showFlash); 787 if (onAnimationComplete != null) { 788 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 789 @Override 790 public void onAnimationEnd(Animator animation) { 791 super.onAnimationEnd(animation); 792 onAnimationComplete.run(); 793 } 794 }); 795 } 796 797 // Play the shutter sound to notify that we've taken a screenshot 798 playCameraSoundIfNeeded(); 799 800 if (DEBUG_ANIM) { 801 Log.d(TAG, "starting post-screenshot animation"); 802 } 803 mScreenshotAnimation.start(); 804 } 805 806 /** 807 * Supplies the necessary bits for the shared element transition to share sheet. 808 * Note that once called, the action intent to share must be sent immediately after. 809 */ createWindowTransition()810 private Pair<ActivityOptions, ExitTransitionCoordinator> createWindowTransition() { 811 ExitTransitionCoordinator.ExitTransitionCallbacks callbacks = 812 new ExitTransitionCoordinator.ExitTransitionCallbacks() { 813 @Override 814 public boolean isReturnTransitionAllowed() { 815 return false; 816 } 817 818 @Override 819 public void hideSharedElements() { 820 finishDismiss(); 821 } 822 823 @Override 824 public void onFinish() { 825 } 826 }; 827 828 return ActivityOptions.startSharedElementAnimation(mWindow, callbacks, null, 829 Pair.create(mViewProxy.getScreenshotPreview(), 830 ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)); 831 } 832 833 /** Reset screenshot view and then call onCompleteRunnable */ finishDismiss()834 private void finishDismiss() { 835 Log.d(TAG, "finishDismiss"); 836 mActionsController.endScreenshotSession(); 837 mScrollCaptureExecutor.close(); 838 if (mCurrentRequestCallback != null) { 839 mCurrentRequestCallback.onFinish(); 840 mCurrentRequestCallback = null; 841 } 842 mViewProxy.reset(); 843 removeWindow(); 844 mScreenshotHandler.cancelTimeout(); 845 } 846 saveScreenshotInBackground( ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher)847 private void saveScreenshotInBackground( 848 ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher) { 849 ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor, 850 requestId, screenshot.getBitmap(), screenshot.getUserOrDefault(), 851 mDisplay.getDisplayId()); 852 future.addListener(() -> { 853 try { 854 ImageExporter.Result result = future.get(); 855 Log.d(TAG, "Saved screenshot: " + result); 856 logScreenshotResultStatus(result.uri, screenshot.getUserHandle()); 857 if (result.uri != null) { 858 mActionsController.setCompletedScreenshot(requestId, new ScreenshotSavedResult( 859 result.uri, screenshot.getUserOrDefault(), result.timestamp)); 860 } 861 if (DEBUG_CALLBACK) { 862 Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) " 863 + "finisher.accept(\"" + result.uri + "\""); 864 } 865 finisher.accept(result.uri); 866 } catch (Exception e) { 867 Log.d(TAG, "Failed to store screenshot", e); 868 if (DEBUG_CALLBACK) { 869 Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)"); 870 } 871 finisher.accept(null); 872 } 873 }, mMainExecutor); 874 } 875 876 /** 877 * Creates a new worker thread and saves the screenshot to the media store. 878 */ saveScreenshotInWorkerThread( UserHandle owner, @NonNull Consumer<Uri> finisher, @Nullable ActionsReadyListener actionsReadyListener, @Nullable QuickShareActionReadyListener quickShareActionsReadyListener)879 private void saveScreenshotInWorkerThread( 880 UserHandle owner, 881 @NonNull Consumer<Uri> finisher, 882 @Nullable ActionsReadyListener actionsReadyListener, 883 @Nullable QuickShareActionReadyListener 884 quickShareActionsReadyListener) { 885 ScreenshotController.SaveImageInBackgroundData 886 data = new ScreenshotController.SaveImageInBackgroundData(); 887 data.image = mScreenBitmap; 888 data.finisher = finisher; 889 data.mActionsReadyListener = actionsReadyListener; 890 data.mQuickShareActionsReadyListener = quickShareActionsReadyListener; 891 data.owner = owner; 892 data.displayId = mDisplay.getDisplayId(); 893 894 if (mSaveInBgTask != null) { 895 // just log success/failure for the pre-existing screenshot 896 mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady); 897 } 898 899 mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mFlags, mImageExporter, 900 mScreenshotSmartActions, data, 901 mScreenshotNotificationSmartActionsProvider); 902 mSaveInBgTask.execute(); 903 } 904 905 906 /** 907 * Sets up the action shade and its entrance animation, once we get the screenshot URI. 908 */ showUiOnActionsReady(ScreenshotController.SavedImageData imageData)909 private void showUiOnActionsReady(ScreenshotController.SavedImageData imageData) { 910 logSuccessOnActionsReady(imageData); 911 mScreenshotHandler.resetTimeout(); 912 913 if (imageData.uri != null) { 914 if (DEBUG_UI) { 915 Log.d(TAG, "Showing UI actions"); 916 } 917 if (!imageData.owner.equals(Process.myUserHandle())) { 918 Log.d(TAG, "Screenshot saved to user " + imageData.owner + " as " 919 + imageData.uri); 920 } 921 mScreenshotHandler.post(() -> { 922 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 923 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 924 @Override 925 public void onAnimationEnd(Animator animation) { 926 super.onAnimationEnd(animation); 927 mViewProxy.setChipIntents(imageData); 928 } 929 }); 930 } else { 931 mViewProxy.setChipIntents(imageData); 932 } 933 }); 934 } 935 } 936 937 /** 938 * Sets up the action shade and its entrance animation, once we get the Quick Share action data. 939 */ showUiOnQuickShareActionReady(ScreenshotController.QuickShareData quickShareData)940 private void showUiOnQuickShareActionReady(ScreenshotController.QuickShareData quickShareData) { 941 if (DEBUG_UI) { 942 Log.d(TAG, "Showing UI for Quick Share action"); 943 } 944 if (quickShareData.quickShareAction != null) { 945 mScreenshotHandler.post(() -> { 946 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 947 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 948 @Override 949 public void onAnimationEnd(Animator animation) { 950 super.onAnimationEnd(animation); 951 mViewProxy.addQuickShareChip(quickShareData.quickShareAction); 952 } 953 }); 954 } else { 955 mViewProxy.addQuickShareChip(quickShareData.quickShareAction); 956 } 957 }); 958 } 959 } 960 961 /** 962 * Logs success/failure of the screenshot saving task, and shows an error if it failed. 963 */ logScreenshotResultStatus(Uri uri, UserHandle owner)964 private void logScreenshotResultStatus(Uri uri, UserHandle owner) { 965 if (uri == null) { 966 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName); 967 mNotificationsController.notifyScreenshotError( 968 R.string.screenshot_failed_to_save_text); 969 } else { 970 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName); 971 if (mUserManager.isManagedProfile(owner.getIdentifier())) { 972 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE, 0, 973 mPackageName); 974 } 975 } 976 } 977 978 /** 979 * Logs success/failure of the screenshot saving task, and shows an error if it failed. 980 */ logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData)981 private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) { 982 logScreenshotResultStatus(imageData.uri, imageData.owner); 983 } 984 isUserSetupComplete(UserHandle owner)985 private boolean isUserSetupComplete(UserHandle owner) { 986 return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0) 987 .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; 988 } 989 990 /** 991 * Updates the window focusability. If the window is already showing, then it updates the 992 * window immediately, otherwise the layout params will be applied when the window is next 993 * shown. 994 */ setWindowFocusable(boolean focusable)995 private void setWindowFocusable(boolean focusable) { 996 if (DEBUG_WINDOW) { 997 Log.d(TAG, "setWindowFocusable: " + focusable); 998 } 999 int flags = mWindowLayoutParams.flags; 1000 if (focusable) { 1001 mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 1002 } else { 1003 mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 1004 } 1005 if (mWindowLayoutParams.flags == flags) { 1006 if (DEBUG_WINDOW) { 1007 Log.d(TAG, "setWindowFocusable: skipping, already " + focusable); 1008 } 1009 return; 1010 } 1011 final View decorView = mWindow.peekDecorView(); 1012 if (decorView != null && decorView.isAttachedToWindow()) { 1013 mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); 1014 } 1015 } 1016 getFullScreenRect()1017 private Rect getFullScreenRect() { 1018 DisplayMetrics displayMetrics = new DisplayMetrics(); 1019 mDisplay.getRealMetrics(displayMetrics); 1020 return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels); 1021 } 1022 1023 /** Does the aspect ratio of the bitmap with insets removed match the bounds. */ aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds)1024 private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, 1025 Rect screenBounds) { 1026 int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right; 1027 int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom; 1028 1029 if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 1030 || bitmap.getHeight() == 0) { 1031 if (DEBUG_UI) { 1032 Log.e(TAG, "Provided bitmap and insets create degenerate region: " 1033 + bitmap.getWidth() + "x" + bitmap.getHeight() + " " + bitmapInsets); 1034 } 1035 return false; 1036 } 1037 1038 float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight; 1039 float boundsAspect = ((float) screenBounds.width()) / screenBounds.height(); 1040 1041 boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f; 1042 if (DEBUG_UI) { 1043 Log.d(TAG, "aspectRatiosMatch: don't match bitmap: " + insettedBitmapAspect 1044 + ", bounds: " + boundsAspect); 1045 } 1046 return matchWithinTolerance; 1047 } 1048 1049 /** Injectable factory to create screenshot controller instances for a specific display. */ 1050 @AssistedFactory 1051 public interface Factory { 1052 /** 1053 * Creates an instance of the controller for that specific display. 1054 * 1055 * @param display display to capture 1056 * @param showUIOnExternalDisplay Whether the UI should be shown if this is an external 1057 * display. 1058 */ 1059 ScreenshotController create(Display display, boolean showUIOnExternalDisplay); 1060 } 1061 } 1062