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