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