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_LANDSCAPE;
20 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
21 import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
22 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
23 
24 import static com.android.systemui.statusbar.phone.StatusBar.SYSTEM_DIALOG_REASON_SCREENSHOT;
25 
26 import android.animation.Animator;
27 import android.animation.AnimatorListenerAdapter;
28 import android.animation.AnimatorSet;
29 import android.animation.ValueAnimator;
30 import android.annotation.Nullable;
31 import android.annotation.SuppressLint;
32 import android.app.ActivityManager;
33 import android.app.ActivityOptions;
34 import android.app.Notification;
35 import android.app.PendingIntent;
36 import android.content.BroadcastReceiver;
37 import android.content.ComponentName;
38 import android.content.Context;
39 import android.content.Intent;
40 import android.content.res.Configuration;
41 import android.content.res.Resources;
42 import android.graphics.Bitmap;
43 import android.graphics.Color;
44 import android.graphics.Insets;
45 import android.graphics.Outline;
46 import android.graphics.PixelFormat;
47 import android.graphics.PointF;
48 import android.graphics.Rect;
49 import android.graphics.Region;
50 import android.graphics.drawable.BitmapDrawable;
51 import android.graphics.drawable.ColorDrawable;
52 import android.graphics.drawable.Drawable;
53 import android.graphics.drawable.InsetDrawable;
54 import android.graphics.drawable.LayerDrawable;
55 import android.media.MediaActionSound;
56 import android.net.Uri;
57 import android.os.Handler;
58 import android.os.Looper;
59 import android.os.Message;
60 import android.os.PowerManager;
61 import android.os.RemoteException;
62 import android.provider.Settings;
63 import android.util.DisplayMetrics;
64 import android.util.Log;
65 import android.util.MathUtils;
66 import android.util.Slog;
67 import android.view.Display;
68 import android.view.KeyEvent;
69 import android.view.LayoutInflater;
70 import android.view.MotionEvent;
71 import android.view.SurfaceControl;
72 import android.view.View;
73 import android.view.ViewGroup;
74 import android.view.ViewOutlineProvider;
75 import android.view.ViewTreeObserver;
76 import android.view.WindowInsets;
77 import android.view.WindowManager;
78 import android.view.accessibility.AccessibilityManager;
79 import android.view.animation.AccelerateInterpolator;
80 import android.view.animation.AnimationUtils;
81 import android.view.animation.Interpolator;
82 import android.widget.FrameLayout;
83 import android.widget.HorizontalScrollView;
84 import android.widget.ImageView;
85 import android.widget.LinearLayout;
86 import android.widget.Toast;
87 
88 import com.android.internal.logging.UiEventLogger;
89 import com.android.systemui.R;
90 import com.android.systemui.dagger.qualifiers.Main;
91 import com.android.systemui.shared.system.ActivityManagerWrapper;
92 import com.android.systemui.shared.system.QuickStepContract;
93 import com.android.systemui.statusbar.phone.StatusBar;
94 
95 import java.util.ArrayList;
96 import java.util.List;
97 import java.util.Optional;
98 import java.util.concurrent.ExecutionException;
99 import java.util.concurrent.TimeUnit;
100 import java.util.concurrent.TimeoutException;
101 import java.util.function.Consumer;
102 
103 import javax.inject.Inject;
104 import javax.inject.Singleton;
105 
106 import dagger.Lazy;
107 
108 /**
109  * Class for handling device screen shots
110  */
111 @Singleton
112 public class GlobalScreenshot implements ViewTreeObserver.OnComputeInternalInsetsListener {
113 
114     /**
115      * POD used in the AsyncTask which saves an image in the background.
116      */
117     static class SaveImageInBackgroundData {
118         public Bitmap image;
119         public Consumer<Uri> finisher;
120         public GlobalScreenshot.ActionsReadyListener mActionsReadyListener;
121         public int errorMsgResId;
122 
clearImage()123         void clearImage() {
124             image = null;
125         }
126     }
127 
128     /**
129      * Structure returned by the SaveImageInBackgroundTask
130      */
131     static class SavedImageData {
132         public Uri uri;
133         public Notification.Action shareAction;
134         public Notification.Action editAction;
135         public Notification.Action deleteAction;
136         public List<Notification.Action> smartActions;
137 
138         /**
139          * Used to reset the return data on error
140          */
reset()141         public void reset() {
142             uri = null;
143             shareAction = null;
144             editAction = null;
145             deleteAction = null;
146             smartActions = null;
147         }
148     }
149 
150     abstract static class ActionsReadyListener {
onActionsReady(SavedImageData imageData)151         abstract void onActionsReady(SavedImageData imageData);
152     }
153 
154     // These strings are used for communicating the action invoked to
155     // ScreenshotNotificationSmartActionsProvider.
156     static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type";
157     static final String EXTRA_ID = "android:screenshot_id";
158     static final String ACTION_TYPE_DELETE = "Delete";
159     static final String ACTION_TYPE_SHARE = "Share";
160     static final String ACTION_TYPE_EDIT = "Edit";
161     static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled";
162     static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent";
163 
164     static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id";
165     static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification";
166     static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip";
167 
168     // From WizardManagerHelper.java
169     private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete";
170 
171     private static final String TAG = "GlobalScreenshot";
172 
173     private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133;
174     private static final long SCREENSHOT_FLASH_OUT_DURATION_MS = 217;
175     // delay before starting to fade in dismiss button
176     private static final long SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS = 200;
177     private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234;
178     private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500;
179     private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234;
180     private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
181     private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100;
182     private static final long SCREENSHOT_DISMISS_Y_DURATION_MS = 350;
183     private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 183;
184     private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade
185     private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
186     private static final float ROUNDED_CORNER_RADIUS = .05f;
187     private static final int SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS = 6000;
188     private static final int MESSAGE_CORNER_TIMEOUT = 2;
189 
190     private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator();
191 
192     private final ScreenshotNotificationsController mNotificationsController;
193     private final UiEventLogger mUiEventLogger;
194 
195     private final Context mContext;
196     private final WindowManager mWindowManager;
197     private final WindowManager.LayoutParams mWindowLayoutParams;
198     private final Display mDisplay;
199     private final DisplayMetrics mDisplayMetrics;
200 
201     private View mScreenshotLayout;
202     private ScreenshotSelectorView mScreenshotSelectorView;
203     private ImageView mScreenshotAnimatedView;
204     private ImageView mScreenshotPreview;
205     private ImageView mScreenshotFlash;
206     private ImageView mActionsContainerBackground;
207     private HorizontalScrollView mActionsContainer;
208     private LinearLayout mActionsView;
209     private ImageView mBackgroundProtection;
210     private FrameLayout mDismissButton;
211 
212     private Bitmap mScreenBitmap;
213     private SaveImageInBackgroundTask mSaveInBgTask;
214     private Animator mScreenshotAnimation;
215     private Runnable mOnCompleteRunnable;
216     private Animator mDismissAnimation;
217     private boolean mInDarkMode = false;
218     private boolean mDirectionLTR = true;
219     private boolean mOrientationPortrait = true;
220 
221     private float mCornerSizeX;
222     private float mDismissDeltaY;
223 
224     private MediaActionSound mCameraSound;
225 
226     private int mNavMode;
227     private int mLeftInset;
228     private int mRightInset;
229 
230     // standard material ease
231     private final Interpolator mFastOutSlowIn;
232 
233     private final Handler mScreenshotHandler = new Handler(Looper.getMainLooper()) {
234         @Override
235         public void handleMessage(Message msg) {
236             switch (msg.what) {
237                 case MESSAGE_CORNER_TIMEOUT:
238                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT);
239                     GlobalScreenshot.this.dismissScreenshot("timeout", false);
240                     mOnCompleteRunnable.run();
241                     break;
242                 default:
243                     break;
244             }
245         }
246     };
247 
248     /**
249      * @param context everything needs a context :(
250      */
251     @Inject
GlobalScreenshot( Context context, @Main Resources resources, ScreenshotNotificationsController screenshotNotificationsController, UiEventLogger uiEventLogger)252     public GlobalScreenshot(
253             Context context, @Main Resources resources,
254             ScreenshotNotificationsController screenshotNotificationsController,
255             UiEventLogger uiEventLogger) {
256         mContext = context;
257         mNotificationsController = screenshotNotificationsController;
258         mUiEventLogger = uiEventLogger;
259 
260         reloadAssets();
261         Configuration config = mContext.getResources().getConfiguration();
262         mInDarkMode = config.isNightModeActive();
263         mDirectionLTR = config.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
264         mOrientationPortrait = config.orientation == ORIENTATION_PORTRAIT;
265 
266         // Setup the window that we are going to use
267         mWindowLayoutParams = new WindowManager.LayoutParams(
268                 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0,
269                 WindowManager.LayoutParams.TYPE_SCREENSHOT,
270                 WindowManager.LayoutParams.FLAG_FULLSCREEN
271                         | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
272                         | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
273                         | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
274                         | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
275                         | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
276                 PixelFormat.TRANSLUCENT);
277         mWindowLayoutParams.setTitle("ScreenshotAnimation");
278         mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
279         mWindowLayoutParams.setFitInsetsTypes(0 /* types */);
280         mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
281         mDisplay = mWindowManager.getDefaultDisplay();
282         mDisplayMetrics = new DisplayMetrics();
283         mDisplay.getRealMetrics(mDisplayMetrics);
284 
285         mCornerSizeX = resources.getDimensionPixelSize(R.dimen.global_screenshot_x_scale);
286         mDismissDeltaY = resources.getDimensionPixelSize(R.dimen.screenshot_dismissal_height_delta);
287 
288         mFastOutSlowIn =
289                 AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in);
290 
291         // Setup the Camera shutter sound
292         mCameraSound = new MediaActionSound();
293         mCameraSound.load(MediaActionSound.SHUTTER_CLICK);
294     }
295 
296     @Override // ViewTreeObserver.OnComputeInternalInsetsListener
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)297     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
298         inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
299         Region touchRegion = new Region();
300 
301         Rect screenshotRect = new Rect();
302         mScreenshotPreview.getBoundsOnScreen(screenshotRect);
303         touchRegion.op(screenshotRect, Region.Op.UNION);
304         Rect actionsRect = new Rect();
305         mActionsContainer.getBoundsOnScreen(actionsRect);
306         touchRegion.op(actionsRect, Region.Op.UNION);
307         Rect dismissRect = new Rect();
308         mDismissButton.getBoundsOnScreen(dismissRect);
309         touchRegion.op(dismissRect, Region.Op.UNION);
310 
311         if (QuickStepContract.isGesturalMode(mNavMode)) {
312             // Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE
313             Rect inset = new Rect(0, 0, mLeftInset, mDisplayMetrics.heightPixels);
314             touchRegion.op(inset, Region.Op.UNION);
315             inset.set(mDisplayMetrics.widthPixels - mRightInset, 0, mDisplayMetrics.widthPixels,
316                     mDisplayMetrics.heightPixels);
317             touchRegion.op(inset, Region.Op.UNION);
318         }
319 
320         inoutInfo.touchableRegion.set(touchRegion);
321     }
322 
onConfigChanged(Configuration newConfig)323     private void onConfigChanged(Configuration newConfig) {
324         boolean needsUpdate = false;
325         // dark mode
326         if (newConfig.isNightModeActive()) {
327             // Night mode is active, we're using dark theme
328             if (!mInDarkMode) {
329                 mInDarkMode = true;
330                 needsUpdate = true;
331             }
332         } else {
333             // Night mode is not active, we're using the light theme
334             if (mInDarkMode) {
335                 mInDarkMode = false;
336                 needsUpdate = true;
337             }
338         }
339 
340         // RTL configuration
341         switch (newConfig.getLayoutDirection()) {
342             case View.LAYOUT_DIRECTION_LTR:
343                 if (!mDirectionLTR) {
344                     mDirectionLTR = true;
345                     needsUpdate = true;
346                 }
347                 break;
348             case View.LAYOUT_DIRECTION_RTL:
349                 if (mDirectionLTR) {
350                     mDirectionLTR = false;
351                     needsUpdate = true;
352                 }
353                 break;
354         }
355 
356         // portrait/landscape orientation
357         switch (newConfig.orientation) {
358             case ORIENTATION_PORTRAIT:
359                 if (!mOrientationPortrait) {
360                     mOrientationPortrait = true;
361                     needsUpdate = true;
362                 }
363                 break;
364             case ORIENTATION_LANDSCAPE:
365                 if (mOrientationPortrait) {
366                     mOrientationPortrait = false;
367                     needsUpdate = true;
368                 }
369                 break;
370         }
371 
372         if (needsUpdate) {
373             reloadAssets();
374         }
375 
376         mNavMode = mContext.getResources().getInteger(
377                 com.android.internal.R.integer.config_navBarInteractionMode);
378     }
379 
380     /**
381      * Update assets (called when the dark theme status changes). We only need to update the dismiss
382      * button and the actions container background, since the buttons are re-inflated on demand.
383      */
reloadAssets()384     private void reloadAssets() {
385         boolean wasAttached = mScreenshotLayout != null && mScreenshotLayout.isAttachedToWindow();
386         if (wasAttached) {
387             mWindowManager.removeView(mScreenshotLayout);
388         }
389 
390         // Inflate the screenshot layout
391         mScreenshotLayout = LayoutInflater.from(mContext).inflate(R.layout.global_screenshot, null);
392         // TODO(159460485): Remove this when focus is handled properly in the system
393         mScreenshotLayout.setOnTouchListener((v, event) -> {
394             if (event.getActionMasked() == MotionEvent.ACTION_OUTSIDE) {
395                 // Once the user touches outside, stop listening for input
396                 setWindowFocusable(false);
397             }
398             return false;
399         });
400         mScreenshotLayout.setOnApplyWindowInsetsListener((v, insets) -> {
401             if (QuickStepContract.isGesturalMode(mNavMode)) {
402                 Insets gestureInsets = insets.getInsets(
403                         WindowInsets.Type.systemGestures());
404                 mLeftInset = gestureInsets.left;
405                 mRightInset = gestureInsets.right;
406             } else {
407                 mLeftInset = mRightInset = 0;
408             }
409             return mScreenshotLayout.onApplyWindowInsets(insets);
410         });
411         mScreenshotLayout.setOnKeyListener(new View.OnKeyListener() {
412             @Override
413             public boolean onKey(View v, int keyCode, KeyEvent event) {
414                 if (keyCode == KeyEvent.KEYCODE_BACK) {
415                     dismissScreenshot("back pressed", true);
416                     return true;
417                 }
418                 return false;
419             }
420         });
421         // Get focus so that the key events go to the layout.
422         mScreenshotLayout.setFocusableInTouchMode(true);
423         mScreenshotLayout.requestFocus();
424 
425         mScreenshotAnimatedView =
426                 mScreenshotLayout.findViewById(R.id.global_screenshot_animated_view);
427         mScreenshotAnimatedView.setClipToOutline(true);
428         mScreenshotAnimatedView.setOutlineProvider(new ViewOutlineProvider() {
429             @Override
430             public void getOutline(View view, Outline outline) {
431                 outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()),
432                         ROUNDED_CORNER_RADIUS * view.getWidth());
433             }
434         });
435         mScreenshotPreview = mScreenshotLayout.findViewById(R.id.global_screenshot_preview);
436         mScreenshotPreview.setClipToOutline(true);
437         mScreenshotPreview.setOutlineProvider(new ViewOutlineProvider() {
438             @Override
439             public void getOutline(View view, Outline outline) {
440                 outline.setRoundRect(new Rect(0, 0, view.getWidth(), view.getHeight()),
441                         ROUNDED_CORNER_RADIUS * view.getWidth());
442             }
443         });
444 
445         mActionsContainerBackground = mScreenshotLayout.findViewById(
446                 R.id.global_screenshot_actions_container_background);
447         mActionsContainer = mScreenshotLayout.findViewById(
448                 R.id.global_screenshot_actions_container);
449         mActionsView = mScreenshotLayout.findViewById(R.id.global_screenshot_actions);
450         mBackgroundProtection = mScreenshotLayout.findViewById(
451                 R.id.global_screenshot_actions_background);
452         mDismissButton = mScreenshotLayout.findViewById(R.id.global_screenshot_dismiss_button);
453         mDismissButton.setOnClickListener(view -> {
454             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL);
455             dismissScreenshot("dismiss_button", false);
456             mOnCompleteRunnable.run();
457         });
458 
459         mScreenshotFlash = mScreenshotLayout.findViewById(R.id.global_screenshot_flash);
460         mScreenshotSelectorView = mScreenshotLayout.findViewById(R.id.global_screenshot_selector);
461         mScreenshotLayout.setFocusable(true);
462         mScreenshotSelectorView.setFocusable(true);
463         mScreenshotSelectorView.setFocusableInTouchMode(true);
464         mScreenshotAnimatedView.setPivotX(0);
465         mScreenshotAnimatedView.setPivotY(0);
466         mActionsContainer.setScrollX(0);
467 
468         if (wasAttached) {
469             mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
470         }
471     }
472 
473     /**
474      * Updates the window focusability.  If the window is already showing, then it updates the
475      * window immediately, otherwise the layout params will be applied when the window is next
476      * shown.
477      */
setWindowFocusable(boolean focusable)478     private void setWindowFocusable(boolean focusable) {
479         if (focusable) {
480             mWindowLayoutParams.flags &= ~FLAG_NOT_FOCUSABLE;
481         } else {
482             mWindowLayoutParams.flags |= FLAG_NOT_FOCUSABLE;
483         }
484         if (mScreenshotLayout.isAttachedToWindow()) {
485             mWindowManager.updateViewLayout(mScreenshotLayout, mWindowLayoutParams);
486         }
487     }
488 
489     /**
490      * Creates a new worker thread and saves the screenshot to the media store.
491      */
saveScreenshotInWorkerThread( Consumer<Uri> finisher, @Nullable ActionsReadyListener actionsReadyListener)492     private void saveScreenshotInWorkerThread(
493             Consumer<Uri> finisher, @Nullable ActionsReadyListener actionsReadyListener) {
494         SaveImageInBackgroundData data = new SaveImageInBackgroundData();
495         data.image = mScreenBitmap;
496         data.finisher = finisher;
497         data.mActionsReadyListener = actionsReadyListener;
498 
499         if (mSaveInBgTask != null) {
500             // just log success/failure for the pre-existing screenshot
501             mSaveInBgTask.setActionsReadyListener(new ActionsReadyListener() {
502                 @Override
503                 void onActionsReady(SavedImageData imageData) {
504                     logSuccessOnActionsReady(imageData);
505                 }
506             });
507         }
508 
509         mSaveInBgTask = new SaveImageInBackgroundTask(mContext, data);
510         mSaveInBgTask.execute();
511     }
512 
513     /**
514      * Takes a screenshot of the current display and shows an animation.
515      */
takeScreenshot(Consumer<Uri> finisher, Rect crop)516     private void takeScreenshot(Consumer<Uri> finisher, Rect crop) {
517         // copy the input Rect, since SurfaceControl.screenshot can mutate it
518         Rect screenRect = new Rect(crop);
519         int rot = mDisplay.getRotation();
520         int width = crop.width();
521         int height = crop.height();
522         takeScreenshot(SurfaceControl.screenshot(crop, width, height, rot), finisher, screenRect,
523                 Insets.NONE, true);
524     }
525 
takeScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, boolean showFlash)526     private void takeScreenshot(Bitmap screenshot, Consumer<Uri> finisher, Rect screenRect,
527             Insets screenInsets, boolean showFlash) {
528         dismissScreenshot("new screenshot requested", true);
529 
530         mScreenBitmap = screenshot;
531 
532         if (mScreenBitmap == null) {
533             mNotificationsController.notifyScreenshotError(
534                     R.string.screenshot_failed_to_capture_text);
535             finisher.accept(null);
536             mOnCompleteRunnable.run();
537             return;
538         }
539 
540         if (!isUserSetupComplete()) {
541             // User setup isn't complete, so we don't want to show any UI beyond a toast, as editing
542             // and sharing shouldn't be exposed to the user.
543             saveScreenshotAndToast(finisher);
544             return;
545         }
546 
547         // Optimizations
548         mScreenBitmap.setHasAlpha(false);
549         mScreenBitmap.prepareToDraw();
550 
551         onConfigChanged(mContext.getResources().getConfiguration());
552 
553         if (mDismissAnimation != null && mDismissAnimation.isRunning()) {
554             mDismissAnimation.cancel();
555         }
556 
557         // The window is focusable by default
558         setWindowFocusable(true);
559 
560         // Start the post-screenshot animation
561         startAnimation(finisher, screenRect, screenInsets, showFlash);
562     }
563 
takeScreenshot(Consumer<Uri> finisher, Runnable onComplete)564     void takeScreenshot(Consumer<Uri> finisher, Runnable onComplete) {
565         mOnCompleteRunnable = onComplete;
566 
567         mDisplay.getRealMetrics(mDisplayMetrics);
568         takeScreenshot(
569                 finisher,
570                 new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels));
571     }
572 
handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds, Insets visibleInsets, int taskId, int userId, ComponentName topComponent, Consumer<Uri> finisher, Runnable onComplete)573     void handleImageAsScreenshot(Bitmap screenshot, Rect screenshotScreenBounds,
574             Insets visibleInsets, int taskId, int userId, ComponentName topComponent,
575             Consumer<Uri> finisher, Runnable onComplete) {
576         // TODO: use task Id, userId, topComponent for smart handler
577 
578         mOnCompleteRunnable = onComplete;
579         if (aspectRatiosMatch(screenshot, visibleInsets, screenshotScreenBounds)) {
580             takeScreenshot(screenshot, finisher, screenshotScreenBounds, visibleInsets, false);
581         } else {
582             takeScreenshot(screenshot, finisher,
583                     new Rect(0, 0, screenshot.getWidth(), screenshot.getHeight()), Insets.NONE,
584                     true);
585         }
586     }
587 
588     /**
589      * Displays a screenshot selector
590      */
591     @SuppressLint("ClickableViewAccessibility")
takeScreenshotPartial(final Consumer<Uri> finisher, Runnable onComplete)592     void takeScreenshotPartial(final Consumer<Uri> finisher, Runnable onComplete) {
593         dismissScreenshot("new screenshot requested", true);
594         mOnCompleteRunnable = onComplete;
595 
596         mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
597         mScreenshotSelectorView.setOnTouchListener(new View.OnTouchListener() {
598             @Override
599             public boolean onTouch(View v, MotionEvent event) {
600                 ScreenshotSelectorView view = (ScreenshotSelectorView) v;
601                 switch (event.getAction()) {
602                     case MotionEvent.ACTION_DOWN:
603                         view.startSelection((int) event.getX(), (int) event.getY());
604                         return true;
605                     case MotionEvent.ACTION_MOVE:
606                         view.updateSelection((int) event.getX(), (int) event.getY());
607                         return true;
608                     case MotionEvent.ACTION_UP:
609                         view.setVisibility(View.GONE);
610                         mWindowManager.removeView(mScreenshotLayout);
611                         final Rect rect = view.getSelectionRect();
612                         if (rect != null) {
613                             if (rect.width() != 0 && rect.height() != 0) {
614                                 // Need mScreenshotLayout to handle it after the view disappears
615                                 mScreenshotLayout.post(() -> takeScreenshot(finisher, rect));
616                             }
617                         }
618 
619                         view.stopSelection();
620                         return true;
621                 }
622 
623                 return false;
624             }
625         });
626         mScreenshotLayout.post(() -> {
627             mScreenshotSelectorView.setVisibility(View.VISIBLE);
628             mScreenshotSelectorView.requestFocus();
629         });
630     }
631 
632     /**
633      * Cancels screenshot request
634      */
stopScreenshot()635     void stopScreenshot() {
636         // If the selector layer still presents on screen, we remove it and resets its state.
637         if (mScreenshotSelectorView.getSelectionRect() != null) {
638             mWindowManager.removeView(mScreenshotLayout);
639             mScreenshotSelectorView.stopSelection();
640         }
641     }
642 
643     /**
644      * Save the bitmap but don't show the normal screenshot UI.. just a toast (or notification on
645      * failure).
646      */
saveScreenshotAndToast(Consumer<Uri> finisher)647     private void saveScreenshotAndToast(Consumer<Uri> finisher) {
648         // Play the shutter sound to notify that we've taken a screenshot
649         mScreenshotHandler.post(() -> {
650             mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
651         });
652 
653         saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() {
654             @Override
655             void onActionsReady(SavedImageData imageData) {
656                 finisher.accept(imageData.uri);
657                 if (imageData.uri == null) {
658                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED);
659                     mNotificationsController.notifyScreenshotError(
660                             R.string.screenshot_failed_to_capture_text);
661                 } else {
662                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED);
663 
664                     mScreenshotHandler.post(() -> {
665                         Toast.makeText(mContext, R.string.screenshot_saved_title,
666                                 Toast.LENGTH_SHORT).show();
667                     });
668                 }
669             }
670         });
671     }
672 
isUserSetupComplete()673     private boolean isUserSetupComplete() {
674         return Settings.Secure.getInt(mContext.getContentResolver(),
675                 SETTINGS_SECURE_USER_SETUP_COMPLETE, 0) == 1;
676     }
677 
678     /**
679      * Clears current screenshot
680      */
dismissScreenshot(String reason, boolean immediate)681     void dismissScreenshot(String reason, boolean immediate) {
682         Log.v(TAG, "clearing screenshot: " + reason);
683         mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT);
684         mScreenshotLayout.getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
685         if (!immediate) {
686             mDismissAnimation = createScreenshotDismissAnimation();
687             mDismissAnimation.addListener(new AnimatorListenerAdapter() {
688                 @Override
689                 public void onAnimationEnd(Animator animation) {
690                     super.onAnimationEnd(animation);
691                     clearScreenshot();
692                 }
693             });
694             mDismissAnimation.start();
695         } else {
696             clearScreenshot();
697         }
698     }
699 
clearScreenshot()700     private void clearScreenshot() {
701         if (mScreenshotLayout.isAttachedToWindow()) {
702             mWindowManager.removeView(mScreenshotLayout);
703         }
704 
705         // Clear any references to the bitmap
706         mScreenshotPreview.setImageDrawable(null);
707         mScreenshotAnimatedView.setImageDrawable(null);
708         mScreenshotAnimatedView.setVisibility(View.GONE);
709         mActionsContainerBackground.setVisibility(View.GONE);
710         mActionsContainer.setVisibility(View.GONE);
711         mBackgroundProtection.setAlpha(0f);
712         mDismissButton.setVisibility(View.GONE);
713         mScreenshotPreview.setVisibility(View.GONE);
714         mScreenshotPreview.setLayerType(View.LAYER_TYPE_NONE, null);
715         mScreenshotPreview.setContentDescription(
716                 mContext.getResources().getString(R.string.screenshot_preview_description));
717         mScreenshotLayout.setAlpha(1);
718         mDismissButton.setTranslationY(0);
719         mActionsContainer.setTranslationY(0);
720         mActionsContainerBackground.setTranslationY(0);
721         mScreenshotPreview.setTranslationY(0);
722     }
723 
724     /**
725      * Sets up the action shade and its entrance animation, once we get the screenshot URI.
726      */
showUiOnActionsReady(SavedImageData imageData)727     private void showUiOnActionsReady(SavedImageData imageData) {
728         logSuccessOnActionsReady(imageData);
729 
730         AccessibilityManager accessibilityManager = (AccessibilityManager)
731                 mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
732         long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis(
733                 SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS,
734                 AccessibilityManager.FLAG_CONTENT_CONTROLS);
735 
736         mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT);
737         mScreenshotHandler.sendMessageDelayed(
738                 mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT),
739                 timeoutMs);
740 
741         if (imageData.uri != null) {
742             mScreenshotHandler.post(() -> {
743                 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
744                     mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
745                         @Override
746                         public void onAnimationEnd(Animator animation) {
747                             super.onAnimationEnd(animation);
748                             createScreenshotActionsShadeAnimation(imageData).start();
749                         }
750                     });
751                 } else {
752                     createScreenshotActionsShadeAnimation(imageData).start();
753                 }
754             });
755         }
756     }
757 
758     /**
759      * Logs success/failure of the screenshot saving task, and shows an error if it failed.
760      */
logSuccessOnActionsReady(SavedImageData imageData)761     private void logSuccessOnActionsReady(SavedImageData imageData) {
762         if (imageData.uri == null) {
763             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED);
764             mNotificationsController.notifyScreenshotError(
765                     R.string.screenshot_failed_to_capture_text);
766         } else {
767             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED);
768         }
769     }
770 
771     /**
772      * Starts the animation after taking the screenshot
773      */
startAnimation(final Consumer<Uri> finisher, Rect screenRect, Insets screenInsets, boolean showFlash)774     private void startAnimation(final Consumer<Uri> finisher, Rect screenRect, Insets screenInsets,
775             boolean showFlash) {
776 
777         // If power save is on, show a toast so there is some visual indication that a
778         // screenshot has been taken.
779         PowerManager powerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
780         if (powerManager.isPowerSaveMode()) {
781             Toast.makeText(mContext, R.string.screenshot_saved_title, Toast.LENGTH_SHORT).show();
782         }
783 
784         mScreenshotHandler.post(() -> {
785             if (!mScreenshotLayout.isAttachedToWindow()) {
786                 mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
787             }
788             mScreenshotAnimatedView.setImageDrawable(
789                     createScreenDrawable(mScreenBitmap, screenInsets));
790             setAnimatedViewSize(screenRect.width(), screenRect.height());
791             // Show when the animation starts
792             mScreenshotAnimatedView.setVisibility(View.GONE);
793 
794             mScreenshotPreview.setImageDrawable(createScreenDrawable(mScreenBitmap, screenInsets));
795             // make static preview invisible (from gone) so we can query its location on screen
796             mScreenshotPreview.setVisibility(View.INVISIBLE);
797 
798             mScreenshotHandler.post(() -> {
799                 mScreenshotLayout.getViewTreeObserver().addOnComputeInternalInsetsListener(this);
800 
801                 mScreenshotAnimation =
802                         createScreenshotDropInAnimation(screenRect, showFlash);
803 
804                 saveScreenshotInWorkerThread(finisher, new ActionsReadyListener() {
805                     @Override
806                     void onActionsReady(SavedImageData imageData) {
807                         showUiOnActionsReady(imageData);
808                     }
809                 });
810 
811                 // Play the shutter sound to notify that we've taken a screenshot
812                 mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
813 
814                 mScreenshotPreview.setLayerType(View.LAYER_TYPE_HARDWARE, null);
815                 mScreenshotPreview.buildLayer();
816                 mScreenshotAnimation.start();
817             });
818         });
819     }
820 
createScreenshotDropInAnimation(Rect bounds, boolean showFlash)821     private AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash) {
822         Rect previewBounds = new Rect();
823         mScreenshotPreview.getBoundsOnScreen(previewBounds);
824 
825         float cornerScale =
826                 mCornerSizeX / (mOrientationPortrait ? bounds.width() : bounds.height());
827         final float currentScale = 1f;
828 
829         mScreenshotAnimatedView.setScaleX(currentScale);
830         mScreenshotAnimatedView.setScaleY(currentScale);
831 
832         mDismissButton.setAlpha(0);
833         mDismissButton.setVisibility(View.VISIBLE);
834 
835         AnimatorSet dropInAnimation = new AnimatorSet();
836         ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1);
837         flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS);
838         flashInAnimator.setInterpolator(mFastOutSlowIn);
839         flashInAnimator.addUpdateListener(animation ->
840                 mScreenshotFlash.setAlpha((float) animation.getAnimatedValue()));
841 
842         ValueAnimator flashOutAnimator = ValueAnimator.ofFloat(1, 0);
843         flashOutAnimator.setDuration(SCREENSHOT_FLASH_OUT_DURATION_MS);
844         flashOutAnimator.setInterpolator(mFastOutSlowIn);
845         flashOutAnimator.addUpdateListener(animation ->
846                 mScreenshotFlash.setAlpha((float) animation.getAnimatedValue()));
847 
848         // animate from the current location, to the static preview location
849         final PointF startPos = new PointF(bounds.centerX(), bounds.centerY());
850         final PointF finalPos = new PointF(previewBounds.centerX(), previewBounds.centerY());
851 
852         ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1);
853         toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS);
854         float xPositionPct =
855                 SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
856         float dismissPct =
857                 SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
858         float scalePct =
859                 SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
860         toCorner.addUpdateListener(animation -> {
861             float t = animation.getAnimatedFraction();
862             if (t < scalePct) {
863                 float scale = MathUtils.lerp(
864                         currentScale, cornerScale, mFastOutSlowIn.getInterpolation(t / scalePct));
865                 mScreenshotAnimatedView.setScaleX(scale);
866                 mScreenshotAnimatedView.setScaleY(scale);
867             } else {
868                 mScreenshotAnimatedView.setScaleX(cornerScale);
869                 mScreenshotAnimatedView.setScaleY(cornerScale);
870             }
871 
872             float currentScaleX = mScreenshotAnimatedView.getScaleX();
873             float currentScaleY = mScreenshotAnimatedView.getScaleY();
874 
875             if (t < xPositionPct) {
876                 float xCenter = MathUtils.lerp(startPos.x, finalPos.x,
877                         mFastOutSlowIn.getInterpolation(t / xPositionPct));
878                 mScreenshotAnimatedView.setX(xCenter - bounds.width() * currentScaleX / 2f);
879             } else {
880                 mScreenshotAnimatedView.setX(finalPos.x - bounds.width() * currentScaleX / 2f);
881             }
882             float yCenter = MathUtils.lerp(
883                     startPos.y, finalPos.y, mFastOutSlowIn.getInterpolation(t));
884             mScreenshotAnimatedView.setY(yCenter - bounds.height() * currentScaleY / 2f);
885 
886             if (t >= dismissPct) {
887                 mDismissButton.setAlpha((t - dismissPct) / (1 - dismissPct));
888                 float currentX = mScreenshotAnimatedView.getX();
889                 float currentY = mScreenshotAnimatedView.getY();
890                 mDismissButton.setY(currentY - mDismissButton.getHeight() / 2f);
891                 if (mDirectionLTR) {
892                     mDismissButton.setX(currentX
893                             + bounds.width() * currentScaleX - mDismissButton.getWidth() / 2f);
894                 } else {
895                     mDismissButton.setX(currentX - mDismissButton.getWidth() / 2f);
896                 }
897             }
898         });
899 
900         toCorner.addListener(new AnimatorListenerAdapter() {
901             @Override
902             public void onAnimationStart(Animator animation) {
903                 super.onAnimationStart(animation);
904                 mScreenshotAnimatedView.setVisibility(View.VISIBLE);
905             }
906         });
907 
908         mScreenshotFlash.setAlpha(0f);
909         mScreenshotFlash.setVisibility(View.VISIBLE);
910 
911         if (showFlash) {
912             dropInAnimation.play(flashOutAnimator).after(flashInAnimator);
913             dropInAnimation.play(flashOutAnimator).with(toCorner);
914         } else {
915             dropInAnimation.play(toCorner);
916         }
917 
918         dropInAnimation.addListener(new AnimatorListenerAdapter() {
919             @Override
920             public void onAnimationEnd(Animator animation) {
921                 super.onAnimationEnd(animation);
922                 mDismissButton.setAlpha(1);
923                 float dismissOffset = mDismissButton.getWidth() / 2f;
924                 float finalDismissX = mDirectionLTR
925                         ? finalPos.x - dismissOffset + bounds.width() * cornerScale / 2f
926                         : finalPos.x - dismissOffset - bounds.width() * cornerScale / 2f;
927                 mDismissButton.setX(finalDismissX);
928                 mDismissButton.setY(
929                         finalPos.y - dismissOffset - bounds.height() * cornerScale / 2f);
930                 mScreenshotAnimatedView.setScaleX(1);
931                 mScreenshotAnimatedView.setScaleY(1);
932                 mScreenshotAnimatedView.setX(finalPos.x - bounds.width() * cornerScale / 2f);
933                 mScreenshotAnimatedView.setY(finalPos.y - bounds.height() * cornerScale / 2f);
934                 mScreenshotAnimatedView.setVisibility(View.GONE);
935                 mScreenshotPreview.setVisibility(View.VISIBLE);
936                 mScreenshotLayout.forceLayout();
937             }
938         });
939 
940         return dropInAnimation;
941     }
942 
createScreenshotActionsShadeAnimation(SavedImageData imageData)943     private ValueAnimator createScreenshotActionsShadeAnimation(SavedImageData imageData) {
944         LayoutInflater inflater = LayoutInflater.from(mContext);
945         mActionsView.removeAllViews();
946         mScreenshotLayout.invalidate();
947         mScreenshotLayout.requestLayout();
948         mScreenshotLayout.getViewTreeObserver().dispatchOnGlobalLayout();
949 
950         // By default the activities won't be able to start immediately; override this to keep
951         // the same behavior as if started from a notification
952         try {
953             ActivityManager.getService().resumeAppSwitches();
954         } catch (RemoteException e) {
955         }
956 
957         ArrayList<ScreenshotActionChip> chips = new ArrayList<>();
958 
959         for (Notification.Action smartAction : imageData.smartActions) {
960             ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate(
961                     R.layout.global_screenshot_action_chip, mActionsView, false);
962             actionChip.setText(smartAction.title);
963             actionChip.setIcon(smartAction.getIcon(), false);
964             actionChip.setPendingIntent(smartAction.actionIntent,
965                     () -> {
966                         mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED);
967                         dismissScreenshot("chip tapped", false);
968                         mOnCompleteRunnable.run();
969                     });
970             mActionsView.addView(actionChip);
971             chips.add(actionChip);
972         }
973 
974         ScreenshotActionChip shareChip = (ScreenshotActionChip) inflater.inflate(
975                 R.layout.global_screenshot_action_chip, mActionsView, false);
976         shareChip.setText(imageData.shareAction.title);
977         shareChip.setIcon(imageData.shareAction.getIcon(), true);
978         shareChip.setPendingIntent(imageData.shareAction.actionIntent, () -> {
979             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED);
980             dismissScreenshot("chip tapped", false);
981             mOnCompleteRunnable.run();
982         });
983         mActionsView.addView(shareChip);
984         chips.add(shareChip);
985 
986         ScreenshotActionChip editChip = (ScreenshotActionChip) inflater.inflate(
987                 R.layout.global_screenshot_action_chip, mActionsView, false);
988         editChip.setText(imageData.editAction.title);
989         editChip.setIcon(imageData.editAction.getIcon(), true);
990         editChip.setPendingIntent(imageData.editAction.actionIntent, () -> {
991             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED);
992             dismissScreenshot("chip tapped", false);
993             mOnCompleteRunnable.run();
994         });
995         mActionsView.addView(editChip);
996         chips.add(editChip);
997 
998         mScreenshotPreview.setOnClickListener(v -> {
999             try {
1000                 imageData.editAction.actionIntent.send();
1001                 mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED);
1002                 dismissScreenshot("screenshot preview tapped", false);
1003                 mOnCompleteRunnable.run();
1004             } catch (PendingIntent.CanceledException e) {
1005                 Log.e(TAG, "Intent cancelled", e);
1006             }
1007         });
1008         mScreenshotPreview.setContentDescription(imageData.editAction.title);
1009 
1010         // remove the margin from the last chip so that it's correctly aligned with the end
1011         LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)
1012                 mActionsView.getChildAt(mActionsView.getChildCount() - 1).getLayoutParams();
1013         params.setMarginEnd(0);
1014 
1015         ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
1016         animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS);
1017         float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS
1018                 / SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS;
1019         mActionsContainer.setAlpha(0f);
1020         mActionsContainerBackground.setAlpha(0f);
1021         mActionsContainer.setVisibility(View.VISIBLE);
1022         mActionsContainerBackground.setVisibility(View.VISIBLE);
1023 
1024         animator.addUpdateListener(animation -> {
1025             float t = animation.getAnimatedFraction();
1026             mBackgroundProtection.setAlpha(t);
1027             float containerAlpha = t < alphaFraction ? t / alphaFraction : 1;
1028             mActionsContainer.setAlpha(containerAlpha);
1029             mActionsContainerBackground.setAlpha(containerAlpha);
1030             float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X
1031                     + (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X));
1032             mActionsContainer.setScaleX(containerScale);
1033             mActionsContainerBackground.setScaleX(containerScale);
1034             for (ScreenshotActionChip chip : chips) {
1035                 chip.setAlpha(t);
1036                 chip.setScaleX(1 / containerScale); // invert to keep size of children constant
1037             }
1038             mActionsContainer.setScrollX(mDirectionLTR ? 0 : mActionsContainer.getWidth());
1039             mActionsContainer.setPivotX(mDirectionLTR ? 0 : mActionsContainer.getWidth());
1040             mActionsContainerBackground.setPivotX(
1041                     mDirectionLTR ? 0 : mActionsContainerBackground.getWidth());
1042         });
1043         return animator;
1044     }
1045 
createScreenshotDismissAnimation()1046     private AnimatorSet createScreenshotDismissAnimation() {
1047         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
1048         alphaAnim.setStartDelay(SCREENSHOT_DISMISS_ALPHA_OFFSET_MS);
1049         alphaAnim.setDuration(SCREENSHOT_DISMISS_ALPHA_DURATION_MS);
1050         alphaAnim.addUpdateListener(animation -> {
1051             mScreenshotLayout.setAlpha(1 - animation.getAnimatedFraction());
1052         });
1053 
1054         ValueAnimator yAnim = ValueAnimator.ofFloat(0, 1);
1055         yAnim.setInterpolator(mAccelerateInterpolator);
1056         yAnim.setDuration(SCREENSHOT_DISMISS_Y_DURATION_MS);
1057         float screenshotStartY = mScreenshotPreview.getTranslationY();
1058         float dismissStartY = mDismissButton.getTranslationY();
1059         yAnim.addUpdateListener(animation -> {
1060             float yDelta = MathUtils.lerp(0, mDismissDeltaY, animation.getAnimatedFraction());
1061             mScreenshotPreview.setTranslationY(screenshotStartY + yDelta);
1062             mDismissButton.setTranslationY(dismissStartY + yDelta);
1063             mActionsContainer.setTranslationY(yDelta);
1064             mActionsContainerBackground.setTranslationY(yDelta);
1065         });
1066 
1067         AnimatorSet animSet = new AnimatorSet();
1068         animSet.play(yAnim).with(alphaAnim);
1069 
1070         return animSet;
1071     }
1072 
setAnimatedViewSize(int width, int height)1073     private void setAnimatedViewSize(int width, int height) {
1074         ViewGroup.LayoutParams layoutParams = mScreenshotAnimatedView.getLayoutParams();
1075         layoutParams.width = width;
1076         layoutParams.height = height;
1077         mScreenshotAnimatedView.setLayoutParams(layoutParams);
1078     }
1079 
1080     /** Does the aspect ratio of the bitmap with insets removed match the bounds. */
aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds)1081     private boolean aspectRatiosMatch(Bitmap bitmap, Insets bitmapInsets, Rect screenBounds) {
1082         int insettedWidth = bitmap.getWidth() - bitmapInsets.left - bitmapInsets.right;
1083         int insettedHeight = bitmap.getHeight() - bitmapInsets.top - bitmapInsets.bottom;
1084 
1085         if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0
1086                 || bitmap.getHeight() == 0) {
1087             Log.e(TAG, String.format(
1088                     "Provided bitmap and insets create degenerate region: %dx%d %s",
1089                     bitmap.getWidth(), bitmap.getHeight(), bitmapInsets));
1090             return false;
1091         }
1092 
1093         float insettedBitmapAspect = ((float) insettedWidth) / insettedHeight;
1094         float boundsAspect = ((float) screenBounds.width()) / screenBounds.height();
1095 
1096         boolean matchWithinTolerance = Math.abs(insettedBitmapAspect - boundsAspect) < 0.1f;
1097         if (!matchWithinTolerance) {
1098             Log.d(TAG, String.format("aspectRatiosMatch: don't match bitmap: %f, bounds: %f",
1099                     insettedBitmapAspect, boundsAspect));
1100         }
1101 
1102         return matchWithinTolerance;
1103     }
1104 
1105     /**
1106      * Create a drawable using the size of the bitmap and insets as the fractional inset parameters.
1107      */
1108     private Drawable createScreenDrawable(Bitmap bitmap, Insets insets) {
1109         int insettedWidth = bitmap.getWidth() - insets.left - insets.right;
1110         int insettedHeight = bitmap.getHeight() - insets.top - insets.bottom;
1111 
1112         BitmapDrawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap);
1113         if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0
1114                 || bitmap.getHeight() == 0) {
1115             Log.e(TAG, String.format(
1116                     "Can't create insetted drawable, using 0 insets "
1117                             + "bitmap and insets create degenerate region: %dx%d %s",
1118                     bitmap.getWidth(), bitmap.getHeight(), insets));
1119             return bitmapDrawable;
1120         }
1121 
1122         InsetDrawable insetDrawable = new InsetDrawable(bitmapDrawable,
1123                 -1f * insets.left / insettedWidth,
1124                 -1f * insets.top / insettedHeight,
1125                 -1f * insets.right / insettedWidth,
1126                 -1f * insets.bottom / insettedHeight);
1127 
1128         if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) {
1129             // Are any of the insets negative, meaning the bitmap is smaller than the bounds so need
1130             // to fill in the background of the drawable.
1131             return new LayerDrawable(new Drawable[] {
1132                     new ColorDrawable(Color.BLACK), insetDrawable});
1133         } else {
1134             return insetDrawable;
1135         }
1136     }
1137 
1138     /**
1139      * Receiver to proxy the share or edit intent, used to clean up the notification and send
1140      * appropriate signals to the system (ie. to dismiss the keyguard if necessary).
1141      */
1142     public static class ActionProxyReceiver extends BroadcastReceiver {
1143         static final int CLOSE_WINDOWS_TIMEOUT_MILLIS = 3000;
1144         private final StatusBar mStatusBar;
1145 
1146         @Inject
1147         public ActionProxyReceiver(Optional<Lazy<StatusBar>> statusBarLazy) {
1148             Lazy<StatusBar> statusBar = statusBarLazy.orElse(null);
1149             mStatusBar = statusBar != null ? statusBar.get() : null;
1150         }
1151 
1152         @Override
1153         public void onReceive(Context context, final Intent intent) {
1154             Runnable startActivityRunnable = () -> {
1155                 try {
1156                     ActivityManagerWrapper.getInstance().closeSystemWindows(
1157                             SYSTEM_DIALOG_REASON_SCREENSHOT).get(
1158                             CLOSE_WINDOWS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
1159                 } catch (TimeoutException | InterruptedException | ExecutionException e) {
1160                     Slog.e(TAG, "Unable to share screenshot", e);
1161                     return;
1162                 }
1163 
1164                 PendingIntent actionIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
1165                 if (intent.getBooleanExtra(EXTRA_CANCEL_NOTIFICATION, false)) {
1166                     ScreenshotNotificationsController.cancelScreenshotNotification(context);
1167                 }
1168                 ActivityOptions opts = ActivityOptions.makeBasic();
1169                 opts.setDisallowEnterPictureInPictureWhileLaunching(
1170                         intent.getBooleanExtra(EXTRA_DISALLOW_ENTER_PIP, false));
1171                 try {
1172                     actionIntent.send(context, 0, null, null, null, null, opts.toBundle());
1173                 } catch (PendingIntent.CanceledException e) {
1174                     Log.e(TAG, "Pending intent canceled", e);
1175                 }
1176 
1177             };
1178 
1179             if (mStatusBar != null) {
1180                 mStatusBar.executeRunnableDismissingKeyguard(startActivityRunnable, null,
1181                         true /* dismissShade */, true /* afterKeyguardGone */,
1182                         true /* deferred */);
1183             } else {
1184                 startActivityRunnable.run();
1185             }
1186 
1187             if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
1188                 String actionType = Intent.ACTION_EDIT.equals(intent.getAction())
1189                         ? ACTION_TYPE_EDIT
1190                         : ACTION_TYPE_SHARE;
1191                 ScreenshotSmartActions.notifyScreenshotAction(
1192                         context, intent.getStringExtra(EXTRA_ID), actionType, false);
1193             }
1194         }
1195     }
1196 
1197     /**
1198      * Removes the notification for a screenshot after a share target is chosen.
1199      */
1200     public static class TargetChosenReceiver extends BroadcastReceiver {
1201         @Override
1202         public void onReceive(Context context, Intent intent) {
1203             // Clear the notification only after the user has chosen a share action
1204             ScreenshotNotificationsController.cancelScreenshotNotification(context);
1205         }
1206     }
1207 
1208     /**
1209      * Removes the last screenshot.
1210      */
1211     public static class DeleteScreenshotReceiver extends BroadcastReceiver {
1212         @Override
1213         public void onReceive(Context context, Intent intent) {
1214             if (!intent.hasExtra(SCREENSHOT_URI_ID)) {
1215                 return;
1216             }
1217 
1218             // Clear the notification when the image is deleted
1219             ScreenshotNotificationsController.cancelScreenshotNotification(context);
1220 
1221             // And delete the image from the media store
1222             final Uri uri = Uri.parse(intent.getStringExtra(SCREENSHOT_URI_ID));
1223             new DeleteImageInBackgroundTask(context).execute(uri);
1224             if (intent.getBooleanExtra(EXTRA_SMART_ACTIONS_ENABLED, false)) {
1225                 ScreenshotSmartActions.notifyScreenshotAction(
1226                         context, intent.getStringExtra(EXTRA_ID), ACTION_TYPE_DELETE, false);
1227             }
1228         }
1229     }
1230 
1231     /**
1232      * Executes the smart action tapped by the user in the notification.
1233      */
1234     public static class SmartActionsReceiver extends BroadcastReceiver {
1235         @Override
1236         public void onReceive(Context context, Intent intent) {
1237             PendingIntent pendingIntent = intent.getParcelableExtra(EXTRA_ACTION_INTENT);
1238             String actionType = intent.getStringExtra(EXTRA_ACTION_TYPE);
1239             Slog.d(TAG, "Executing smart action [" + actionType + "]:" + pendingIntent.getIntent());
1240             ActivityOptions opts = ActivityOptions.makeBasic();
1241 
1242             try {
1243                 pendingIntent.send(context, 0, null, null, null, null, opts.toBundle());
1244             } catch (PendingIntent.CanceledException e) {
1245                 Log.e(TAG, "Pending intent canceled", e);
1246             }
1247 
1248             ScreenshotSmartActions.notifyScreenshotAction(
1249                     context, intent.getStringExtra(EXTRA_ID), actionType, true);
1250         }
1251     }
1252 }
1253