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