• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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