1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.screenshot; 18 19 import static android.content.res.Configuration.ORIENTATION_PORTRAIT; 20 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; 21 22 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM; 23 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK; 24 import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT; 25 import static com.android.systemui.screenshot.LogConfig.DEBUG_UI; 26 import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW; 27 import static com.android.systemui.screenshot.LogConfig.logTag; 28 import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER; 29 import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT; 30 31 import android.animation.Animator; 32 import android.animation.AnimatorListenerAdapter; 33 import android.annotation.MainThread; 34 import android.annotation.NonNull; 35 import android.annotation.Nullable; 36 import android.content.BroadcastReceiver; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.content.IntentFilter; 40 import android.content.pm.ActivityInfo; 41 import android.content.res.Configuration; 42 import android.graphics.Bitmap; 43 import android.graphics.Insets; 44 import android.graphics.Rect; 45 import android.net.Uri; 46 import android.os.Process; 47 import android.os.UserHandle; 48 import android.os.UserManager; 49 import android.provider.Settings; 50 import android.util.DisplayMetrics; 51 import android.util.Log; 52 import android.view.Display; 53 import android.view.ScrollCaptureResponse; 54 import android.view.View; 55 import android.view.ViewGroup; 56 import android.view.ViewRootImpl; 57 import android.view.ViewTreeObserver; 58 import android.view.WindowInsets; 59 import android.view.WindowManager; 60 import android.widget.Toast; 61 import android.window.WindowContext; 62 63 import com.android.internal.logging.UiEventLogger; 64 import com.android.internal.policy.PhoneWindow; 65 import com.android.settingslib.applications.InterestingConfigChanges; 66 import com.android.systemui.broadcast.BroadcastDispatcher; 67 import com.android.systemui.broadcast.BroadcastSender; 68 import com.android.systemui.clipboardoverlay.ClipboardOverlayController; 69 import com.android.systemui.dagger.qualifiers.Main; 70 import com.android.systemui.flags.FeatureFlags; 71 import com.android.systemui.res.R; 72 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback; 73 import com.android.systemui.screenshot.scroll.ScrollCaptureExecutor; 74 import com.android.systemui.util.Assert; 75 76 import com.google.common.util.concurrent.ListenableFuture; 77 78 import dagger.assisted.Assisted; 79 import dagger.assisted.AssistedFactory; 80 import dagger.assisted.AssistedInject; 81 82 import kotlin.Unit; 83 84 import java.util.UUID; 85 import java.util.concurrent.Executor; 86 import java.util.concurrent.ExecutorService; 87 import java.util.concurrent.Executors; 88 import java.util.function.Consumer; 89 90 import javax.inject.Provider; 91 92 /** 93 * Controls the state and flow for screenshots. 94 */ 95 public class LegacyScreenshotController implements InteractiveScreenshotHandler { 96 private static final String TAG = logTag(LegacyScreenshotController.class); 97 98 // From WizardManagerHelper.java 99 private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete"; 100 101 static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000; 102 103 private final WindowContext mContext; 104 private final FeatureFlags mFlags; 105 private final ScreenshotShelfViewProxy mViewProxy; 106 private final ScreenshotNotificationsController mNotificationsController; 107 private final ScreenshotSmartActions mScreenshotSmartActions; 108 private final UiEventLogger mUiEventLogger; 109 private final ImageExporter mImageExporter; 110 private final ImageCapture mImageCapture; 111 private final Executor mMainExecutor; 112 private final ExecutorService mBgExecutor; 113 private final BroadcastSender mBroadcastSender; 114 private final BroadcastDispatcher mBroadcastDispatcher; 115 private final ScreenshotActionsController mActionsController; 116 117 private final WindowManager mWindowManager; 118 private final WindowManager.LayoutParams mWindowLayoutParams; 119 @Nullable 120 private final ScreenshotSoundController mScreenshotSoundController; 121 private final PhoneWindow mWindow; 122 private final Display mDisplay; 123 private final ScrollCaptureExecutor mScrollCaptureExecutor; 124 private final ScreenshotNotificationSmartActionsProvider 125 mScreenshotNotificationSmartActionsProvider; 126 private final TimeoutHandler mScreenshotHandler; 127 private final UserManager mUserManager; 128 private final AssistContentRequester mAssistContentRequester; 129 private final ActionExecutor mActionExecutor; 130 131 132 private final MessageContainerController mMessageContainerController; 133 private final AnnouncementResolver mAnnouncementResolver; 134 private Bitmap mScreenBitmap; 135 private boolean mScreenshotTakenInPortrait; 136 private boolean mAttachRequested; 137 private boolean mDetachRequested; 138 private Animator mScreenshotAnimation; 139 private RequestCallback mCurrentRequestCallback; 140 private String mPackageName = ""; 141 private final BroadcastReceiver mCopyBroadcastReceiver; 142 private final ActionIntentCreator mActionIntentCreator; 143 144 /** Tracks config changes that require re-creating UI */ 145 private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges( 146 ActivityInfo.CONFIG_ORIENTATION 147 | ActivityInfo.CONFIG_LAYOUT_DIRECTION 148 | ActivityInfo.CONFIG_LOCALE 149 | ActivityInfo.CONFIG_UI_MODE 150 | ActivityInfo.CONFIG_SCREEN_LAYOUT 151 | ActivityInfo.CONFIG_ASSETS_PATHS); 152 153 154 @AssistedInject LegacyScreenshotController( Context context, WindowManager windowManager, FeatureFlags flags, ScreenshotShelfViewProxy.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, ActionExecutor.Factory actionExecutorFactory, UserManager userManager, AssistContentRequester assistContentRequester, MessageContainerController messageContainerController, Provider<ScreenshotSoundController> screenshotSoundController, AnnouncementResolver announcementResolver, ActionIntentCreator actionIntentCreator, @Assisted Display display )155 LegacyScreenshotController( 156 Context context, 157 WindowManager windowManager, 158 FeatureFlags flags, 159 ScreenshotShelfViewProxy.Factory viewProxyFactory, 160 ScreenshotSmartActions screenshotSmartActions, 161 ScreenshotNotificationsController.Factory screenshotNotificationsControllerFactory, 162 UiEventLogger uiEventLogger, 163 ImageExporter imageExporter, 164 ImageCapture imageCapture, 165 @Main Executor mainExecutor, 166 ScrollCaptureExecutor scrollCaptureExecutor, 167 TimeoutHandler timeoutHandler, 168 BroadcastSender broadcastSender, 169 BroadcastDispatcher broadcastDispatcher, 170 ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider, 171 ScreenshotActionsController.Factory screenshotActionsControllerFactory, 172 ActionExecutor.Factory actionExecutorFactory, 173 UserManager userManager, 174 AssistContentRequester assistContentRequester, 175 MessageContainerController messageContainerController, 176 Provider<ScreenshotSoundController> screenshotSoundController, 177 AnnouncementResolver announcementResolver, 178 ActionIntentCreator actionIntentCreator, 179 @Assisted Display display 180 ) { 181 mScreenshotSmartActions = screenshotSmartActions; 182 mNotificationsController = screenshotNotificationsControllerFactory.create( 183 display.getDisplayId()); 184 mUiEventLogger = uiEventLogger; 185 mImageExporter = imageExporter; 186 mImageCapture = imageCapture; 187 mMainExecutor = mainExecutor; 188 mScrollCaptureExecutor = scrollCaptureExecutor; 189 mScreenshotNotificationSmartActionsProvider = screenshotNotificationSmartActionsProvider; 190 mBgExecutor = Executors.newSingleThreadExecutor(); 191 mBroadcastSender = broadcastSender; 192 mBroadcastDispatcher = broadcastDispatcher; 193 194 mScreenshotHandler = timeoutHandler; 195 mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS); 196 197 mDisplay = display; 198 mWindowManager = windowManager; 199 final Context displayContext = context.createDisplayContext(display); 200 mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null); 201 mFlags = flags; 202 mUserManager = userManager; 203 mMessageContainerController = messageContainerController; 204 mAssistContentRequester = assistContentRequester; 205 mAnnouncementResolver = announcementResolver; 206 mActionIntentCreator = actionIntentCreator; 207 208 mViewProxy = viewProxyFactory.getProxy(mContext, mDisplay.getDisplayId()); 209 210 mScreenshotHandler.setOnTimeoutRunnable(() -> { 211 if (DEBUG_UI) { 212 Log.d(TAG, "Corner timeout hit"); 213 } 214 mViewProxy.requestDismissal(SCREENSHOT_INTERACTION_TIMEOUT); 215 }); 216 217 // Setup the window that we are going to use 218 mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams(); 219 mWindowLayoutParams.setTitle("ScreenshotAnimation"); 220 221 mWindow = FloatingWindowUtil.getFloatingWindow(mContext); 222 mWindow.setWindowManager(mWindowManager, null, null); 223 224 mConfigChanges.applyNewConfig(context.getResources()); 225 reloadAssets(); 226 227 mActionExecutor = actionExecutorFactory.create(mWindow, mViewProxy, 228 () -> { 229 finishDismiss(); 230 return Unit.INSTANCE; 231 }); 232 mActionsController = screenshotActionsControllerFactory.getController(mActionExecutor); 233 234 235 // Sound is only reproduced from the controller of the default display. 236 if (mDisplay.getDisplayId() == Display.DEFAULT_DISPLAY) { 237 mScreenshotSoundController = screenshotSoundController.get(); 238 } else { 239 mScreenshotSoundController = null; 240 } 241 242 mCopyBroadcastReceiver = new BroadcastReceiver() { 243 @Override 244 public void onReceive(Context context, Intent intent) { 245 if (ClipboardOverlayController.COPY_OVERLAY_ACTION.equals(intent.getAction())) { 246 mViewProxy.requestDismissal(SCREENSHOT_DISMISSED_OTHER); 247 } 248 } 249 }; 250 mBroadcastDispatcher.registerReceiver(mCopyBroadcastReceiver, new IntentFilter( 251 ClipboardOverlayController.COPY_OVERLAY_ACTION), null, null, 252 Context.RECEIVER_NOT_EXPORTED, ClipboardOverlayController.SELF_PERMISSION); 253 } 254 255 @Override handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, RequestCallback requestCallback)256 public void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher, 257 RequestCallback requestCallback) { 258 Assert.isMainThread(); 259 260 mCurrentRequestCallback = requestCallback; 261 262 if (screenshot.getBitmap() == null) { 263 Log.e(TAG, "handleScreenshot: Screenshot bitmap was null"); 264 mNotificationsController.notifyScreenshotError( 265 R.string.screenshot_failed_to_capture_text); 266 if (mCurrentRequestCallback != null) { 267 mCurrentRequestCallback.reportError(); 268 } 269 return; 270 } 271 272 mScreenBitmap = screenshot.getBitmap(); 273 String oldPackageName = mPackageName; 274 mPackageName = screenshot.getPackageNameString(); 275 276 if (!isUserSetupComplete(Process.myUserHandle())) { 277 Log.w(TAG, "User setup not complete, displaying toast only"); 278 // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing 279 // and sharing shouldn't be exposed to the user. 280 saveScreenshotAndToast(screenshot, finisher); 281 return; 282 } 283 284 mBroadcastSender.sendBroadcast(new Intent(ClipboardOverlayController.SCREENSHOT_ACTION), 285 ClipboardOverlayController.SELF_PERMISSION); 286 287 mScreenshotTakenInPortrait = 288 mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT; 289 290 // Optimizations 291 mScreenBitmap.setHasAlpha(false); 292 mScreenBitmap.prepareToDraw(); 293 294 prepareViewForNewScreenshot(screenshot, oldPackageName); 295 296 final UUID requestId; 297 requestId = mActionsController.setCurrentScreenshot(screenshot); 298 saveScreenshotInBackground(screenshot, requestId, finisher, result -> { 299 if (result.uri != null) { 300 ScreenshotSavedResult savedScreenshot = new ScreenshotSavedResult( 301 result.uri, screenshot.getUserHandle(), result.timestamp); 302 mActionsController.setCompletedScreenshot(requestId, savedScreenshot); 303 } 304 }); 305 306 if (screenshot.getTaskId() >= 0) { 307 mAssistContentRequester.requestAssistContent( 308 screenshot.getTaskId(), 309 assistContent -> 310 mActionsController.onAssistContent(requestId, assistContent)); 311 } else { 312 mActionsController.onAssistContent(requestId, null); 313 } 314 315 // The window is focusable by default 316 setWindowFocusable(true); 317 mViewProxy.requestFocus(); 318 319 if (screenshot.getType() != WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) { 320 enqueueScrollCaptureRequest(requestId, screenshot.getUserHandle()); 321 } 322 323 attachWindow(); 324 325 Rect bounds = screenshot.getOriginalScreenBounds(); 326 boolean showFlash; 327 if (screenshot.getType() == WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE) { 328 if (bounds != null 329 && aspectRatiosMatch(screenshot.getBitmap(), screenshot.getOriginalInsets(), 330 bounds)) { 331 showFlash = false; 332 } else { 333 showFlash = true; 334 bounds = new Rect(0, 0, screenshot.getBitmap().getWidth(), 335 screenshot.getBitmap().getHeight()); 336 } 337 } else { 338 showFlash = true; 339 } 340 341 final Rect animationBounds = bounds; 342 mViewProxy.prepareEntranceAnimation( 343 () -> startAnimation(animationBounds, showFlash, 344 () -> mMessageContainerController.onScreenshotTaken(screenshot))); 345 346 mViewProxy.setScreenshot(screenshot); 347 348 // ignore system bar insets for the purpose of window layout 349 mWindow.getDecorView().setOnApplyWindowInsetsListener( 350 (v, insets) -> WindowInsets.CONSUMED); 351 } 352 prepareViewForNewScreenshot(@onNull ScreenshotData screenshot, String oldPackageName)353 void prepareViewForNewScreenshot(@NonNull ScreenshotData screenshot, String oldPackageName) { 354 withWindowAttached(() -> { 355 mAnnouncementResolver.getScreenshotAnnouncement( 356 screenshot.getUserHandle().getIdentifier(), 357 mViewProxy::announceForAccessibility); 358 }); 359 360 mViewProxy.reset(); 361 362 if (mViewProxy.isAttachedToWindow()) { 363 // if we didn't already dismiss for another reason 364 if (!mViewProxy.isDismissing()) { 365 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0, 366 oldPackageName); 367 } 368 if (DEBUG_WINDOW) { 369 Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. " 370 + "(dismissing=" + mViewProxy.isDismissing() + ")"); 371 } 372 } 373 374 mViewProxy.setPackageName(mPackageName); 375 } 376 377 /** 378 * Requests the view to dismiss the current screenshot (may be ignored, if screenshot is already 379 * being dismissed) 380 */ 381 @Override requestDismissal(ScreenshotEvent event)382 public void requestDismissal(ScreenshotEvent event) { 383 mViewProxy.requestDismissal(event); 384 } 385 386 @Override isPendingSharedTransition()387 public boolean isPendingSharedTransition() { 388 return mActionExecutor.isPendingSharedTransition(); 389 } 390 391 // Any cleanup needed when the service is being destroyed. 392 @Override onDestroy()393 public void onDestroy() { 394 removeWindow(); 395 releaseMediaPlayer(); 396 releaseContext(); 397 mBgExecutor.shutdown(); 398 } 399 400 /** 401 * Release the constructed window context. 402 */ releaseContext()403 private void releaseContext() { 404 mBroadcastDispatcher.unregisterReceiver(mCopyBroadcastReceiver); 405 mContext.release(); 406 } 407 releaseMediaPlayer()408 private void releaseMediaPlayer() { 409 if (mScreenshotSoundController == null) return; 410 mScreenshotSoundController.releaseScreenshotSoundAsync(); 411 } 412 413 /** 414 * Update resources on configuration change. Reinflate for theme/color changes. 415 */ reloadAssets()416 private void reloadAssets() { 417 if (DEBUG_UI) { 418 Log.d(TAG, "reloadAssets()"); 419 } 420 421 mMessageContainerController.setView(mViewProxy.getView()); 422 mViewProxy.setCallbacks(new ScreenshotShelfViewProxy.ScreenshotViewCallback() { 423 @Override 424 public void onUserInteraction() { 425 if (DEBUG_INPUT) { 426 Log.d(TAG, "onUserInteraction"); 427 } 428 mScreenshotHandler.resetTimeout(); 429 } 430 431 @Override 432 public void onDismiss() { 433 finishDismiss(); 434 } 435 436 @Override 437 public void onTouchOutside() { 438 // TODO(159460485): Remove this when focus is handled properly in the system 439 setWindowFocusable(false); 440 } 441 }); 442 443 if (DEBUG_WINDOW) { 444 Log.d(TAG, "setContentView: " + mViewProxy.getView()); 445 } 446 mWindow.setContentView(mViewProxy.getView()); 447 } 448 enqueueScrollCaptureRequest(UUID requestId, UserHandle owner)449 private void enqueueScrollCaptureRequest(UUID requestId, UserHandle owner) { 450 // Wait until this window is attached to request because it is 451 // the reference used to locate the target window (below). 452 withWindowAttached(() -> { 453 requestScrollCapture(requestId, owner); 454 mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback( 455 new ViewRootImpl.ActivityConfigCallback() { 456 @Override 457 public void onConfigurationChanged(Configuration overrideConfig, 458 int newDisplayId) { 459 if (mConfigChanges.applyNewConfig(mContext.getResources())) { 460 // Hide the scroll chip until we know it's available in this 461 // orientation 462 mActionsController.onScrollChipInvalidated(); 463 // Delay scroll capture eval a bit to allow the underlying activity 464 // to set up in the new orientation. 465 mScreenshotHandler.postDelayed( 466 () -> requestScrollCapture(requestId, owner), 150); 467 mViewProxy.updateInsets( 468 mWindowManager.getCurrentWindowMetrics().getWindowInsets()); 469 // Screenshot animation calculations won't be valid anymore, 470 // so just end 471 if (mScreenshotAnimation != null 472 && mScreenshotAnimation.isRunning()) { 473 mScreenshotAnimation.end(); 474 } 475 } 476 } 477 }); 478 }); 479 } 480 requestScrollCapture(UUID requestId, UserHandle owner)481 private void requestScrollCapture(UUID requestId, UserHandle owner) { 482 mScrollCaptureExecutor.requestScrollCapture( 483 mDisplay.getDisplayId(), 484 mWindow.getDecorView().getWindowToken(), 485 (response) -> { 486 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION, 487 0, response.getPackageName()); 488 mActionsController.onScrollChipReady(requestId, 489 () -> onScrollButtonClicked(owner, response)); 490 return Unit.INSTANCE; 491 } 492 ); 493 } 494 onScrollButtonClicked(UserHandle owner, ScrollCaptureResponse response)495 private void onScrollButtonClicked(UserHandle owner, ScrollCaptureResponse response) { 496 if (DEBUG_INPUT) { 497 Log.d(TAG, "scroll chip tapped"); 498 } 499 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED, 0, 500 response.getPackageName()); 501 Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplay.getDisplayId(), 502 getFullScreenRect()); 503 if (newScreenshot == null) { 504 Log.e(TAG, "Failed to capture current screenshot for scroll transition!"); 505 return; 506 } 507 // delay starting scroll capture to make sure scrim is up before the app moves 508 mViewProxy.prepareScrollingTransition(response, newScreenshot, mScreenshotTakenInPortrait, 509 () -> executeBatchScrollCapture(response, owner)); 510 } 511 executeBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner)512 private void executeBatchScrollCapture(ScrollCaptureResponse response, UserHandle owner) { 513 mScrollCaptureExecutor.executeBatchScrollCapture(response, 514 () -> { 515 final Intent intent = mActionIntentCreator.createLongScreenshotIntent(owner); 516 mContext.startActivity(intent); 517 }, 518 mViewProxy::restoreNonScrollingUi, 519 mViewProxy::startLongScreenshotTransition); 520 } 521 withWindowAttached(Runnable action)522 private void withWindowAttached(Runnable action) { 523 View decorView = mWindow.getDecorView(); 524 if (decorView.isAttachedToWindow()) { 525 action.run(); 526 } else { 527 decorView.getViewTreeObserver().addOnWindowAttachListener( 528 new ViewTreeObserver.OnWindowAttachListener() { 529 @Override 530 public void onWindowAttached() { 531 mAttachRequested = false; 532 decorView.getViewTreeObserver().removeOnWindowAttachListener(this); 533 action.run(); 534 } 535 536 @Override 537 public void onWindowDetached() { 538 } 539 }); 540 541 } 542 } 543 544 @MainThread attachWindow()545 private void attachWindow() { 546 View decorView = mWindow.getDecorView(); 547 if (decorView.isAttachedToWindow() || mAttachRequested) { 548 return; 549 } 550 if (DEBUG_WINDOW) { 551 Log.d(TAG, "attachWindow"); 552 } 553 mAttachRequested = true; 554 mWindowManager.addView(decorView, mWindowLayoutParams); 555 decorView.requestApplyInsets(); 556 557 ViewGroup layout = decorView.requireViewById(android.R.id.content); 558 layout.setClipChildren(false); 559 layout.setClipToPadding(false); 560 } 561 562 @Override removeWindow()563 public void removeWindow() { 564 final View decorView = mWindow.peekDecorView(); 565 if (decorView != null && decorView.isAttachedToWindow()) { 566 if (DEBUG_WINDOW) { 567 Log.d(TAG, "Removing screenshot window"); 568 } 569 mWindowManager.removeViewImmediate(decorView); 570 mDetachRequested = false; 571 } 572 if (mAttachRequested && !mDetachRequested) { 573 mDetachRequested = true; 574 withWindowAttached(this::removeWindow); 575 } 576 577 mViewProxy.stopInputListening(); 578 } 579 playCameraSoundIfNeeded()580 private void playCameraSoundIfNeeded() { 581 if (mScreenshotSoundController == null) return; 582 // the controller is not-null only on the default display controller 583 mScreenshotSoundController.playScreenshotSoundAsync(); 584 } 585 586 /** 587 * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on 588 * failure). 589 */ saveScreenshotAndToast(ScreenshotData screenshot, Consumer<Uri> finisher)590 private void saveScreenshotAndToast(ScreenshotData screenshot, Consumer<Uri> finisher) { 591 // Play the shutter sound to notify that we've taken a screenshot 592 playCameraSoundIfNeeded(); 593 594 saveScreenshotInBackground(screenshot, UUID.randomUUID(), finisher, result -> { 595 if (result.uri != null) { 596 mScreenshotHandler.post(() -> Toast.makeText(mContext, 597 R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show()); 598 } 599 }); 600 } 601 602 /** 603 * Starts the animation after taking the screenshot 604 */ startAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete)605 private void startAnimation(Rect screenRect, boolean showFlash, Runnable onAnimationComplete) { 606 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) { 607 mScreenshotAnimation.cancel(); 608 } 609 610 mScreenshotAnimation = 611 mViewProxy.createScreenshotDropInAnimation(screenRect, showFlash); 612 if (onAnimationComplete != null) { 613 mScreenshotAnimation.addListener(new AnimatorListenerAdapter() { 614 @Override 615 public void onAnimationEnd(Animator animation) { 616 super.onAnimationEnd(animation); 617 onAnimationComplete.run(); 618 } 619 }); 620 } 621 622 // Play the shutter sound to notify that we've taken a screenshot 623 playCameraSoundIfNeeded(); 624 625 if (DEBUG_ANIM) { 626 Log.d(TAG, "starting post-screenshot animation"); 627 } 628 mScreenshotAnimation.start(); 629 } 630 631 /** Reset screenshot view and then call onCompleteRunnable */ finishDismiss()632 private void finishDismiss() { 633 Log.d(TAG, "finishDismiss"); 634 mActionsController.endScreenshotSession(); 635 mScrollCaptureExecutor.close(); 636 if (mCurrentRequestCallback != null) { 637 mCurrentRequestCallback.onFinish(); 638 mCurrentRequestCallback = null; 639 } 640 mViewProxy.reset(); 641 removeWindow(); 642 mScreenshotHandler.cancelTimeout(); 643 } 644 saveScreenshotInBackground(ScreenshotData screenshot, UUID requestId, Consumer<Uri> finisher, Consumer<ImageExporter.Result> onResult)645 private void saveScreenshotInBackground(ScreenshotData screenshot, UUID requestId, 646 Consumer<Uri> finisher, Consumer<ImageExporter.Result> onResult) { 647 ListenableFuture<ImageExporter.Result> future = mImageExporter.export(mBgExecutor, 648 requestId, screenshot.getBitmap(), screenshot.getUserHandle(), 649 mDisplay.getDisplayId()); 650 future.addListener(() -> { 651 try { 652 ImageExporter.Result result = future.get(); 653 Log.d(TAG, "Saved screenshot: " + result); 654 logScreenshotResultStatus(result.uri, screenshot.getUserHandle()); 655 onResult.accept(result); 656 if (DEBUG_CALLBACK) { 657 Log.d(TAG, "finished background processing, Calling (Consumer<Uri>) " 658 + "finisher.accept(\"" + result.uri + "\""); 659 } 660 finisher.accept(result.uri); 661 } catch (Exception e) { 662 Log.d(TAG, "Failed to store screenshot", e); 663 if (DEBUG_CALLBACK) { 664 Log.d(TAG, "Calling (Consumer<Uri>) finisher.accept(null)"); 665 } 666 finisher.accept(null); 667 } 668 }, mMainExecutor); 669 } 670 671 /** 672 * Logs success/failure of the screenshot saving task, and shows an error if it failed. 673 */ logScreenshotResultStatus(Uri uri, UserHandle owner)674 private void logScreenshotResultStatus(Uri uri, UserHandle owner) { 675 if (uri == null) { 676 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName); 677 mNotificationsController.notifyScreenshotError( 678 R.string.screenshot_failed_to_save_text); 679 } else { 680 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName); 681 if (mUserManager.isManagedProfile(owner.getIdentifier())) { 682 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE, 0, 683 mPackageName); 684 } 685 } 686 } 687 isUserSetupComplete(UserHandle owner)688 private boolean isUserSetupComplete(UserHandle owner) { 689 return Settings.Secure.getInt(mContext.createContextAsUser(owner, 0) 690 .getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1; 691 } 692 693 /** 694 * Updates the window focusability. If the window is already showing, then it updates the 695 * window immediately, otherwise the layout params will be applied when the window is next 696 * shown. 697 */ setWindowFocusable(boolean focusable)698 private void setWindowFocusable(boolean focusable) { 699 if (DEBUG_WINDOW) { 700 Log.d(TAG, "setWindowFocusable: " + focusable); 701 } 702 int flags = mWindowLayoutParams.flags; 703 if (focusable) { 704 mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 705 } else { 706 mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; 707 } 708 if (mWindowLayoutParams.flags == flags) { 709 if (DEBUG_WINDOW) { 710 Log.d(TAG, "setWindowFocusable: skipping, already " + focusable); 711 } 712 return; 713 } 714 final View decorView = mWindow.peekDecorView(); 715 if (decorView != null && decorView.isAttachedToWindow()) { 716 mWindowManager.updateViewLayout(decorView, mWindowLayoutParams); 717 } 718 } 719 getFullScreenRect()720 private Rect getFullScreenRect() { 721 DisplayMetrics displayMetrics = new DisplayMetrics(); 722 mDisplay.getRealMetrics(displayMetrics); 723 return new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels); 724 } 725 726 /** Does the aspect ratio of the bitmap with insets removed match the bounds. */ aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds)727 private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, 728 Rect screenBounds) { 729 int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right; 730 int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom; 731 732 if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0 733 || bitmap.getHeight() == 0) { 734 if (DEBUG_UI) { 735 Log.e(TAG, "Provided bitmap and insets create degenerate region: " 736 + bitmap.getWidth() + "x" + bitmap.getHeight() + " " + bitmapInsets); 737 } 738 return false; 739 } 740 741 float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight; 742 float boundsAspect = ((float) screenBounds.width()) / screenBounds.height(); 743 744 boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f; 745 if (DEBUG_UI) { 746 Log.d(TAG, "aspectRatiosMatch: don't match bitmap: " + insettedBitmapAspect 747 + ", bounds: " + boundsAspect); 748 } 749 return matchWithinTolerance; 750 } 751 752 /** Injectable factory to create screenshot controller instances for a specific display. */ 753 @AssistedFactory 754 public interface Factory extends InteractiveScreenshotHandler.Factory { 755 /** 756 * Creates an instance of the controller for that specific display. 757 * 758 * @param display display to capture 759 */ 760 LegacyScreenshotController create(Display display); 761 } 762 } 763