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.wm.shell.pip.phone;
18 
19 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
20 
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.graphics.Insets;
24 import android.graphics.PixelFormat;
25 import android.graphics.Point;
26 import android.graphics.Rect;
27 import android.graphics.drawable.TransitionDrawable;
28 import android.view.Gravity;
29 import android.view.MotionEvent;
30 import android.view.SurfaceControl;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.ViewTreeObserver;
34 import android.view.WindowInsets;
35 import android.view.WindowManager;
36 import android.widget.FrameLayout;
37 
38 import androidx.annotation.NonNull;
39 import androidx.dynamicanimation.animation.DynamicAnimation;
40 import androidx.dynamicanimation.animation.SpringForce;
41 
42 import com.android.wm.shell.R;
43 import com.android.wm.shell.animation.PhysicsAnimator;
44 import com.android.wm.shell.common.DismissCircleView;
45 import com.android.wm.shell.common.ShellExecutor;
46 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
47 import com.android.wm.shell.pip.PipUiEventLogger;
48 
49 import kotlin.Unit;
50 
51 /**
52  * Handler of all Magnetized Object related code for PiP.
53  */
54 public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListener {
55 
56     /* The multiplier to apply scale the target size by when applying the magnetic field radius */
57     private static final float MAGNETIC_FIELD_RADIUS_MULTIPLIER = 1.25f;
58 
59     /** Duration of the dismiss scrim fading in/out. */
60     private static final int DISMISS_TRANSITION_DURATION_MS = 200;
61 
62     /**
63      * MagnetizedObject wrapper for PIP. This allows the magnetic target library to locate and move
64      * PIP.
65      */
66     private MagnetizedObject<Rect> mMagnetizedPip;
67 
68     /**
69      * Container for the dismiss circle, so that it can be animated within the container via
70      * translation rather than within the WindowManager via slow layout animations.
71      */
72     private ViewGroup mTargetViewContainer;
73 
74     /** Circle view used to render the dismiss target. */
75     private DismissCircleView mTargetView;
76 
77     /**
78      * MagneticTarget instance wrapping the target view and allowing us to set its magnetic radius.
79      */
80     private MagnetizedObject.MagneticTarget mMagneticTarget;
81 
82     /**
83      * PhysicsAnimator instance for animating the dismiss target in/out.
84      */
85     private PhysicsAnimator<View> mMagneticTargetAnimator;
86 
87     /** Default configuration to use for springing the dismiss target in/out. */
88     private final PhysicsAnimator.SpringConfig mTargetSpringConfig =
89             new PhysicsAnimator.SpringConfig(
90                     SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
91 
92     // Allow dragging the PIP to a location to close it
93     private boolean mEnableDismissDragToEdge;
94 
95     private int mTargetSize;
96     private int mDismissAreaHeight;
97     private float mMagneticFieldRadiusPercent = 1f;
98     private WindowInsets mWindowInsets;
99 
100     private SurfaceControl mTaskLeash;
101     private boolean mHasDismissTargetSurface;
102 
103     private final Context mContext;
104     private final PipMotionHelper mMotionHelper;
105     private final PipUiEventLogger mPipUiEventLogger;
106     private final WindowManager mWindowManager;
107     private final ShellExecutor mMainExecutor;
108 
PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger, PipMotionHelper motionHelper, ShellExecutor mainExecutor)109     public PipDismissTargetHandler(Context context, PipUiEventLogger pipUiEventLogger,
110             PipMotionHelper motionHelper, ShellExecutor mainExecutor) {
111         mContext = context;
112         mPipUiEventLogger = pipUiEventLogger;
113         mMotionHelper = motionHelper;
114         mMainExecutor = mainExecutor;
115         mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
116     }
117 
init()118     public void init() {
119         Resources res = mContext.getResources();
120         mEnableDismissDragToEdge = res.getBoolean(R.bool.config_pipEnableDismissDragToEdge);
121         mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height);
122 
123         if (mTargetViewContainer != null) {
124             // init can be called multiple times, remove the old one from view hierarchy first.
125             cleanUpDismissTarget();
126         }
127 
128         mTargetView = new DismissCircleView(mContext);
129         mTargetViewContainer = new FrameLayout(mContext);
130         mTargetViewContainer.setBackgroundDrawable(
131                 mContext.getDrawable(R.drawable.floating_dismiss_gradient_transition));
132         mTargetViewContainer.setClipChildren(false);
133         mTargetViewContainer.addView(mTargetView);
134         mTargetViewContainer.setOnApplyWindowInsetsListener((view, windowInsets) -> {
135             if (!windowInsets.equals(mWindowInsets)) {
136                 mWindowInsets = windowInsets;
137                 updateMagneticTargetSize();
138             }
139             return windowInsets;
140         });
141 
142         mMagnetizedPip = mMotionHelper.getMagnetizedPip();
143         mMagnetizedPip.clearAllTargets();
144         mMagneticTarget = mMagnetizedPip.addTarget(mTargetView, 0);
145         updateMagneticTargetSize();
146 
147         mMagnetizedPip.setAnimateStuckToTarget(
148                 (target, velX, velY, flung, after) -> {
149                     if (mEnableDismissDragToEdge) {
150                         mMotionHelper.animateIntoDismissTarget(target, velX, velY, flung, after);
151                     }
152                     return Unit.INSTANCE;
153                 });
154         mMagnetizedPip.setMagnetListener(new MagnetizedObject.MagnetListener() {
155             @Override
156             public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
157                 // Show the dismiss target, in case the initial touch event occurred within
158                 // the magnetic field radius.
159                 if (mEnableDismissDragToEdge) {
160                     showDismissTargetMaybe();
161                 }
162             }
163 
164             @Override
165             public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
166                     float velX, float velY, boolean wasFlungOut) {
167                 if (wasFlungOut) {
168                     mMotionHelper.flingToSnapTarget(velX, velY, null /* endAction */);
169                     hideDismissTargetMaybe();
170                 } else {
171                     mMotionHelper.setSpringingToTouch(true);
172                 }
173             }
174 
175             @Override
176             public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
177                 if (mEnableDismissDragToEdge) {
178                     mMainExecutor.executeDelayed(() -> {
179                         mMotionHelper.notifyDismissalPending();
180                         mMotionHelper.animateDismiss();
181                         hideDismissTargetMaybe();
182 
183                         mPipUiEventLogger.log(
184                                 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_DRAG_TO_REMOVE);
185                     }, 0);
186                 }
187             }
188         });
189 
190         mMagneticTargetAnimator = PhysicsAnimator.getInstance(mTargetView);
191     }
192 
193     @Override
onPreDraw()194     public boolean onPreDraw() {
195         mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this);
196         mHasDismissTargetSurface = true;
197         updateDismissTargetLayer();
198         return true;
199     }
200 
201     /**
202      * Potentially start consuming future motion events if PiP is currently near the magnetized
203      * object.
204      */
maybeConsumeMotionEvent(MotionEvent ev)205     public boolean maybeConsumeMotionEvent(MotionEvent ev) {
206         return mMagnetizedPip.maybeConsumeMotionEvent(ev);
207     }
208 
209     /**
210      * Update the magnet size.
211      */
updateMagneticTargetSize()212     public void updateMagneticTargetSize() {
213         if (mTargetView == null) {
214             return;
215         }
216 
217         final Resources res = mContext.getResources();
218         mTargetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
219         mDismissAreaHeight = res.getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height);
220         final WindowInsets insets = mWindowManager.getCurrentWindowMetrics().getWindowInsets();
221         final Insets navInset = insets.getInsetsIgnoringVisibility(
222                 WindowInsets.Type.navigationBars());
223         final FrameLayout.LayoutParams newParams =
224                 new FrameLayout.LayoutParams(mTargetSize, mTargetSize);
225         newParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
226         newParams.bottomMargin = navInset.bottom + mContext.getResources().getDimensionPixelSize(
227                 R.dimen.floating_dismiss_bottom_margin);
228         mTargetView.setLayoutParams(newParams);
229 
230         // Set the magnetic field radius equal to the target size from the center of the target
231         setMagneticFieldRadiusPercent(mMagneticFieldRadiusPercent);
232     }
233 
234     /**
235      * Increase or decrease the field radius of the magnet object, e.g. with larger percent,
236      * PiP will magnetize to the field sooner.
237      */
setMagneticFieldRadiusPercent(float percent)238     public void setMagneticFieldRadiusPercent(float percent) {
239         mMagneticFieldRadiusPercent = percent;
240         mMagneticTarget.setMagneticFieldRadiusPx((int) (mMagneticFieldRadiusPercent * mTargetSize
241                         * MAGNETIC_FIELD_RADIUS_MULTIPLIER));
242     }
243 
setTaskLeash(SurfaceControl taskLeash)244     public void setTaskLeash(SurfaceControl taskLeash) {
245         mTaskLeash = taskLeash;
246     }
247 
updateDismissTargetLayer()248     private void updateDismissTargetLayer() {
249         if (!mHasDismissTargetSurface || mTaskLeash == null) {
250             // No dismiss target surface, can just return
251             return;
252         }
253 
254         // Put the dismiss target behind the task
255         SurfaceControl.Transaction t = new SurfaceControl.Transaction();
256         t.setRelativeLayer(mTargetViewContainer.getViewRootImpl().getSurfaceControl(),
257                 mTaskLeash, -1);
258         t.apply();
259     }
260 
261     /** Adds the magnetic target view to the WindowManager so it's ready to be animated in. */
createOrUpdateDismissTarget()262     public void createOrUpdateDismissTarget() {
263         if (!mTargetViewContainer.isAttachedToWindow()) {
264             mMagneticTargetAnimator.cancel();
265 
266             mTargetViewContainer.setVisibility(View.INVISIBLE);
267             mTargetViewContainer.getViewTreeObserver().removeOnPreDrawListener(this);
268             mHasDismissTargetSurface = false;
269 
270             try {
271                 mWindowManager.addView(mTargetViewContainer, getDismissTargetLayoutParams());
272             } catch (IllegalStateException e) {
273                 // This shouldn't happen, but if the target is already added, just update its layout
274                 // params.
275                 mWindowManager.updateViewLayout(
276                         mTargetViewContainer, getDismissTargetLayoutParams());
277             }
278         } else {
279             mWindowManager.updateViewLayout(mTargetViewContainer, getDismissTargetLayoutParams());
280         }
281     }
282 
283     /** Returns layout params for the dismiss target, using the latest display metrics. */
getDismissTargetLayoutParams()284     private WindowManager.LayoutParams getDismissTargetLayoutParams() {
285         final Point windowSize = new Point();
286         mWindowManager.getDefaultDisplay().getRealSize(windowSize);
287 
288         final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
289                 WindowManager.LayoutParams.MATCH_PARENT,
290                 mDismissAreaHeight,
291                 0, windowSize.y - mDismissAreaHeight,
292                 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
293                 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
294                         | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
295                         | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
296                 PixelFormat.TRANSLUCENT);
297 
298         lp.setTitle("pip-dismiss-overlay");
299         lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
300         lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
301         lp.setFitInsetsTypes(0 /* types */);
302 
303         return lp;
304     }
305 
306     /** Makes the dismiss target visible and animates it in, if it isn't already visible. */
showDismissTargetMaybe()307     public void showDismissTargetMaybe() {
308         if (!mEnableDismissDragToEdge) {
309             return;
310         }
311 
312         createOrUpdateDismissTarget();
313 
314         if (mTargetViewContainer.getVisibility() != View.VISIBLE) {
315             mTargetView.setTranslationY(mTargetViewContainer.getHeight());
316             mTargetViewContainer.setVisibility(View.VISIBLE);
317             mTargetViewContainer.getViewTreeObserver().addOnPreDrawListener(this);
318 
319             // Cancel in case we were in the middle of animating it out.
320             mMagneticTargetAnimator.cancel();
321             mMagneticTargetAnimator
322                     .spring(DynamicAnimation.TRANSLATION_Y, 0f, mTargetSpringConfig)
323                     .start();
324 
325             ((TransitionDrawable) mTargetViewContainer.getBackground()).startTransition(
326                     DISMISS_TRANSITION_DURATION_MS);
327         }
328     }
329 
330     /** Animates the magnetic dismiss target out and then sets it to GONE. */
hideDismissTargetMaybe()331     public void hideDismissTargetMaybe() {
332         if (!mEnableDismissDragToEdge) {
333             return;
334         }
335 
336         mMagneticTargetAnimator
337                 .spring(DynamicAnimation.TRANSLATION_Y,
338                         mTargetViewContainer.getHeight(),
339                         mTargetSpringConfig)
340                 .withEndActions(() -> mTargetViewContainer.setVisibility(View.GONE))
341                 .start();
342 
343         ((TransitionDrawable) mTargetViewContainer.getBackground()).reverseTransition(
344                 DISMISS_TRANSITION_DURATION_MS);
345     }
346 
347     /**
348      * Removes the dismiss target and cancels any pending callbacks to show it.
349      */
cleanUpDismissTarget()350     public void cleanUpDismissTarget() {
351         if (mTargetViewContainer.isAttachedToWindow()) {
352             mWindowManager.removeViewImmediate(mTargetViewContainer);
353         }
354     }
355 }
356