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