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.Display.DEFAULT_DISPLAY;
21 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
22 import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT;
23 
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_DISMISS;
27 import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT;
28 import static com.android.systemui.screenshot.LogConfig.DEBUG_UI;
29 import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW;
30 import static com.android.systemui.screenshot.LogConfig.logTag;
31 
32 import static java.util.Objects.requireNonNull;
33 
34 import android.animation.Animator;
35 import android.animation.AnimatorListenerAdapter;
36 import android.annotation.MainThread;
37 import android.annotation.Nullable;
38 import android.app.ActivityManager;
39 import android.app.ActivityOptions;
40 import android.app.ExitTransitionCoordinator;
41 import android.app.ExitTransitionCoordinator.ExitTransitionCallbacks;
42 import android.app.Notification;
43 import android.content.ComponentName;
44 import android.content.Context;
45 import android.content.Intent;
46 import android.content.pm.ActivityInfo;
47 import android.graphics.Bitmap;
48 import android.graphics.Insets;
49 import android.graphics.PixelFormat;
50 import android.graphics.Rect;
51 import android.hardware.display.DisplayManager;
52 import android.media.MediaActionSound;
53 import android.net.Uri;
54 import android.os.Bundle;
55 import android.os.Handler;
56 import android.os.IBinder;
57 import android.os.Looper;
58 import android.os.Message;
59 import android.os.RemoteException;
60 import android.provider.Settings;
61 import android.util.DisplayMetrics;
62 import android.util.Log;
63 import android.util.Pair;
64 import android.view.Display;
65 import android.view.DisplayAddress;
66 import android.view.IRemoteAnimationFinishedCallback;
67 import android.view.IRemoteAnimationRunner;
68 import android.view.KeyEvent;
69 import android.view.LayoutInflater;
70 import android.view.RemoteAnimationAdapter;
71 import android.view.RemoteAnimationTarget;
72 import android.view.ScrollCaptureResponse;
73 import android.view.SurfaceControl;
74 import android.view.View;
75 import android.view.ViewTreeObserver;
76 import android.view.Window;
77 import android.view.WindowInsets;
78 import android.view.WindowManager;
79 import android.view.WindowManagerGlobal;
80 import android.view.accessibility.AccessibilityEvent;
81 import android.view.accessibility.AccessibilityManager;
82 import android.widget.Toast;
83 import android.window.WindowContext;
84 
85 import com.android.internal.app.ChooserActivity;
86 import com.android.internal.logging.UiEventLogger;
87 import com.android.internal.policy.PhoneWindow;
88 import com.android.settingslib.applications.InterestingConfigChanges;
89 import com.android.systemui.R;
90 import com.android.systemui.dagger.qualifiers.Main;
91 import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
92 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
93 
94 import com.google.common.util.concurrent.ListenableFuture;
95 
96 import java.util.List;
97 import java.util.concurrent.CancellationException;
98 import java.util.concurrent.ExecutionException;
99 import java.util.concurrent.Executor;
100 import java.util.concurrent.ExecutorService;
101 import java.util.concurrent.Executors;
102 import java.util.concurrent.Future;
103 import java.util.function.Consumer;
104 import java.util.function.Supplier;
105 
106 import javax.inject.Inject;
107 
108 /**
109  * Controls the state and flow for screenshots.
110  */
111 public class ScreenshotController {
112     private static final String TAG = logTag(ScreenshotController.class);
113 
114     private ScrollCaptureResponse mLastScrollCaptureResponse;
115     private ListenableFuture<ScrollCaptureResponse> mLastScrollCaptureRequest;
116 
117     /**
118      * This is effectively a no-op, but we need something non-null to pass in, in order to
119      * successfully override the pending activity entrance animation.
120      */
121     static final IRemoteAnimationRunner.Stub SCREENSHOT_REMOTE_RUNNER =
122             new IRemoteAnimationRunner.Stub() {
123                 @Override
124                 public void onAnimationStart(
125                         @WindowManager.TransitionOldType int transit,
126                         RemoteAnimationTarget[] apps,
127                         RemoteAnimationTarget[] wallpapers,
128                         RemoteAnimationTarget[] nonApps,
129                         final IRemoteAnimationFinishedCallback finishedCallback) {
130                     try {
131                         finishedCallback.onAnimationFinished();
132                     } catch (RemoteException e) {
133                         Log.e(TAG, "Error finishing screenshot remote animation", e);
134                     }
135                 }
136 
137                 @Override
138                 public void onAnimationCancelled() {
139                 }
140             };
141 
142     /**
143      * POD used in the AsyncTask which saves an image in the background.
144      */
145     static class SaveImageInBackgroundData {
146         public Bitmap image;
147         public Consumer<Uri> finisher;
148         public ScreenshotController.ActionsReadyListener mActionsReadyListener;
149         public ScreenshotController.QuickShareActionReadyListener mQuickShareActionsReadyListener;
150 
clearImage()151         void clearImage() {
152             image = null;
153         }
154     }
155 
156     /**
157      * Structure returned by the SaveImageInBackgroundTask
158      */
159     static class SavedImageData {
160         public Uri uri;
161         public Supplier<ActionTransition> shareTransition;
162         public Supplier<ActionTransition> editTransition;
163         public Notification.Action deleteAction;
164         public List<Notification.Action> smartActions;
165         public Notification.Action quickShareAction;
166 
167         /**
168          * POD for shared element transition.
169          */
170         static class ActionTransition {
171             public Bundle bundle;
172             public Notification.Action action;
173             public Runnable onCancelRunnable;
174         }
175 
176         /**
177          * Used to reset the return data on error
178          */
reset()179         public void reset() {
180             uri = null;
181             shareTransition = null;
182             editTransition = null;
183             deleteAction = null;
184             smartActions = null;
185             quickShareAction = null;
186         }
187     }
188 
189     /**
190      * Structure returned by the QueryQuickShareInBackgroundTask
191      */
192     static class QuickShareData {
193         public Notification.Action quickShareAction;
194 
195         /**
196          * Used to reset the return data on error
197          */
reset()198         public void reset() {
199             quickShareAction = null;
200         }
201     }
202 
203     interface ActionsReadyListener {
onActionsReady(ScreenshotController.SavedImageData imageData)204         void onActionsReady(ScreenshotController.SavedImageData imageData);
205     }
206 
207     interface QuickShareActionReadyListener {
onActionsReady(ScreenshotController.QuickShareData quickShareData)208         void onActionsReady(ScreenshotController.QuickShareData quickShareData);
209     }
210 
211     interface TransitionDestination {
212         /**
213          * Allows the long screenshot activity to call back with a destination location (the bounds
214          * on screen of the destination for the transitioning view) and a Runnable to be run once
215          * the transition animation is complete.
216          */
setTransitionDestination(Rect transitionDestination, Runnable onTransitionEnd)217         void setTransitionDestination(Rect transitionDestination, Runnable onTransitionEnd);
218     }
219 
220     // These strings are used for communicating the action invoked to
221     // ScreenshotNotificationSmartActionsProvider.
222     static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type";
223     static final String EXTRA_ID = "android:screenshot_id";
224     static final String ACTION_TYPE_DELETE = "Delete";
225     static final String ACTION_TYPE_SHARE = "Share";
226     static final String ACTION_TYPE_EDIT = "Edit";
227     static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled";
228     static final String EXTRA_OVERRIDE_TRANSITION = "android:screenshot_override_transition";
229     static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent";
230 
231     static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id";
232     static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification";
233     static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip";
234 
235 
236     private static final int MESSAGE_CORNER_TIMEOUT = 2;
237     private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;
238 
239     // From WizardManagerHelper.java
240     private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete";
241 
242     private final WindowContext mContext;
243     private final ScreenshotNotificationsController mNotificationsController;
244     private final ScreenshotSmartActions mScreenshotSmartActions;
245     private final UiEventLogger mUiEventLogger;
246     private final ImageExporter mImageExporter;
247     private final Executor mMainExecutor;
248     private final ExecutorService mBgExecutor;
249 
250     private final WindowManager mWindowManager;
251     private final WindowManager.LayoutParams mWindowLayoutParams;
252     private final AccessibilityManager mAccessibilityManager;
253     private final MediaActionSound mCameraSound;
254     private final ScrollCaptureClient mScrollCaptureClient;
255     private final PhoneWindow mWindow;
256     private final DisplayManager mDisplayManager;
257     private final ScrollCaptureController mScrollCaptureController;
258     private final LongScreenshotData mLongScreenshotHolder;
259     private final boolean mIsLowRamDevice;
260 
261     private ScreenshotView mScreenshotView;
262     private Bitmap mScreenBitmap;
263     private SaveImageInBackgroundTask mSaveInBgTask;
264     private boolean mScreenshotTakenInPortrait;
265     private boolean mBlockAttach;
266 
267     private Animator mScreenshotAnimation;
268     private RequestCallback mCurrentRequestCallback;
269     private String mPackageName = "";
270 
271     private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) {
272         @Override
273         public void handleMessage(Message msg) {
274             switch (msg.what) {
275                 case MESSAGE_CORNER_TIMEOUT:
276                     if (DEBUG_UI) {
277                         Log.d(TAG, "Corner timeout hit");
278                     }
279                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT, 0,
280                             mPackageName);
281                     ScreenshotController.this.dismissScreenshot(false);
282                     break;
283                 default:
284                     break;
285             }
286         }
287     };
288 
289     /** Tracks config changes that require re-creating UI */
290     private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges(
291             ActivityInfo.CONFIG_ORIENTATION
292                     | ActivityInfo.CONFIG_LAYOUT_DIRECTION
293                     | ActivityInfo.CONFIG_LOCALE
294                     | ActivityInfo.CONFIG_UI_MODE
295                     | ActivityInfo.CONFIG_SCREEN_LAYOUT
296                     | ActivityInfo.CONFIG_ASSETS_PATHS);
297 
298     @Inject
ScreenshotController( Context context, ScreenshotSmartActions screenshotSmartActions, ScreenshotNotificationsController screenshotNotificationsController, ScrollCaptureClient scrollCaptureClient, UiEventLogger uiEventLogger, ImageExporter imageExporter, @Main Executor mainExecutor, ScrollCaptureController scrollCaptureController, LongScreenshotData longScreenshotHolder, ActivityManager activityManager)299     ScreenshotController(
300             Context context,
301             ScreenshotSmartActions screenshotSmartActions,
302             ScreenshotNotificationsController screenshotNotificationsController,
303             ScrollCaptureClient scrollCaptureClient,
304             UiEventLogger uiEventLogger,
305             ImageExporter imageExporter,
306             @Main Executor mainExecutor,
307             ScrollCaptureController scrollCaptureController,
308             LongScreenshotData longScreenshotHolder,
309             ActivityManager activityManager) {
310         mScreenshotSmartActions = screenshotSmartActions;
311         mNotificationsController = screenshotNotificationsController;
312         mScrollCaptureClient = scrollCaptureClient;
313         mUiEventLogger = uiEventLogger;
314         mImageExporter = imageExporter;
315         mMainExecutor = mainExecutor;
316         mScrollCaptureController = scrollCaptureController;
317         mLongScreenshotHolder = longScreenshotHolder;
318         mIsLowRamDevice = activityManager.isLowRamDevice();
319         mBgExecutor = Executors.newSingleThreadExecutor();
320 
321         mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
322         final Context displayContext = context.createDisplayContext(getDefaultDisplay());
323         mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
324         mWindowManager = mContext.getSystemService(WindowManager.class);
325 
326         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
327 
328         // Setup the window that we are going to use
329         mWindowLayoutParams = new WindowManager.LayoutParams(
330                 MATCH_PARENT, MATCH_PARENT, /* xpos */ 0, /* ypos */ 0, TYPE_SCREENSHOT,
331                 WindowManager.LayoutParams.FLAG_FULLSCREEN
332                         | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
333                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
334                         | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
335                         | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
336                         | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
337                 PixelFormat.TRANSLUCENT);
338         mWindowLayoutParams.setTitle("ScreenshotAnimation");
339         mWindowLayoutParams.layoutInDisplayCutoutMode =
340                 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
341         mWindowLayoutParams.setFitInsetsTypes(0);
342         // This is needed to let touches pass through outside the touchable areas
343         mWindowLayoutParams.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
344 
345         mWindow = new PhoneWindow(mContext);
346         mWindow.setWindowManager(mWindowManager, null, null);
347         mWindow.requestFeature(Window.FEATURE_NO_TITLE);
348         mWindow.requestFeature(Window.FEATURE_ACTIVITY_TRANSITIONS);
349         mWindow.setBackgroundDrawableResource(android.R.color.transparent);
350 
351         mConfigChanges.applyNewConfig(context.getResources());
352         reloadAssets();
353 
354         // Setup the Camera shutter sound
355         mCameraSound = new MediaActionSound();
356         mCameraSound.load(MediaActionSound.SHUTTER_CLICK);
357     }
358 
takeScreenshotFullscreen(ComponentName topComponent, Consumer<Uri> finisher, RequestCallback requestCallback)359     void takeScreenshotFullscreen(ComponentName topComponent, Consumer<Uri> finisher,
360             RequestCallback requestCallback) {
361         mCurrentRequestCallback = requestCallback;
362         DisplayMetrics displayMetrics = new DisplayMetrics();
363         getDefaultDisplay().getRealMetrics(displayMetrics);
364         takeScreenshotInternal(
365                 topComponent, finisher,
366                 new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
367     }
368 
handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, Insets visibleInsets, int taskId, int userId, ComponentName topComponent, Consumer<Uri> finisher, RequestCallback requestCallback)369     void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds,
370             Insets visibleInsets, int taskId, int userId, ComponentName topComponent,
371             Consumer<Uri> finisher, RequestCallback requestCallback) {
372         // TODO: use task Id, userId, topComponent for smart handler
373 
374         if (screenshot == null) {
375             Log.e(TAG, "Got null bitmap from screenshot message");
376             mNotificationsController.notifyScreenshotError(
377                     R.string.screenshot_failed_to_capture_text);
378             requestCallback.reportError();
379             return;
380         }
381 
382         boolean showFlash = false;
383         if (!aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) {
384             showFlash = true;
385             visibleInsets = Insets.NONE;
386             screenshotScreenBounds.set(0, 0, screenshot.getWidth(), screenshot.getHeight());
387         }
388         mCurrentRequestCallback = requestCallback;
389         saveScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, topComponent,
390                 showFlash);
391     }
392 
393     /**
394      * Displays a screenshot selector
395      */
takeScreenshotPartial(ComponentName topComponent, final Consumer<Uri> finisher, RequestCallback requestCallback)396     void takeScreenshotPartial(ComponentName topComponent,
397             final Consumer<Uri> finisher, RequestCallback requestCallback) {
398         mScreenshotView.reset();
399         mCurrentRequestCallback = requestCallback;
400 
401         attachWindow();
402         mWindow.setContentView(mScreenshotView);
403         mScreenshotView.requestApplyInsets();
404 
405         mScreenshotView.takePartialScreenshot(
406                 rect -> takeScreenshotInternal(topComponent, finisher, rect));
407     }
408 
409     /**
410      * Clears current screenshot
411      */
dismissScreenshot(boolean immediate)412     void dismissScreenshot(boolean immediate) {
413         if (DEBUG_DISMISS) {
414             Log.d(TAG, "dismissScreenshot(immediate=" + immediate + ")");
415         }
416         // If we're already animating out, don't restart the animation
417         // (but do obey an immediate dismissal)
418         if (!immediate && mScreenshotView.isDismissing()) {
419             if (DEBUG_DISMISS) {
420                 Log.v(TAG, "Already dismissing, ignoring duplicate command");
421             }
422             return;
423         }
424         cancelTimeout();
425         if (immediate) {
426             finishDismiss();
427         } else {
428             mScreenshotView.animateDismissal();
429         }
430 
431         if (mLastScrollCaptureResponse != null) {
432             mLastScrollCaptureResponse.close();
433             mLastScrollCaptureResponse = null;
434         }
435     }
436 
isPendingSharedTransition()437     boolean isPendingSharedTransition() {
438         return mScreenshotView.isPendingSharedTransition();
439     }
440 
441     /**
442      * Release the constructed window context.
443      */
releaseContext()444     void releaseContext() {
445         mContext.release();
446         mCameraSound.release();
447         mBgExecutor.shutdownNow();
448     }
449 
450     /**
451      * Update resources on configuration change. Reinflate for theme/color changes.
452      */
reloadAssets()453     private void reloadAssets() {
454         if (DEBUG_UI) {
455             Log.d(TAG, "reloadAssets()");
456         }
457 
458         // Inflate the screenshot layout
459         mScreenshotView = (ScreenshotView)
460                 LayoutInflater.from(mContext).inflate(R.layout.global_screenshot, null);
461         mScreenshotView.init(mUiEventLogger, new ScreenshotView.ScreenshotViewCallback() {
462             @Override
463             public void onUserInteraction() {
464                 resetTimeout();
465             }
466 
467             @Override
468             public void onDismiss() {
469                 finishDismiss();
470             }
471 
472             @Override
473             public void onTouchOutside() {
474                 // TODO(159460485): Remove this when focus is handled properly in the system
475                 setWindowFocusable(false);
476             }
477         });
478 
479         mScreenshotView.setOnKeyListener((v, keyCode, event) -> {
480             if (keyCode == KeyEvent.KEYCODE_BACK) {
481                 if (DEBUG_INPUT) {
482                     Log.d(TAG, "onKeyEvent: KeyEvent.KEYCODE_BACK");
483                 }
484                 dismissScreenshot(false);
485                 return true;
486             }
487             return false;
488         });
489 
490         if (DEBUG_WINDOW) {
491             Log.d(TAG, "adding OnComputeInternalInsetsListener");
492         }
493         mScreenshotView.getViewTreeObserver().addOnComputeInternalInsetsListener(mScreenshotView);
494     }
495 
496     /**
497      * Takes a screenshot of the current display and shows an animation.
498      */
takeScreenshotInternal(ComponentName topComponent, Consumer<Uri> finisher, Rect crop)499     private void takeScreenshotInternal(ComponentName topComponent, Consumer<Uri> finisher,
500             Rect crop) {
501         mScreenshotTakenInPortrait =
502                 mContext.getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
503 
504         // copy the input Rect, since SurfaceControl.screenshot can mutate it
505         Rect screenRect = new Rect(crop);
506         Bitmap screenshot = captureScreenshot(crop);
507 
508         if (screenshot == null) {
509             Log.e(TAG, "takeScreenshotInternal: Screenshot bitmap was null");
510             mNotificationsController.notifyScreenshotError(
511                     R.string.screenshot_failed_to_capture_text);
512             if (mCurrentRequestCallback != null) {
513                 mCurrentRequestCallback.reportError();
514             }
515             return;
516         }
517 
518         saveScreenshot(screenshot, finisher, screenRect, Insets.NONE, topComponent, true);
519     }
520 
captureScreenshot(Rect crop)521     private Bitmap captureScreenshot(Rect crop) {
522         int width = crop.width();
523         int height = crop.height();
524         Bitmap screenshot = null;
525         final Display display = getDefaultDisplay();
526         final DisplayAddress address = display.getAddress();
527         if (!(address instanceof DisplayAddress.Physical)) {
528             Log.e(TAG, "Skipping Screenshot - Default display does not have a physical address: "
529                     + display);
530         } else {
531             final DisplayAddress.Physical physicalAddress = (DisplayAddress.Physical) address;
532 
533             final IBinder displayToken = SurfaceControl.getPhysicalDisplayToken(
534                     physicalAddress.getPhysicalDisplayId());
535             final SurfaceControl.DisplayCaptureArgs captureArgs =
536                     new SurfaceControl.DisplayCaptureArgs.Builder(displayToken)
537                             .setSourceCrop(crop)
538                             .setSize(width, height)
539                             .build();
540             final SurfaceControl.ScreenshotHardwareBuffer screenshotBuffer =
541                     SurfaceControl.captureDisplay(captureArgs);
542             screenshot = screenshotBuffer == null ? null : screenshotBuffer.asBitmap();
543         }
544         return screenshot;
545     }
546 
saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, ComponentName topComponent, boolean showFlash)547     private void saveScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
548             Insets screenInsets, ComponentName topComponent, boolean showFlash) {
549         if (mAccessibilityManager.isEnabled()) {
550             AccessibilityEvent event =
551                     new AccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
552             event.setContentDescription(
553                     mContext.getResources().getString(R.string.screenshot_saving_title));
554             mAccessibilityManager.sendAccessibilityEvent(event);
555         }
556 
557 
558         if (mScreenshotView.isAttachedToWindow()) {
559             // if we didn't already dismiss for another reason
560             if (!mScreenshotView.isDismissing()) {
561                 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_REENTERED, 0, mPackageName);
562             }
563             if (DEBUG_WINDOW) {
564                 Log.d(TAG, "saveScreenshot: screenshotView is already attached, resetting. "
565                         + "(dismissing=" + mScreenshotView.isDismissing() + ")");
566             }
567             mScreenshotView.reset();
568         }
569         mPackageName = topComponent == null ? "" : topComponent.getPackageName();
570         mScreenshotView.setPackageName(mPackageName);
571 
572         mScreenshotView.updateOrientation(
573                 mWindowManager.getCurrentWindowMetrics().getWindowInsets());
574 
575         mScreenBitmap = screenshot;
576 
577         if (!isUserSetupComplete()) {
578             Log.w(TAG, "User setup not complete, displaying toast only");
579             // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
580             // and sharing shouldn't be exposed to the user.
581             saveScreenshotAndToast(finisher);
582             return;
583         }
584 
585         // Optimizations
586         mScreenBitmap.setHasAlpha(false);
587         mScreenBitmap.prepareToDraw();
588 
589         saveScreenshotInWorkerThread(finisher, this::showUiOnActionsReady,
590                 this::showUiOnQuickShareActionReady);
591 
592         // The window is focusable by default
593         setWindowFocusable(true);
594 
595         // Wait until this window is attached to request because it is
596         // the reference used to locate the target window (below).
597         withWindowAttached(() -> {
598             requestScrollCapture();
599             mWindow.peekDecorView().getViewRootImpl().setActivityConfigCallback(
600                     (overrideConfig, newDisplayId) -> {
601                         if (mConfigChanges.applyNewConfig(mContext.getResources())) {
602                             // Hide the scroll chip until we know it's available in this orientation
603                             mScreenshotView.hideScrollChip();
604                             // Delay scroll capture eval a bit to allow the underlying activity
605                             // to set up in the new orientation.
606                             mScreenshotHandler.postDelayed(this::requestScrollCapture, 150);
607                             mScreenshotView.updateInsets(
608                                     mWindowManager.getCurrentWindowMetrics().getWindowInsets());
609                             // screenshot animation calculations won't be valid anymore, so just end
610                             if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
611                                 mScreenshotAnimation.end();
612                             }
613                         }
614                     });
615         });
616 
617         attachWindow();
618         mScreenshotView.getViewTreeObserver().addOnPreDrawListener(
619                 new ViewTreeObserver.OnPreDrawListener() {
620                     @Override
621                     public boolean onPreDraw() {
622                         if (DEBUG_WINDOW) {
623                             Log.d(TAG, "onPreDraw: startAnimation");
624                         }
625                         mScreenshotView.getViewTreeObserver().removeOnPreDrawListener(this);
626                         startAnimation(screenRect, showFlash);
627                         return true;
628                     }
629                 });
630         mScreenshotView.setScreenshot(mScreenBitmap, screenInsets);
631         if (DEBUG_WINDOW) {
632             Log.d(TAG, "setContentView: " + mScreenshotView);
633         }
634         setContentView(mScreenshotView);
635         // ignore system bar insets for the purpose of window layout
636         mWindow.getDecorView().setOnApplyWindowInsetsListener(
637                 (v, insets) -> WindowInsets.CONSUMED);
638         cancelTimeout(); // restarted after animation
639     }
640 
requestScrollCapture()641     private void requestScrollCapture() {
642         if (!allowLongScreenshots()) {
643             Log.d(TAG, "Long screenshots not supported on this device");
644             return;
645         }
646         mScrollCaptureClient.setHostWindowToken(mWindow.getDecorView().getWindowToken());
647         if (mLastScrollCaptureRequest != null) {
648             mLastScrollCaptureRequest.cancel(true);
649         }
650         mLastScrollCaptureRequest = mScrollCaptureClient.request(DEFAULT_DISPLAY);
651         mLastScrollCaptureRequest.addListener(() ->
652                 onScrollCaptureResponseReady(mLastScrollCaptureRequest), mMainExecutor);
653     }
654 
onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture)655     private void onScrollCaptureResponseReady(Future<ScrollCaptureResponse> responseFuture) {
656         try {
657             if (mLastScrollCaptureResponse != null) {
658                 mLastScrollCaptureResponse.close();
659             }
660             mLastScrollCaptureResponse = responseFuture.get();
661             if (!mLastScrollCaptureResponse.isConnected()) {
662                 // No connection means that the target window wasn't found
663                 // or that it cannot support scroll capture.
664                 Log.d(TAG, "ScrollCapture: " + mLastScrollCaptureResponse.getDescription() + " ["
665                         + mLastScrollCaptureResponse.getWindowTitle() + "]");
666                 return;
667             }
668             Log.d(TAG, "ScrollCapture: connected to window ["
669                     + mLastScrollCaptureResponse.getWindowTitle() + "]");
670 
671             final ScrollCaptureResponse response = mLastScrollCaptureResponse;
672             mScreenshotView.showScrollChip(response.getPackageName(), /* onClick */ () -> {
673                 DisplayMetrics displayMetrics = new DisplayMetrics();
674                 getDefaultDisplay().getRealMetrics(displayMetrics);
675                 Bitmap newScreenshot = captureScreenshot(
676                         new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
677 
678                 mScreenshotView.prepareScrollingTransition(response, mScreenBitmap, newScreenshot,
679                         mScreenshotTakenInPortrait);
680                 // delay starting scroll capture to make sure the scrim is up before the app moves
681                 mScreenshotView.post(() -> {
682                     // Clear the reference to prevent close() in dismissScreenshot
683                     mLastScrollCaptureResponse = null;
684                     final ListenableFuture<ScrollCaptureController.LongScreenshot> future =
685                             mScrollCaptureController.run(response);
686                     future.addListener(() -> {
687                         ScrollCaptureController.LongScreenshot longScreenshot;
688 
689                         try {
690                             longScreenshot = future.get();
691                         } catch (CancellationException
692                                 | InterruptedException
693                                 | ExecutionException e) {
694                             Log.e(TAG, "Exception", e);
695                             mScreenshotView.restoreNonScrollingUi();
696                             return;
697                         }
698 
699                         if (longScreenshot.getHeight() == 0) {
700                             mScreenshotView.restoreNonScrollingUi();
701                             return;
702                         }
703 
704                         mLongScreenshotHolder.setLongScreenshot(longScreenshot);
705                         mLongScreenshotHolder.setTransitionDestinationCallback(
706                                 (transitionDestination, onTransitionEnd) ->
707                                         mScreenshotView.startLongScreenshotTransition(
708                                                 transitionDestination, onTransitionEnd,
709                                                 longScreenshot));
710 
711                         final Intent intent = new Intent(mContext, LongScreenshotActivity.class);
712                         intent.setFlags(
713                                 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
714 
715                         mContext.startActivity(intent,
716                                 ActivityOptions.makeCustomAnimation(mContext, 0, 0).toBundle());
717                         RemoteAnimationAdapter runner = new RemoteAnimationAdapter(
718                                 SCREENSHOT_REMOTE_RUNNER, 0, 0);
719                         try {
720                             WindowManagerGlobal.getWindowManagerService()
721                                     .overridePendingAppTransitionRemote(runner, DEFAULT_DISPLAY);
722                         } catch (Exception e) {
723                             Log.e(TAG, "Error overriding screenshot app transition", e);
724                         }
725                     }, mMainExecutor);
726                 });
727             });
728         } catch (CancellationException e) {
729             // Ignore
730         } catch (InterruptedException | ExecutionException e) {
731             Log.e(TAG, "requestScrollCapture failed", e);
732         }
733     }
734 
withWindowAttached(Runnable action)735     private void withWindowAttached(Runnable action) {
736         View decorView = mWindow.getDecorView();
737         if (decorView.isAttachedToWindow()) {
738             action.run();
739         } else {
740             decorView.getViewTreeObserver().addOnWindowAttachListener(
741                     new ViewTreeObserver.OnWindowAttachListener() {
742                         @Override
743                         public void onWindowAttached() {
744                             mBlockAttach = false;
745                             decorView.getViewTreeObserver().removeOnWindowAttachListener(this);
746                             action.run();
747                         }
748 
749                         @Override
750                         public void onWindowDetached() {
751                         }
752                     });
753 
754         }
755     }
756 
setContentView(View contentView)757     private void setContentView(View contentView) {
758         mWindow.setContentView(contentView);
759     }
760 
761     @MainThread
attachWindow()762     private void attachWindow() {
763         View decorView = mWindow.getDecorView();
764         if (decorView.isAttachedToWindow() || mBlockAttach) {
765             return;
766         }
767         if (DEBUG_WINDOW) {
768             Log.d(TAG, "attachWindow");
769         }
770         mBlockAttach = true;
771         mWindowManager.addView(decorView, mWindowLayoutParams);
772         decorView.requestApplyInsets();
773     }
774 
removeWindow()775     void removeWindow() {
776         final View decorView = mWindow.peekDecorView();
777         if (decorView != null && decorView.isAttachedToWindow()) {
778             if (DEBUG_WINDOW) {
779                 Log.d(TAG, "Removing screenshot window");
780             }
781             mWindowManager.removeViewImmediate(decorView);
782         }
783         // Ensure that we remove the input monitor
784         if (mScreenshotView != null) {
785             mScreenshotView.stopInputListening();
786         }
787     }
788 
789     /**
790      * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
791      * failure).
792      */
saveScreenshotAndToast(Consumer<Uri> finisher)793     private void saveScreenshotAndToast(Consumer<Uri> finisher) {
794         // Play the shutter sound to notify that we've taken a screenshot
795         mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
796 
797         saveScreenshotInWorkerThread(
798                 /* onComplete */ finisher,
799                 /* actionsReadyListener */ imageData -> {
800                     if (DEBUG_CALLBACK) {
801                         Log.d(TAG, "returning URI to finisher (Consumer<URI>): " + imageData.uri);
802                     }
803                     finisher.accept(imageData.uri);
804                     if (imageData.uri == null) {
805                         mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName);
806                         mNotificationsController.notifyScreenshotError(
807                                 R.string.screenshot_failed_to_save_text);
808                     } else {
809                         mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
810                         mScreenshotHandler.post(() -> Toast.makeText(mContext,
811                                 R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show());
812                     }
813                 },
814                 null);
815     }
816 
817     /**
818      * Starts the animation after taking the screenshot
819      */
startAnimation(Rect screenRect, boolean showFlash)820     private void startAnimation(Rect screenRect, boolean showFlash) {
821         if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
822             mScreenshotAnimation.cancel();
823         }
824 
825         mScreenshotAnimation =
826                 mScreenshotView.createScreenshotDropInAnimation(screenRect, showFlash);
827 
828         // Play the shutter sound to notify that we've taken a screenshot
829         mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
830 
831         if (DEBUG_ANIM) {
832             Log.d(TAG, "starting post-screenshot animation");
833         }
834         mScreenshotAnimation.start();
835     }
836 
837     /** Reset screenshot view and then call onCompleteRunnable */
finishDismiss()838     private void finishDismiss() {
839         if (DEBUG_UI) {
840             Log.d(TAG, "finishDismiss");
841         }
842         cancelTimeout();
843         removeWindow();
844         mScreenshotView.reset();
845         if (mCurrentRequestCallback != null) {
846             mCurrentRequestCallback.onFinish();
847             mCurrentRequestCallback = null;
848         }
849     }
850 
851     /**
852      * Creates a new worker thread and saves the screenshot to the media store.
853      */
saveScreenshotInWorkerThread(Consumer<Uri> finisher, @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener, @Nullable ScreenshotController.QuickShareActionReadyListener quickShareActionsReadyListener)854     private void saveScreenshotInWorkerThread(Consumer<Uri> finisher,
855             @Nullable ScreenshotController.ActionsReadyListener actionsReadyListener,
856             @Nullable ScreenshotController.QuickShareActionReadyListener
857                     quickShareActionsReadyListener) {
858         ScreenshotController.SaveImageInBackgroundData
859                 data = new ScreenshotController.SaveImageInBackgroundData();
860         data.image = mScreenBitmap;
861         data.finisher = finisher;
862         data.mActionsReadyListener = actionsReadyListener;
863         data.mQuickShareActionsReadyListener = quickShareActionsReadyListener;
864 
865         if (mSaveInBgTask != null) {
866             // just log success/failure for the pre-existing screenshot
867             mSaveInBgTask.setActionsReadyListener(this::logSuccessOnActionsReady);
868         }
869 
870         mSaveInBgTask = new SaveImageInBackgroundTask(mContext, mImageExporter,
871                 mScreenshotSmartActions, data, getActionTransitionSupplier());
872         mSaveInBgTask.execute();
873     }
874 
cancelTimeout()875     private void cancelTimeout() {
876         mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT);
877     }
878 
resetTimeout()879     private void resetTimeout() {
880         cancelTimeout();
881 
882         AccessibilityManager accessibilityManager = (AccessibilityManager)
883                 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
884         long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis(
885                 SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS,
886                 AccessibilityManager.FLAG_CONTENT_CONTROLS);
887 
888         mScreenshotHandler.sendMessageDelayed(
889                 mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT),
890                 timeoutMs);
891         if (DEBUG_UI) {
892             Log.d(TAG, "dismiss timeout: " + timeoutMs + " ms");
893         }
894 
895     }
896 
897     /**
898      * Sets up the action shade and its entrance animation, once we get the screenshot URI.
899      */
showUiOnActionsReady(ScreenshotController.SavedImageData imageData)900     private void showUiOnActionsReady(ScreenshotController.SavedImageData imageData) {
901         logSuccessOnActionsReady(imageData);
902         if (DEBUG_UI) {
903             Log.d(TAG, "Showing UI actions");
904         }
905 
906         resetTimeout();
907 
908         if (imageData.uri != null) {
909             mScreenshotHandler.post(() -> {
910                 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
911                     mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
912                         @Override
913                         public void onAnimationEnd(Animator animation) {
914                             super.onAnimationEnd(animation);
915                             mScreenshotView.setChipIntents(imageData);
916                         }
917                     });
918                 } else {
919                     mScreenshotView.setChipIntents(imageData);
920                 }
921             });
922         }
923     }
924 
925     /**
926      * Sets up the action shade and its entrance animation, once we get the Quick Share action data.
927      */
showUiOnQuickShareActionReady(ScreenshotController.QuickShareData quickShareData)928     private void showUiOnQuickShareActionReady(ScreenshotController.QuickShareData quickShareData) {
929         if (DEBUG_UI) {
930             Log.d(TAG, "Showing UI for Quick Share action");
931         }
932         if (quickShareData.quickShareAction != null) {
933             mScreenshotHandler.post(() -> {
934                 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
935                     mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
936                         @Override
937                         public void onAnimationEnd(Animator animation) {
938                             super.onAnimationEnd(animation);
939                             mScreenshotView.addQuickShareChip(quickShareData.quickShareAction);
940                         }
941                     });
942                 } else {
943                     mScreenshotView.addQuickShareChip(quickShareData.quickShareAction);
944                 }
945             });
946         }
947     }
948 
949     /**
950      * Supplies the necessary bits for the shared element transition to share sheet.
951      * Note that once supplied, the action intent to share must be sent immediately after.
952      */
getActionTransitionSupplier()953     private Supplier<ActionTransition> getActionTransitionSupplier() {
954         return () -> {
955             Pair<ActivityOptions, ExitTransitionCoordinator> transition =
956                     ActivityOptions.startSharedElementAnimation(
957                             mWindow, new ScreenshotExitTransitionCallbacksSupplier(true).get(),
958                             null, Pair.create(mScreenshotView.getScreenshotPreview(),
959                                     ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME));
960             transition.second.startExit();
961 
962             ActionTransition supply = new ActionTransition();
963             supply.bundle = transition.first.toBundle();
964             supply.onCancelRunnable = () -> ActivityOptions.stopSharedElementAnimation(mWindow);
965             return supply;
966         };
967     }
968 
969     /**
970      * Logs success/failure of the screenshot saving task, and shows an error if it failed.
971      */
logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData)972     private void logSuccessOnActionsReady(ScreenshotController.SavedImageData imageData) {
973         if (imageData.uri == null) {
974             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, mPackageName);
975             mNotificationsController.notifyScreenshotError(
976                     R.string.screenshot_failed_to_save_text);
977         } else {
978             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, mPackageName);
979         }
980     }
981 
isUserSetupComplete()982     private boolean isUserSetupComplete() {
983         return Settings.Secure.getInt(mContext.getContentResolver(),
984                 SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
985     }
986 
987     /**
988      * Updates the window focusability.  If the window is already showing, then it updates the
989      * window immediately, otherwise the layout params will be applied when the window is next
990      * shown.
991      */
setWindowFocusable(boolean focusable)992     private void setWindowFocusable(boolean focusable) {
993         if (DEBUG_WINDOW) {
994             Log.d(TAG, "setWindowFocusable: " + focusable);
995         }
996         int flags = mWindowLayoutParams.flags;
997         if (focusable) {
998             mWindowLayoutParams.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
999         } else {
1000             mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
1001         }
1002         if (mWindowLayoutParams.flags == flags) {
1003             if (DEBUG_WINDOW) {
1004                 Log.d(TAG, "setWindowFocusable: skipping, already " + focusable);
1005             }
1006             return;
1007         }
1008         final View decorView = mWindow.peekDecorView();
1009         if (decorView != null && decorView.isAttachedToWindow()) {
1010             mWindowManager.updateViewLayout(decorView, mWindowLayoutParams);
1011         }
1012     }
1013 
getDefaultDisplay()1014     private Display getDefaultDisplay() {
1015         return mDisplayManager.getDisplay(DEFAULT_DISPLAY);
1016     }
1017 
allowLongScreenshots()1018     private boolean allowLongScreenshots() {
1019         return !mIsLowRamDevice;
1020     }
1021 
1022     /** Does the aspect ratio of the bitmap with insets removed match the bounds. */
aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds)1023     private static boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets,
1024             Rect screenBounds) {
1025         int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right;
1026         int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom;
1027 
1028         if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0
1029                 || bitmap.getHeight() == 0) {
1030             if (DEBUG_UI) {
1031                 Log.e(TAG, "Provided bitmap and insets create degenerate region: "
1032                         + bitmap.getWidth() + "x" + bitmap.getHeight() + " " + bitmapInsets);
1033             }
1034             return false;
1035         }
1036 
1037         float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight;
1038         float boundsAspect = ((float) screenBounds.width()) / screenBounds.height();
1039 
1040         boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f;
1041         if (DEBUG_UI) {
1042             Log.d(TAG, "aspectRatiosMatch: don't match bitmap: " + insettedBitmapAspect
1043                     + ", bounds: " + boundsAspect);
1044         }
1045         return matchWithinTolerance;
1046     }
1047 
1048     private class ScreenshotExitTransitionCallbacksSupplier implements
1049             Supplier<ExitTransitionCallbacks> {
1050         final boolean mDismissOnHideSharedElements;
1051 
1052         ScreenshotExitTransitionCallbacksSupplier(boolean dismissOnHideSharedElements) {
1053             mDismissOnHideSharedElements = dismissOnHideSharedElements;
1054         }
1055 
1056         @Override
1057         public ExitTransitionCallbacks get() {
1058             return new ExitTransitionCallbacks() {
1059                 @Override
1060                 public boolean isReturnTransitionAllowed() {
1061                     return false;
1062                 }
1063 
1064                 @Override
1065                 public void hideSharedElements() {
1066                     if (mDismissOnHideSharedElements) {
1067                         finishDismiss();
1068                     }
1069                 }
1070 
1071                 @Override
1072                 public void onFinish() {
1073                 }
1074             };
1075         }
1076     }
1077 }
1078