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 
21 import static com.android.systemui.screenshot.LogConfig.DEBUG_ANIM;
22 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
23 import static com.android.systemui.screenshot.LogConfig.DEBUG_INPUT;
24 import static com.android.systemui.screenshot.LogConfig.DEBUG_SCROLL;
25 import static com.android.systemui.screenshot.LogConfig.DEBUG_UI;
26 import static com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW;
27 import static com.android.systemui.screenshot.LogConfig.logTag;
28 
29 import static java.util.Objects.requireNonNull;
30 
31 import android.animation.Animator;
32 import android.animation.AnimatorListenerAdapter;
33 import android.animation.AnimatorSet;
34 import android.animation.ValueAnimator;
35 import android.app.ActivityManager;
36 import android.app.Notification;
37 import android.app.PendingIntent;
38 import android.content.Context;
39 import android.content.res.ColorStateList;
40 import android.content.res.Resources;
41 import android.graphics.Bitmap;
42 import android.graphics.BlendMode;
43 import android.graphics.Color;
44 import android.graphics.Insets;
45 import android.graphics.Matrix;
46 import android.graphics.PointF;
47 import android.graphics.Rect;
48 import android.graphics.Region;
49 import android.graphics.drawable.BitmapDrawable;
50 import android.graphics.drawable.ColorDrawable;
51 import android.graphics.drawable.Drawable;
52 import android.graphics.drawable.Icon;
53 import android.graphics.drawable.InsetDrawable;
54 import android.graphics.drawable.LayerDrawable;
55 import android.os.Looper;
56 import android.os.RemoteException;
57 import android.util.AttributeSet;
58 import android.util.DisplayMetrics;
59 import android.util.Log;
60 import android.util.MathUtils;
61 import android.view.Choreographer;
62 import android.view.Display;
63 import android.view.DisplayCutout;
64 import android.view.GestureDetector;
65 import android.view.LayoutInflater;
66 import android.view.MotionEvent;
67 import android.view.ScrollCaptureResponse;
68 import android.view.TouchDelegate;
69 import android.view.View;
70 import android.view.ViewGroup;
71 import android.view.ViewTreeObserver;
72 import android.view.WindowInsets;
73 import android.view.WindowManager;
74 import android.view.WindowMetrics;
75 import android.view.accessibility.AccessibilityManager;
76 import android.view.animation.AccelerateInterpolator;
77 import android.view.animation.AnimationUtils;
78 import android.view.animation.Interpolator;
79 import android.widget.FrameLayout;
80 import android.widget.HorizontalScrollView;
81 import android.widget.ImageView;
82 import android.widget.LinearLayout;
83 
84 import androidx.constraintlayout.widget.ConstraintLayout;
85 
86 import com.android.internal.logging.UiEventLogger;
87 import com.android.systemui.R;
88 import com.android.systemui.screenshot.ScreenshotController.SavedImageData.ActionTransition;
89 import com.android.systemui.shared.system.InputChannelCompat;
90 import com.android.systemui.shared.system.InputMonitorCompat;
91 import com.android.systemui.shared.system.QuickStepContract;
92 
93 import java.util.ArrayList;
94 import java.util.function.Consumer;
95 
96 /**
97  * Handles the visual elements and animations for the screenshot flow.
98  */
99 public class ScreenshotView extends FrameLayout implements
100         ViewTreeObserver.OnComputeInternalInsetsListener {
101 
102     interface ScreenshotViewCallback {
onUserInteraction()103         void onUserInteraction();
104 
onDismiss()105         void onDismiss();
106 
107         /** DOWN motion event was observed outside of the touchable areas of this view. */
onTouchOutside()108         void onTouchOutside();
109     }
110 
111     private static final String TAG = logTag(ScreenshotView.class);
112 
113     private static final long SCREENSHOT_FLASH_IN_DURATION_MS = 133;
114     private static final long SCREENSHOT_FLASH_OUT_DURATION_MS = 217;
115     // delay before starting to fade in dismiss button
116     private static final long SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS = 200;
117     private static final long SCREENSHOT_TO_CORNER_X_DURATION_MS = 234;
118     private static final long SCREENSHOT_TO_CORNER_Y_DURATION_MS = 500;
119     private static final long SCREENSHOT_TO_CORNER_SCALE_DURATION_MS = 234;
120     private static final long SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS = 400;
121     private static final long SCREENSHOT_ACTIONS_ALPHA_DURATION_MS = 100;
122     private static final long SCREENSHOT_DISMISS_X_DURATION_MS = 350;
123     private static final long SCREENSHOT_DISMISS_ALPHA_DURATION_MS = 350;
124     private static final long SCREENSHOT_DISMISS_ALPHA_OFFSET_MS = 50; // delay before starting fade
125     private static final float SCREENSHOT_ACTIONS_START_SCALE_X = .7f;
126     private static final float ROUNDED_CORNER_RADIUS = .25f;
127     private static final int SWIPE_PADDING_DP = 12; // extra padding around views to allow swipe
128 
129     private final Interpolator mAccelerateInterpolator = new AccelerateInterpolator();
130 
131     private final Resources mResources;
132     private final Interpolator mFastOutSlowIn;
133     private final DisplayMetrics mDisplayMetrics;
134     private final float mCornerSizeX;
135     private final float mDismissDeltaY;
136     private final AccessibilityManager mAccessibilityManager;
137 
138     private int mNavMode;
139     private boolean mOrientationPortrait;
140     private boolean mDirectionLTR;
141 
142     private ScreenshotSelectorView mScreenshotSelectorView;
143     private ImageView mScrollingScrim;
144     private View mScreenshotStatic;
145     private ImageView mScreenshotPreview;
146     private View mScreenshotPreviewBorder;
147     private ImageView mScrollablePreview;
148     private ImageView mScreenshotFlash;
149     private ImageView mActionsContainerBackground;
150     private HorizontalScrollView mActionsContainer;
151     private LinearLayout mActionsView;
152     private ImageView mBackgroundProtection;
153     private FrameLayout mDismissButton;
154     private ScreenshotActionChip mShareChip;
155     private ScreenshotActionChip mEditChip;
156     private ScreenshotActionChip mScrollChip;
157     private ScreenshotActionChip mQuickShareChip;
158 
159     private UiEventLogger mUiEventLogger;
160     private ScreenshotViewCallback mCallbacks;
161     private Animator mDismissAnimation;
162     private boolean mPendingSharedTransition;
163     private GestureDetector mSwipeDetector;
164     private SwipeDismissHandler mSwipeDismissHandler;
165     private InputMonitorCompat mInputMonitor;
166     private InputChannelCompat.InputEventReceiver mInputEventReceiver;
167     private boolean mShowScrollablePreview;
168     private String mPackageName = "";
169 
170     private final ArrayList<ScreenshotActionChip> mSmartChips = new ArrayList<>();
171     private PendingInteraction mPendingInteraction;
172 
173     private enum PendingInteraction {
174         PREVIEW,
175         EDIT,
176         SHARE,
177         QUICK_SHARE
178     }
179 
ScreenshotView(Context context)180     public ScreenshotView(Context context) {
181         this(context, null);
182     }
183 
ScreenshotView(Context context, AttributeSet attrs)184     public ScreenshotView(Context context, AttributeSet attrs) {
185         this(context, attrs, 0);
186     }
187 
ScreenshotView(Context context, AttributeSet attrs, int defStyleAttr)188     public ScreenshotView(Context context, AttributeSet attrs, int defStyleAttr) {
189         this(context, attrs, defStyleAttr, 0);
190     }
191 
ScreenshotView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)192     public ScreenshotView(
193             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
194         super(context, attrs, defStyleAttr, defStyleRes);
195         mResources = mContext.getResources();
196 
197         mCornerSizeX = mResources.getDimensionPixelSize(R.dimen.global_screenshot_x_scale);
198         mDismissDeltaY = mResources.getDimensionPixelSize(
199                 R.dimen.screenshot_dismissal_height_delta);
200 
201         // standard material ease
202         mFastOutSlowIn =
203                 AnimationUtils.loadInterpolator(mContext, android.R.interpolator.fast_out_slow_in);
204 
205         mDisplayMetrics = new DisplayMetrics();
206         mContext.getDisplay().getRealMetrics(mDisplayMetrics);
207 
208         mAccessibilityManager = AccessibilityManager.getInstance(mContext);
209 
210         mSwipeDetector = new GestureDetector(mContext,
211                 new GestureDetector.SimpleOnGestureListener() {
212                     final Rect mActionsRect = new Rect();
213 
214                     @Override
215                     public boolean onScroll(
216                             MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
217                         mActionsContainer.getBoundsOnScreen(mActionsRect);
218                         // return true if we aren't in the actions bar, or if we are but it isn't
219                         // scrollable in the direction of movement
220                         return !mActionsRect.contains((int) ev2.getRawX(), (int) ev2.getRawY())
221                                 || !mActionsContainer.canScrollHorizontally((int) distanceX);
222                     }
223                 });
224         mSwipeDetector.setIsLongpressEnabled(false);
225         mSwipeDismissHandler = new SwipeDismissHandler();
226         addOnAttachStateChangeListener(new OnAttachStateChangeListener() {
227             @Override
228             public void onViewAttachedToWindow(View v) {
229                 startInputListening();
230             }
231 
232             @Override
233             public void onViewDetachedFromWindow(View v) {
234                 stopInputListening();
235             }
236         });
237     }
238 
hideScrollChip()239     public void hideScrollChip() {
240         mScrollChip.setVisibility(View.GONE);
241     }
242 
243     /**
244      * Called to display the scroll action chip when support is detected.
245      *
246      * @param packageName the owning package of the window to be captured
247      * @param onClick the action to take when the chip is clicked.
248      */
showScrollChip(String packageName, Runnable onClick)249     public void showScrollChip(String packageName, Runnable onClick) {
250         if (DEBUG_SCROLL) {
251             Log.d(TAG, "Showing Scroll option");
252         }
253         mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_IMPRESSION, 0, packageName);
254         mScrollChip.setVisibility(VISIBLE);
255         mScrollChip.setOnClickListener((v) -> {
256             if (DEBUG_INPUT) {
257                 Log.d(TAG, "scroll chip tapped");
258             }
259             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_LONG_SCREENSHOT_REQUESTED, 0,
260                     packageName);
261             onClick.run();
262         });
263     }
264 
265     @Override // ViewTreeObserver.OnComputeInternalInsetsListener
onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo)266     public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo inoutInfo) {
267         inoutInfo.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
268         inoutInfo.touchableRegion.set(getTouchRegion(true));
269     }
270 
getTouchRegion(boolean includeScrim)271     private Region getTouchRegion(boolean includeScrim) {
272         Region touchRegion = new Region();
273 
274         final Rect tmpRect = new Rect();
275         mScreenshotPreview.getBoundsOnScreen(tmpRect);
276         tmpRect.inset((int) dpToPx(-SWIPE_PADDING_DP), (int) dpToPx(-SWIPE_PADDING_DP));
277         touchRegion.op(tmpRect, Region.Op.UNION);
278         mActionsContainerBackground.getBoundsOnScreen(tmpRect);
279         tmpRect.inset((int) dpToPx(-SWIPE_PADDING_DP), (int) dpToPx(-SWIPE_PADDING_DP));
280         touchRegion.op(tmpRect, Region.Op.UNION);
281         mDismissButton.getBoundsOnScreen(tmpRect);
282         touchRegion.op(tmpRect, Region.Op.UNION);
283 
284         if (includeScrim && mScrollingScrim.getVisibility() == View.VISIBLE) {
285             mScrollingScrim.getBoundsOnScreen(tmpRect);
286             touchRegion.op(tmpRect, Region.Op.UNION);
287         }
288 
289         if (QuickStepContract.isGesturalMode(mNavMode)) {
290             final WindowManager wm = mContext.getSystemService(WindowManager.class);
291             final WindowMetrics windowMetrics = wm.getCurrentWindowMetrics();
292             final Insets gestureInsets = windowMetrics.getWindowInsets().getInsets(
293                     WindowInsets.Type.systemGestures());
294             // Receive touches in gesture insets such that they don't cause TOUCH_OUTSIDE
295             Rect inset = new Rect(0, 0, gestureInsets.left, mDisplayMetrics.heightPixels);
296             touchRegion.op(inset, Region.Op.UNION);
297             inset.set(mDisplayMetrics.widthPixels - gestureInsets.right, 0,
298                     mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels);
299             touchRegion.op(inset, Region.Op.UNION);
300         }
301         return touchRegion;
302     }
303 
startInputListening()304     private void startInputListening() {
305         stopInputListening();
306         mInputMonitor = new InputMonitorCompat("Screenshot", Display.DEFAULT_DISPLAY);
307         mInputEventReceiver = mInputMonitor.getInputReceiver(
308                 Looper.getMainLooper(), Choreographer.getInstance(), ev -> {
309                     if (ev instanceof MotionEvent) {
310                         MotionEvent event = (MotionEvent) ev;
311                         if (event.getActionMasked() == MotionEvent.ACTION_DOWN
312                                 && !getTouchRegion(false).contains(
313                                 (int) event.getRawX(), (int) event.getRawY())) {
314                             mCallbacks.onTouchOutside();
315                         }
316                     }
317                 });
318     }
319 
stopInputListening()320     void stopInputListening() {
321         if (mInputMonitor != null) {
322             mInputMonitor.dispose();
323             mInputMonitor = null;
324         }
325         if (mInputEventReceiver != null) {
326             mInputEventReceiver.dispose();
327             mInputEventReceiver = null;
328         }
329     }
330 
331     @Override // ViewGroup
onInterceptTouchEvent(MotionEvent ev)332     public boolean onInterceptTouchEvent(MotionEvent ev) {
333         // scrolling scrim should not be swipeable; return early if we're on the scrim
334         if (!getTouchRegion(false).contains((int) ev.getRawX(), (int) ev.getRawY())) {
335             return false;
336         }
337         // always pass through the down event so the swipe handler knows the initial state
338         if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
339             mSwipeDismissHandler.onTouch(this, ev);
340         }
341         return mSwipeDetector.onTouchEvent(ev);
342     }
343 
344     @Override // View
onFinishInflate()345     protected void onFinishInflate() {
346         mScrollingScrim = requireNonNull(findViewById(R.id.screenshot_scrolling_scrim));
347         mScreenshotStatic = requireNonNull(findViewById(R.id.global_screenshot_static));
348         mScreenshotPreview = requireNonNull(findViewById(R.id.global_screenshot_preview));
349         mScreenshotPreviewBorder = requireNonNull(
350                 findViewById(R.id.global_screenshot_preview_border));
351         mScreenshotPreview.setClipToOutline(true);
352 
353         mActionsContainerBackground = requireNonNull(findViewById(
354                 R.id.global_screenshot_actions_container_background));
355         mActionsContainer = requireNonNull(findViewById(R.id.global_screenshot_actions_container));
356         mActionsView = requireNonNull(findViewById(R.id.global_screenshot_actions));
357         mBackgroundProtection = requireNonNull(
358                 findViewById(R.id.global_screenshot_actions_background));
359         mDismissButton = requireNonNull(findViewById(R.id.global_screenshot_dismiss_button));
360         mScrollablePreview = requireNonNull(findViewById(R.id.screenshot_scrollable_preview));
361         mScreenshotFlash = requireNonNull(findViewById(R.id.global_screenshot_flash));
362         mScreenshotSelectorView = requireNonNull(findViewById(R.id.global_screenshot_selector));
363         mShareChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_share_chip));
364         mEditChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_edit_chip));
365         mScrollChip = requireNonNull(mActionsContainer.findViewById(R.id.screenshot_scroll_chip));
366 
367         int swipePaddingPx = (int) dpToPx(SWIPE_PADDING_DP);
368         TouchDelegate previewDelegate = new TouchDelegate(
369                 new Rect(swipePaddingPx, swipePaddingPx, swipePaddingPx, swipePaddingPx),
370                 mScreenshotPreview);
371         mScreenshotPreview.setTouchDelegate(previewDelegate);
372         TouchDelegate actionsDelegate = new TouchDelegate(
373                 new Rect(swipePaddingPx, swipePaddingPx, swipePaddingPx, swipePaddingPx),
374                 mActionsContainerBackground);
375         mActionsContainerBackground.setTouchDelegate(actionsDelegate);
376 
377         setFocusable(true);
378         mScreenshotSelectorView.setFocusable(true);
379         mScreenshotSelectorView.setFocusableInTouchMode(true);
380         mActionsContainer.setScrollX(0);
381 
382         mNavMode = getResources().getInteger(
383                 com.android.internal.R.integer.config_navBarInteractionMode);
384         mOrientationPortrait =
385                 getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT;
386         mDirectionLTR =
387                 getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
388 
389         // Get focus so that the key events go to the layout.
390         setFocusableInTouchMode(true);
391         requestFocus();
392     }
393 
getScreenshotPreview()394     View getScreenshotPreview() {
395         return mScreenshotPreview;
396     }
397 
398     /**
399      * Set up the logger and callback on dismissal.
400      *
401      * Note: must be called before any other (non-constructor) method or null pointer exceptions
402      * may occur.
403      */
init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks)404     void init(UiEventLogger uiEventLogger, ScreenshotViewCallback callbacks) {
405         mUiEventLogger = uiEventLogger;
406         mCallbacks = callbacks;
407     }
408 
takePartialScreenshot(Consumer<Rect> onPartialScreenshotSelected)409     void takePartialScreenshot(Consumer<Rect> onPartialScreenshotSelected) {
410         mScreenshotSelectorView.setOnScreenshotSelected(onPartialScreenshotSelected);
411         mScreenshotSelectorView.setVisibility(View.VISIBLE);
412         mScreenshotSelectorView.requestFocus();
413     }
414 
setScreenshot(Bitmap bitmap, Insets screenInsets)415     void setScreenshot(Bitmap bitmap, Insets screenInsets) {
416         mScreenshotPreview.setImageDrawable(createScreenDrawable(mResources, bitmap, screenInsets));
417     }
418 
setPackageName(String packageName)419     void setPackageName(String packageName) {
420         mPackageName = packageName;
421     }
422 
updateInsets(WindowInsets insets)423     void updateInsets(WindowInsets insets) {
424         int orientation = mContext.getResources().getConfiguration().orientation;
425         mOrientationPortrait = (orientation == ORIENTATION_PORTRAIT);
426         FrameLayout.LayoutParams p =
427                 (FrameLayout.LayoutParams) mScreenshotStatic.getLayoutParams();
428         DisplayCutout cutout = insets.getDisplayCutout();
429         Insets navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars());
430         if (cutout == null) {
431             p.setMargins(0, 0, 0, navBarInsets.bottom);
432         } else {
433             Insets waterfall = cutout.getWaterfallInsets();
434             if (mOrientationPortrait) {
435                 p.setMargins(
436                         waterfall.left,
437                         Math.max(cutout.getSafeInsetTop(), waterfall.top),
438                         waterfall.right,
439                         Math.max(cutout.getSafeInsetBottom(),
440                                 Math.max(navBarInsets.bottom, waterfall.bottom)));
441             } else {
442                 p.setMargins(
443                         Math.max(cutout.getSafeInsetLeft(), waterfall.left),
444                         waterfall.top,
445                         Math.max(cutout.getSafeInsetRight(), waterfall.right),
446                         Math.max(navBarInsets.bottom, waterfall.bottom));
447             }
448         }
449         mScreenshotStatic.setLayoutParams(p);
450         mScreenshotStatic.requestLayout();
451     }
452 
updateOrientation(WindowInsets insets)453     void updateOrientation(WindowInsets insets) {
454         int orientation = mContext.getResources().getConfiguration().orientation;
455         mOrientationPortrait = (orientation == ORIENTATION_PORTRAIT);
456         updateInsets(insets);
457         int screenshotFixedSize =
458                 mContext.getResources().getDimensionPixelSize(R.dimen.global_screenshot_x_scale);
459         ViewGroup.LayoutParams params = mScreenshotPreview.getLayoutParams();
460         if (mOrientationPortrait) {
461             params.width = screenshotFixedSize;
462             params.height = LayoutParams.WRAP_CONTENT;
463             mScreenshotPreview.setScaleType(ImageView.ScaleType.FIT_START);
464         } else {
465             params.width = LayoutParams.WRAP_CONTENT;
466             params.height = screenshotFixedSize;
467             mScreenshotPreview.setScaleType(ImageView.ScaleType.FIT_END);
468         }
469 
470         mScreenshotPreview.setLayoutParams(params);
471     }
472 
createScreenshotDropInAnimation(Rect bounds, boolean showFlash)473     AnimatorSet createScreenshotDropInAnimation(Rect bounds, boolean showFlash) {
474         if (DEBUG_ANIM) {
475             Log.d(TAG, "createAnim: bounds=" + bounds + " showFlash=" + showFlash);
476         }
477 
478         Rect targetPosition = new Rect();
479         mScreenshotPreview.getHitRect(targetPosition);
480 
481         // ratio of preview width, end vs. start size
482         float cornerScale =
483                 mCornerSizeX / (mOrientationPortrait ? bounds.width() : bounds.height());
484         final float currentScale = 1 / cornerScale;
485 
486         AnimatorSet dropInAnimation = new AnimatorSet();
487         ValueAnimator flashInAnimator = ValueAnimator.ofFloat(0, 1);
488         flashInAnimator.setDuration(SCREENSHOT_FLASH_IN_DURATION_MS);
489         flashInAnimator.setInterpolator(mFastOutSlowIn);
490         flashInAnimator.addUpdateListener(animation ->
491                 mScreenshotFlash.setAlpha((float) animation.getAnimatedValue()));
492 
493         ValueAnimator flashOutAnimator = ValueAnimator.ofFloat(1, 0);
494         flashOutAnimator.setDuration(SCREENSHOT_FLASH_OUT_DURATION_MS);
495         flashOutAnimator.setInterpolator(mFastOutSlowIn);
496         flashOutAnimator.addUpdateListener(animation ->
497                 mScreenshotFlash.setAlpha((float) animation.getAnimatedValue()));
498 
499         // animate from the current location, to the static preview location
500         final PointF startPos = new PointF(bounds.centerX(), bounds.centerY());
501         final PointF finalPos = new PointF(targetPosition.exactCenterX(),
502                 targetPosition.exactCenterY());
503 
504         // Shift to screen coordinates so that the animation runs on top of the entire screen,
505         // including e.g. bars covering the display cutout.
506         int[] locInScreen = mScreenshotPreview.getLocationOnScreen();
507         startPos.offset(targetPosition.left - locInScreen[0], targetPosition.top - locInScreen[1]);
508 
509         if (DEBUG_ANIM) {
510             Log.d(TAG, "toCorner: startPos=" + startPos);
511             Log.d(TAG, "toCorner: finalPos=" + finalPos);
512         }
513 
514         ValueAnimator toCorner = ValueAnimator.ofFloat(0, 1);
515         toCorner.setDuration(SCREENSHOT_TO_CORNER_Y_DURATION_MS);
516 
517         toCorner.addListener(new AnimatorListenerAdapter() {
518             @Override
519             public void onAnimationStart(Animator animation) {
520                 mScreenshotPreview.setScaleX(currentScale);
521                 mScreenshotPreview.setScaleY(currentScale);
522                 mScreenshotPreview.setVisibility(View.VISIBLE);
523                 if (mAccessibilityManager.isEnabled()) {
524                     mDismissButton.setAlpha(0);
525                     mDismissButton.setVisibility(View.VISIBLE);
526                 }
527             }
528         });
529 
530         float xPositionPct =
531                 SCREENSHOT_TO_CORNER_X_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
532         float dismissPct =
533                 SCREENSHOT_TO_CORNER_DISMISS_DELAY_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
534         float scalePct =
535                 SCREENSHOT_TO_CORNER_SCALE_DURATION_MS / (float) SCREENSHOT_TO_CORNER_Y_DURATION_MS;
536         toCorner.addUpdateListener(animation -> {
537             float t = animation.getAnimatedFraction();
538             if (t < scalePct) {
539                 float scale = MathUtils.lerp(
540                         currentScale, 1, mFastOutSlowIn.getInterpolation(t / scalePct));
541                 mScreenshotPreview.setScaleX(scale);
542                 mScreenshotPreview.setScaleY(scale);
543             } else {
544                 mScreenshotPreview.setScaleX(1);
545                 mScreenshotPreview.setScaleY(1);
546             }
547 
548             if (t < xPositionPct) {
549                 float xCenter = MathUtils.lerp(startPos.x, finalPos.x,
550                         mFastOutSlowIn.getInterpolation(t / xPositionPct));
551                 mScreenshotPreview.setX(xCenter - mScreenshotPreview.getWidth() / 2f);
552             } else {
553                 mScreenshotPreview.setX(finalPos.x - mScreenshotPreview.getWidth() / 2f);
554             }
555             float yCenter = MathUtils.lerp(
556                     startPos.y, finalPos.y, mFastOutSlowIn.getInterpolation(t));
557             mScreenshotPreview.setY(yCenter - mScreenshotPreview.getHeight() / 2f);
558 
559             if (t >= dismissPct) {
560                 mDismissButton.setAlpha((t - dismissPct) / (1 - dismissPct));
561                 float currentX = mScreenshotPreview.getX();
562                 float currentY = mScreenshotPreview.getY();
563                 mDismissButton.setY(currentY - mDismissButton.getHeight() / 2f);
564                 if (mDirectionLTR) {
565                     mDismissButton.setX(currentX + mScreenshotPreview.getWidth()
566                             - mDismissButton.getWidth() / 2f);
567                 } else {
568                     mDismissButton.setX(currentX - mDismissButton.getWidth() / 2f);
569                 }
570             }
571         });
572 
573         mScreenshotFlash.setAlpha(0f);
574         mScreenshotFlash.setVisibility(View.VISIBLE);
575 
576         ValueAnimator borderFadeIn = ValueAnimator.ofFloat(0, 1);
577         borderFadeIn.setDuration(100);
578         borderFadeIn.addUpdateListener((animation) ->
579                 mScreenshotPreviewBorder.setAlpha(animation.getAnimatedFraction()));
580 
581         if (showFlash) {
582             dropInAnimation.play(flashOutAnimator).after(flashInAnimator);
583             dropInAnimation.play(flashOutAnimator).with(toCorner);
584         } else {
585             dropInAnimation.play(toCorner);
586         }
587         dropInAnimation.play(borderFadeIn).after(toCorner);
588 
589         dropInAnimation.addListener(new AnimatorListenerAdapter() {
590             @Override
591             public void onAnimationEnd(Animator animation) {
592                 if (DEBUG_ANIM) {
593                     Log.d(TAG, "drop-in animation ended");
594                 }
595                 mDismissButton.setOnClickListener(view -> {
596                     if (DEBUG_INPUT) {
597                         Log.d(TAG, "dismiss button clicked");
598                     }
599                     mUiEventLogger.log(
600                             ScreenshotEvent.SCREENSHOT_EXPLICIT_DISMISSAL, 0, mPackageName);
601                     animateDismissal();
602                 });
603                 mDismissButton.setAlpha(1);
604                 float dismissOffset = mDismissButton.getWidth() / 2f;
605                 float finalDismissX = mDirectionLTR
606                         ? finalPos.x - dismissOffset + bounds.width() * cornerScale / 2f
607                         : finalPos.x - dismissOffset - bounds.width() * cornerScale / 2f;
608                 mDismissButton.setX(finalDismissX);
609                 mDismissButton.setY(
610                         finalPos.y - dismissOffset - bounds.height() * cornerScale / 2f);
611                 mScreenshotPreview.setScaleX(1);
612                 mScreenshotPreview.setScaleY(1);
613                 mScreenshotPreview.setX(finalPos.x - mScreenshotPreview.getWidth() / 2f);
614                 mScreenshotPreview.setY(finalPos.y - mScreenshotPreview.getHeight() / 2f);
615                 requestLayout();
616 
617                 createScreenshotActionsShadeAnimation().start();
618 
619                 setOnTouchListener(mSwipeDismissHandler);
620             }
621         });
622 
623         return dropInAnimation;
624     }
625 
createScreenshotActionsShadeAnimation()626     ValueAnimator createScreenshotActionsShadeAnimation() {
627         // By default the activities won't be able to start immediately; override this to keep
628         // the same behavior as if started from a notification
629         try {
630             ActivityManager.getService().resumeAppSwitches();
631         } catch (RemoteException e) {
632         }
633 
634         ArrayList<ScreenshotActionChip> chips = new ArrayList<>();
635 
636         mShareChip.setContentDescription(mContext.getString(R.string.screenshot_share_description));
637         mShareChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true);
638         mShareChip.setOnClickListener(v -> {
639             mShareChip.setIsPending(true);
640             mEditChip.setIsPending(false);
641             if (mQuickShareChip != null) {
642                 mQuickShareChip.setIsPending(false);
643             }
644             mPendingInteraction = PendingInteraction.SHARE;
645         });
646         chips.add(mShareChip);
647 
648         mEditChip.setContentDescription(mContext.getString(R.string.screenshot_edit_description));
649         mEditChip.setIcon(Icon.createWithResource(mContext, R.drawable.ic_screenshot_edit), true);
650         mEditChip.setOnClickListener(v -> {
651             mEditChip.setIsPending(true);
652             mShareChip.setIsPending(false);
653             if (mQuickShareChip != null) {
654                 mQuickShareChip.setIsPending(false);
655             }
656             mPendingInteraction = PendingInteraction.EDIT;
657         });
658         chips.add(mEditChip);
659 
660         mScreenshotPreview.setOnClickListener(v -> {
661             mShareChip.setIsPending(false);
662             mEditChip.setIsPending(false);
663             if (mQuickShareChip != null) {
664                 mQuickShareChip.setIsPending(false);
665             }
666             mPendingInteraction = PendingInteraction.PREVIEW;
667         });
668 
669         mScrollChip.setText(mContext.getString(R.string.screenshot_scroll_label));
670         mScrollChip.setIcon(Icon.createWithResource(mContext,
671                 R.drawable.ic_screenshot_scroll), true);
672         chips.add(mScrollChip);
673 
674         // remove the margin from the last chip so that it's correctly aligned with the end
675         LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)
676                 mActionsView.getChildAt(0).getLayoutParams();
677         params.setMarginEnd(0);
678         mActionsView.getChildAt(0).setLayoutParams(params);
679 
680         ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
681         animator.setDuration(SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS);
682         float alphaFraction = (float) SCREENSHOT_ACTIONS_ALPHA_DURATION_MS
683                 / SCREENSHOT_ACTIONS_EXPANSION_DURATION_MS;
684         mActionsContainer.setAlpha(0f);
685         mActionsContainerBackground.setAlpha(0f);
686         mActionsContainer.setVisibility(View.VISIBLE);
687         mActionsContainerBackground.setVisibility(View.VISIBLE);
688 
689         animator.addUpdateListener(animation -> {
690             float t = animation.getAnimatedFraction();
691             mBackgroundProtection.setAlpha(t);
692             float containerAlpha = t < alphaFraction ? t / alphaFraction : 1;
693             mActionsContainer.setAlpha(containerAlpha);
694             mActionsContainerBackground.setAlpha(containerAlpha);
695             float containerScale = SCREENSHOT_ACTIONS_START_SCALE_X
696                     + (t * (1 - SCREENSHOT_ACTIONS_START_SCALE_X));
697             mActionsContainer.setScaleX(containerScale);
698             mActionsContainerBackground.setScaleX(containerScale);
699             for (ScreenshotActionChip chip : chips) {
700                 chip.setAlpha(t);
701                 chip.setScaleX(1 / containerScale); // invert to keep size of children constant
702             }
703             mActionsContainer.setScrollX(mDirectionLTR ? 0 : mActionsContainer.getWidth());
704             mActionsContainer.setPivotX(mDirectionLTR ? 0 : mActionsContainer.getWidth());
705             mActionsContainerBackground.setPivotX(
706                     mDirectionLTR ? 0 : mActionsContainerBackground.getWidth());
707         });
708         return animator;
709     }
710 
setChipIntents(ScreenshotController.SavedImageData imageData)711     void setChipIntents(ScreenshotController.SavedImageData imageData) {
712         mShareChip.setOnClickListener(v -> {
713             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
714             startSharedTransition(
715                     imageData.shareTransition.get());
716         });
717         mEditChip.setOnClickListener(v -> {
718             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_EDIT_TAPPED, 0, mPackageName);
719             startSharedTransition(
720                     imageData.editTransition.get());
721         });
722         mScreenshotPreview.setOnClickListener(v -> {
723             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_PREVIEW_TAPPED, 0, mPackageName);
724             startSharedTransition(
725                     imageData.editTransition.get());
726         });
727         if (mQuickShareChip != null) {
728             mQuickShareChip.setPendingIntent(imageData.quickShareAction.actionIntent,
729                     () -> {
730                         mUiEventLogger.log(
731                                 ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED, 0, mPackageName);
732                         animateDismissal();
733                     });
734         }
735 
736         if (mPendingInteraction != null) {
737             switch (mPendingInteraction) {
738                 case PREVIEW:
739                     mScreenshotPreview.callOnClick();
740                     break;
741                 case SHARE:
742                     mShareChip.callOnClick();
743                     break;
744                 case EDIT:
745                     mEditChip.callOnClick();
746                     break;
747                 case QUICK_SHARE:
748                     mQuickShareChip.callOnClick();
749                     break;
750             }
751         } else {
752             LayoutInflater inflater = LayoutInflater.from(mContext);
753 
754             for (Notification.Action smartAction : imageData.smartActions) {
755                 ScreenshotActionChip actionChip = (ScreenshotActionChip) inflater.inflate(
756                         R.layout.global_screenshot_action_chip, mActionsView, false);
757                 actionChip.setText(smartAction.title);
758                 actionChip.setIcon(smartAction.getIcon(), false);
759                 actionChip.setPendingIntent(smartAction.actionIntent,
760                         () -> {
761                             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SMART_ACTION_TAPPED,
762                                     0, mPackageName);
763                             animateDismissal();
764                         });
765                 actionChip.setAlpha(1);
766                 mActionsView.addView(actionChip);
767                 mSmartChips.add(actionChip);
768             }
769         }
770     }
771 
addQuickShareChip(Notification.Action quickShareAction)772     void addQuickShareChip(Notification.Action quickShareAction) {
773         if (mPendingInteraction == null) {
774             LayoutInflater inflater = LayoutInflater.from(mContext);
775             mQuickShareChip = (ScreenshotActionChip) inflater.inflate(
776                     R.layout.global_screenshot_action_chip, mActionsView, false);
777             mQuickShareChip.setText(quickShareAction.title);
778             mQuickShareChip.setIcon(quickShareAction.getIcon(), false);
779             mQuickShareChip.setOnClickListener(v -> {
780                 mShareChip.setIsPending(false);
781                 mEditChip.setIsPending(false);
782                 mQuickShareChip.setIsPending(true);
783                 mPendingInteraction = PendingInteraction.QUICK_SHARE;
784             });
785             mQuickShareChip.setAlpha(1);
786             mActionsView.addView(mQuickShareChip);
787             mSmartChips.add(mQuickShareChip);
788         }
789     }
790 
scrollableAreaOnScreen(ScrollCaptureResponse response)791     private Rect scrollableAreaOnScreen(ScrollCaptureResponse response) {
792         Rect r = new Rect(response.getBoundsInWindow());
793         Rect windowInScreen = response.getWindowBounds();
794         r.offset(windowInScreen.left, windowInScreen.top);
795         r.intersect(new Rect(0, 0, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels));
796         return r;
797     }
798 
startLongScreenshotTransition(Rect destination, Runnable onTransitionEnd, ScrollCaptureController.LongScreenshot longScreenshot)799     void startLongScreenshotTransition(Rect destination, Runnable onTransitionEnd,
800             ScrollCaptureController.LongScreenshot longScreenshot) {
801         AnimatorSet animSet = new AnimatorSet();
802 
803         ValueAnimator scrimAnim = ValueAnimator.ofFloat(0, 1);
804         scrimAnim.addUpdateListener(animation ->
805                 mScrollingScrim.setAlpha(1 - animation.getAnimatedFraction()));
806 
807         if (mShowScrollablePreview) {
808             mScrollablePreview.setImageBitmap(longScreenshot.toBitmap());
809             float startX = mScrollablePreview.getX();
810             float startY = mScrollablePreview.getY();
811             int[] locInScreen = mScrollablePreview.getLocationOnScreen();
812             destination.offset((int) startX - locInScreen[0], (int) startY - locInScreen[1]);
813             mScrollablePreview.setPivotX(0);
814             mScrollablePreview.setPivotY(0);
815             mScrollablePreview.setAlpha(1f);
816             float currentScale = mScrollablePreview.getWidth() / (float) longScreenshot.getWidth();
817             Matrix matrix = new Matrix();
818             matrix.setScale(currentScale, currentScale);
819             matrix.postTranslate(
820                     longScreenshot.getLeft() * currentScale,
821                     longScreenshot.getTop() * currentScale);
822             mScrollablePreview.setImageMatrix(matrix);
823             float destinationScale = destination.width() / (float) mScrollablePreview.getWidth();
824 
825             ValueAnimator previewAnim = ValueAnimator.ofFloat(0, 1);
826             previewAnim.addUpdateListener(animation -> {
827                 float t = animation.getAnimatedFraction();
828                 float currScale = MathUtils.lerp(1, destinationScale, t);
829                 mScrollablePreview.setScaleX(currScale);
830                 mScrollablePreview.setScaleY(currScale);
831                 mScrollablePreview.setX(MathUtils.lerp(startX, destination.left, t));
832                 mScrollablePreview.setY(MathUtils.lerp(startY, destination.top, t));
833             });
834             ValueAnimator previewFadeAnim = ValueAnimator.ofFloat(1, 0);
835             previewFadeAnim.addUpdateListener(animation ->
836                     mScrollablePreview.setAlpha(1 - animation.getAnimatedFraction()));
837             animSet.play(previewAnim).with(scrimAnim).before(previewFadeAnim);
838             previewAnim.addListener(new AnimatorListenerAdapter() {
839                 @Override
840                 public void onAnimationEnd(Animator animation) {
841                     super.onAnimationEnd(animation);
842                     onTransitionEnd.run();
843                 }
844             });
845         } else {
846             // if we switched orientations between the original screenshot and the long screenshot
847             // capture, just fade out the scrim instead of running the preview animation
848             animSet.play(scrimAnim);
849             animSet.addListener(new AnimatorListenerAdapter() {
850                 @Override
851                 public void onAnimationEnd(Animator animation) {
852                     super.onAnimationEnd(animation);
853                     onTransitionEnd.run();
854                 }
855             });
856         }
857         animSet.addListener(new AnimatorListenerAdapter() {
858             @Override
859             public void onAnimationEnd(Animator animation) {
860                 super.onAnimationEnd(animation);
861                         mCallbacks.onDismiss();
862                     }
863         });
864         animSet.start();
865     }
866 
prepareScrollingTransition(ScrollCaptureResponse response, Bitmap screenBitmap, Bitmap newBitmap, boolean screenshotTakenInPortrait)867     void prepareScrollingTransition(ScrollCaptureResponse response, Bitmap screenBitmap,
868             Bitmap newBitmap, boolean screenshotTakenInPortrait) {
869         mShowScrollablePreview = (screenshotTakenInPortrait == mOrientationPortrait);
870 
871         mScrollingScrim.setImageBitmap(newBitmap);
872         mScrollingScrim.setVisibility(View.VISIBLE);
873 
874         if (mShowScrollablePreview) {
875             Rect scrollableArea = scrollableAreaOnScreen(response);
876 
877             float scale = mCornerSizeX
878                     / (mOrientationPortrait ? screenBitmap.getWidth() : screenBitmap.getHeight());
879             ConstraintLayout.LayoutParams params =
880                     (ConstraintLayout.LayoutParams) mScrollablePreview.getLayoutParams();
881 
882             params.width = (int) (scale * scrollableArea.width());
883             params.height = (int) (scale * scrollableArea.height());
884             Matrix matrix = new Matrix();
885             matrix.setScale(scale, scale);
886             matrix.postTranslate(-scrollableArea.left * scale, -scrollableArea.top * scale);
887 
888             mScrollablePreview.setTranslationX(scale
889                     * (mDirectionLTR ? scrollableArea.left : scrollableArea.right - getWidth()));
890             mScrollablePreview.setTranslationY(scale * scrollableArea.top);
891             mScrollablePreview.setImageMatrix(matrix);
892             mScrollablePreview.setImageBitmap(screenBitmap);
893             mScrollablePreview.setVisibility(View.VISIBLE);
894         }
895         mDismissButton.setVisibility(View.GONE);
896         mActionsContainer.setVisibility(View.GONE);
897         mBackgroundProtection.setVisibility(View.GONE);
898         // set these invisible, but not gone, so that the views are laid out correctly
899         mActionsContainerBackground.setVisibility(View.INVISIBLE);
900         mScreenshotPreviewBorder.setVisibility(View.INVISIBLE);
901         mScreenshotPreview.setVisibility(View.INVISIBLE);
902         mScrollingScrim.setImageTintBlendMode(BlendMode.SRC_ATOP);
903         ValueAnimator anim = ValueAnimator.ofFloat(0, .3f);
904         anim.addUpdateListener(animation -> mScrollingScrim.setImageTintList(
905                 ColorStateList.valueOf(Color.argb((float) animation.getAnimatedValue(), 0, 0, 0))));
906         anim.setDuration(200);
907         anim.start();
908     }
909 
restoreNonScrollingUi()910     void restoreNonScrollingUi() {
911         mScrollChip.setVisibility(View.GONE);
912         mScrollablePreview.setVisibility(View.GONE);
913         mScrollingScrim.setVisibility(View.GONE);
914 
915         if (mAccessibilityManager.isEnabled()) {
916             mDismissButton.setVisibility(View.VISIBLE);
917         }
918         mActionsContainer.setVisibility(View.VISIBLE);
919         mBackgroundProtection.setVisibility(View.VISIBLE);
920         mActionsContainerBackground.setVisibility(View.VISIBLE);
921         mScreenshotPreviewBorder.setVisibility(View.VISIBLE);
922         mScreenshotPreview.setVisibility(View.VISIBLE);
923         // reset the timeout
924         mCallbacks.onUserInteraction();
925     }
926 
isDismissing()927     boolean isDismissing() {
928         return (mDismissAnimation != null && mDismissAnimation.isRunning());
929     }
930 
isPendingSharedTransition()931     boolean isPendingSharedTransition() {
932         return mPendingSharedTransition;
933     }
934 
animateDismissal()935     void animateDismissal() {
936         animateDismissal(createScreenshotTranslateDismissAnimation());
937     }
938 
animateDismissal(Animator dismissAnimation)939     private void animateDismissal(Animator dismissAnimation) {
940         mDismissAnimation = dismissAnimation;
941         mDismissAnimation.addListener(new AnimatorListenerAdapter() {
942             private boolean mCancelled = false;
943 
944             @Override
945             public void onAnimationCancel(Animator animation) {
946                 super.onAnimationCancel(animation);
947                 if (DEBUG_ANIM) {
948                     Log.d(TAG, "Cancelled dismiss animation");
949                 }
950                 mCancelled = true;
951             }
952 
953             @Override
954             public void onAnimationEnd(Animator animation) {
955                 super.onAnimationEnd(animation);
956                 if (!mCancelled) {
957                     if (DEBUG_ANIM) {
958                         Log.d(TAG, "after dismiss animation, calling onDismissRunnable.run()");
959                     }
960                     mCallbacks.onDismiss();
961                 }
962             }
963         });
964         if (DEBUG_ANIM) {
965             Log.d(TAG, "Starting dismiss animation");
966         }
967         mDismissAnimation.start();
968     }
969 
reset()970     void reset() {
971         if (DEBUG_UI) {
972             Log.d(TAG, "reset screenshot view");
973         }
974 
975         if (mDismissAnimation != null && mDismissAnimation.isRunning()) {
976             if (DEBUG_ANIM) {
977                 Log.d(TAG, "cancelling dismiss animation");
978             }
979             mDismissAnimation.cancel();
980         }
981         if (DEBUG_WINDOW) {
982             Log.d(TAG, "removing OnComputeInternalInsetsListener");
983         }
984         // Make sure we clean up the view tree observer
985         getViewTreeObserver().removeOnComputeInternalInsetsListener(this);
986         // Clear any references to the bitmap
987         mScreenshotPreview.setImageDrawable(null);
988         mScreenshotPreview.setVisibility(View.INVISIBLE);
989         mScreenshotPreviewBorder.setAlpha(0);
990         mPendingSharedTransition = false;
991         mActionsContainerBackground.setVisibility(View.GONE);
992         mActionsContainer.setVisibility(View.GONE);
993         mBackgroundProtection.setAlpha(0f);
994         mDismissButton.setVisibility(View.GONE);
995         mScrollingScrim.setVisibility(View.GONE);
996         mScrollablePreview.setVisibility(View.GONE);
997         mScreenshotStatic.setTranslationX(0);
998         mScreenshotPreview.setContentDescription(
999                 mContext.getResources().getString(R.string.screenshot_preview_description));
1000         mScreenshotPreview.setOnClickListener(null);
1001         mShareChip.setOnClickListener(null);
1002         mScrollingScrim.setVisibility(View.GONE);
1003         mEditChip.setOnClickListener(null);
1004         mShareChip.setIsPending(false);
1005         mEditChip.setIsPending(false);
1006         mPendingInteraction = null;
1007         for (ScreenshotActionChip chip : mSmartChips) {
1008             mActionsView.removeView(chip);
1009         }
1010         mSmartChips.clear();
1011         mQuickShareChip = null;
1012         setAlpha(1);
1013         mScreenshotSelectorView.stop();
1014     }
1015 
startSharedTransition(ActionTransition transition)1016     private void startSharedTransition(ActionTransition transition) {
1017         try {
1018             mPendingSharedTransition = true;
1019             transition.action.actionIntent.send();
1020 
1021             // fade out non-preview UI
1022             createScreenshotFadeDismissAnimation().start();
1023         } catch (PendingIntent.CanceledException e) {
1024             mPendingSharedTransition = false;
1025             if (transition.onCancelRunnable != null) {
1026                 transition.onCancelRunnable.run();
1027             }
1028             Log.e(TAG, "Intent cancelled", e);
1029         }
1030     }
1031 
createScreenshotTranslateDismissAnimation()1032     private AnimatorSet createScreenshotTranslateDismissAnimation() {
1033         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
1034         alphaAnim.setStartDelay(SCREENSHOT_DISMISS_ALPHA_OFFSET_MS);
1035         alphaAnim.setDuration(SCREENSHOT_DISMISS_ALPHA_DURATION_MS);
1036         alphaAnim.addUpdateListener(animation -> {
1037             setAlpha(1 - animation.getAnimatedFraction());
1038         });
1039 
1040         ValueAnimator xAnim = ValueAnimator.ofFloat(0, 1);
1041         xAnim.setInterpolator(mAccelerateInterpolator);
1042         xAnim.setDuration(SCREENSHOT_DISMISS_X_DURATION_MS);
1043         float deltaX = mDirectionLTR
1044                     ? -1 * (mScreenshotPreviewBorder.getX() + mScreenshotPreviewBorder.getWidth())
1045                     : (mDisplayMetrics.widthPixels - mScreenshotPreviewBorder.getX());
1046         xAnim.addUpdateListener(animation -> {
1047             float currXDelta = MathUtils.lerp(0, deltaX, animation.getAnimatedFraction());
1048             mScreenshotStatic.setTranslationX(currXDelta);
1049         });
1050 
1051         AnimatorSet animSet = new AnimatorSet();
1052         animSet.play(xAnim).with(alphaAnim);
1053 
1054         return animSet;
1055     }
1056 
createScreenshotFadeDismissAnimation()1057     ValueAnimator createScreenshotFadeDismissAnimation() {
1058         ValueAnimator alphaAnim = ValueAnimator.ofFloat(0, 1);
1059         alphaAnim.addUpdateListener(animation -> {
1060             float alpha = 1 - animation.getAnimatedFraction();
1061             mDismissButton.setAlpha(alpha);
1062             mActionsContainerBackground.setAlpha(alpha);
1063             mActionsContainer.setAlpha(alpha);
1064             mBackgroundProtection.setAlpha(alpha);
1065             mScreenshotPreviewBorder.setAlpha(alpha);
1066         });
1067         alphaAnim.setDuration(600);
1068         return alphaAnim;
1069     }
1070 
1071     /**
1072      * Create a drawable using the size of the bitmap and insets as the fractional inset parameters.
1073      */
createScreenDrawable(Resources res, Bitmap bitmap, Insets insets)1074     private static Drawable createScreenDrawable(Resources res, Bitmap bitmap, Insets insets) {
1075         int insettedWidth = bitmap.getWidth() - insets.left - insets.right;
1076         int insettedHeight = bitmap.getHeight() - insets.top - insets.bottom;
1077 
1078         BitmapDrawable bitmapDrawable = new BitmapDrawable(res, bitmap);
1079         if (insettedHeight == 0 || insettedWidth == 0 || bitmap.getWidth() == 0
1080                 || bitmap.getHeight() == 0) {
1081             Log.e(TAG, "Can't create inset drawable, using 0 insets bitmap and insets create "
1082                     + "degenerate region: " + bitmap.getWidth() + "x" + bitmap.getHeight() + " "
1083                     + bitmapDrawable);
1084             return bitmapDrawable;
1085         }
1086 
1087         InsetDrawable insetDrawable = new InsetDrawable(bitmapDrawable,
1088                 -1f * insets.left / insettedWidth,
1089                 -1f * insets.top / insettedHeight,
1090                 -1f * insets.right / insettedWidth,
1091                 -1f * insets.bottom / insettedHeight);
1092 
1093         if (insets.left < 0 || insets.top < 0 || insets.right < 0 || insets.bottom < 0) {
1094             // Are any of the insets negative, meaning the bitmap is smaller than the bounds so need
1095             // to fill in the background of the drawable.
1096             return new LayerDrawable(new Drawable[]{
1097                     new ColorDrawable(Color.BLACK), insetDrawable});
1098         } else {
1099             return insetDrawable;
1100         }
1101     }
1102 
dpToPx(float dp)1103     private float dpToPx(float dp) {
1104         return dp * mDisplayMetrics.densityDpi / (float) DisplayMetrics.DENSITY_DEFAULT;
1105     }
1106 
1107     class SwipeDismissHandler implements OnTouchListener {
1108         // distance needed to register a dismissal
1109         private static final float DISMISS_DISTANCE_THRESHOLD_DP = 20;
1110 
1111         private final GestureDetector mGestureDetector;
1112 
1113         private float mStartX;
1114         // Keeps track of the most recent direction (between the last two move events).
1115         // -1 for left; +1 for right.
1116         private int mDirectionX;
1117         private float mPreviousX;
1118 
SwipeDismissHandler()1119         SwipeDismissHandler() {
1120             GestureDetector.OnGestureListener gestureListener = new SwipeDismissGestureListener();
1121             mGestureDetector = new GestureDetector(mContext, gestureListener);
1122         }
1123 
1124         @Override
onTouch(View view, MotionEvent event)1125         public boolean onTouch(View view, MotionEvent event) {
1126             boolean gestureResult = mGestureDetector.onTouchEvent(event);
1127             mCallbacks.onUserInteraction();
1128             if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
1129                 mStartX = event.getRawX();
1130                 mPreviousX = mStartX;
1131                 return true;
1132             } else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
1133                 if (isPastDismissThreshold()
1134                         && (mDismissAnimation == null || !mDismissAnimation.isRunning())) {
1135                     if (DEBUG_INPUT) {
1136                         Log.d(TAG, "dismiss triggered via swipe gesture");
1137                     }
1138                     mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SWIPE_DISMISSED, 0, mPackageName);
1139                     animateDismissal(createSwipeDismissAnimation());
1140                 } else {
1141                     // if we've moved, but not past the threshold, start the return animation
1142                     if (DEBUG_DISMISS) {
1143                         Log.d(TAG, "swipe gesture abandoned");
1144                     }
1145                     if ((mDismissAnimation == null || !mDismissAnimation.isRunning())) {
1146                         createSwipeReturnAnimation().start();
1147                     }
1148                 }
1149                 return true;
1150             }
1151             return gestureResult;
1152         }
1153 
1154         class SwipeDismissGestureListener extends GestureDetector.SimpleOnGestureListener {
1155             @Override
onScroll( MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY)1156             public boolean onScroll(
1157                     MotionEvent ev1, MotionEvent ev2, float distanceX, float distanceY) {
1158                 mScreenshotStatic.setTranslationX(ev2.getRawX() - mStartX);
1159                 mDirectionX = (ev2.getRawX() < mPreviousX) ? -1 : 1;
1160                 mPreviousX = ev2.getRawX();
1161                 return true;
1162             }
1163 
1164             @Override
onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY)1165             public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
1166                     float velocityY) {
1167                 if (mScreenshotStatic.getTranslationX() * velocityX > 0
1168                         && (mDismissAnimation == null || !mDismissAnimation.isRunning())) {
1169                     animateDismissal(createSwipeDismissAnimation(velocityX / (float) 1000));
1170                     return true;
1171                 }
1172                 return false;
1173             }
1174         }
1175 
isPastDismissThreshold()1176         private boolean isPastDismissThreshold() {
1177             float translationX = mScreenshotStatic.getTranslationX();
1178             // Determines whether the absolute translation from the start is in the same direction
1179             // as the current movement. For example, if the user moves most of the way to the right,
1180             // but then starts dragging back left, we do not dismiss even though the absolute
1181             // distance is greater than the threshold.
1182             if (translationX * mDirectionX > 0) {
1183                 return Math.abs(translationX) >= dpToPx(DISMISS_DISTANCE_THRESHOLD_DP);
1184             }
1185             return false;
1186         }
1187 
createSwipeDismissAnimation()1188         private ValueAnimator createSwipeDismissAnimation() {
1189             return createSwipeDismissAnimation(1);
1190         }
1191 
createSwipeDismissAnimation(float velocity)1192         private ValueAnimator createSwipeDismissAnimation(float velocity) {
1193             // velocity is measured in pixels per millisecond
1194             velocity = Math.min(3, Math.max(1, velocity));
1195             ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
1196             float startX = mScreenshotStatic.getTranslationX();
1197             // make sure the UI gets all the way off the screen in the direction of movement
1198             // (the actions container background is guaranteed to be both the leftmost and
1199             // rightmost UI element in LTR and RTL)
1200             float finalX = startX < 0
1201                     ? -1 * mActionsContainerBackground.getRight()
1202                     : mDisplayMetrics.widthPixels;
1203             float distance = Math.abs(finalX - startX);
1204 
1205             anim.addUpdateListener(animation -> {
1206                 float translation = MathUtils.lerp(startX, finalX, animation.getAnimatedFraction());
1207                 mScreenshotStatic.setTranslationX(translation);
1208                 setAlpha(1 - animation.getAnimatedFraction());
1209             });
1210             anim.setDuration((long) (distance / Math.abs(velocity)));
1211             return anim;
1212         }
1213 
createSwipeReturnAnimation()1214         private ValueAnimator createSwipeReturnAnimation() {
1215             ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
1216             float startX = mScreenshotStatic.getTranslationX();
1217             float finalX = 0;
1218 
1219             anim.addUpdateListener(animation -> {
1220                 float translation = MathUtils.lerp(
1221                         startX, finalX, animation.getAnimatedFraction());
1222                 mScreenshotStatic.setTranslationX(translation);
1223             });
1224 
1225             return anim;
1226         }
1227     }
1228 }
1229