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 package com.android.systemui.pip.phone; 17 18 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_USER_RESIZE; 19 import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_BOTTOM; 20 import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT; 21 import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_NONE; 22 import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT; 23 import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP; 24 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING; 25 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED; 26 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_GLOBAL_ACTIONS_SHOWING; 27 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED; 28 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED; 29 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING; 30 import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; 31 32 import android.content.Context; 33 import android.content.res.Resources; 34 import android.graphics.Point; 35 import android.graphics.PointF; 36 import android.graphics.Rect; 37 import android.graphics.Region; 38 import android.hardware.input.InputManager; 39 import android.os.Handler; 40 import android.os.Looper; 41 import android.provider.DeviceConfig; 42 import android.view.BatchedInputEventReceiver; 43 import android.view.Choreographer; 44 import android.view.InputChannel; 45 import android.view.InputEvent; 46 import android.view.InputEventReceiver; 47 import android.view.InputMonitor; 48 import android.view.MotionEvent; 49 import android.view.ViewConfiguration; 50 51 import com.android.internal.policy.TaskResizingAlgorithm; 52 import com.android.systemui.R; 53 import com.android.systemui.model.SysUiState; 54 import com.android.systemui.pip.PipBoundsHandler; 55 import com.android.systemui.pip.PipTaskOrganizer; 56 import com.android.systemui.pip.PipUiEventLogger; 57 import com.android.systemui.util.DeviceConfigProxy; 58 59 import java.io.PrintWriter; 60 import java.util.concurrent.Executor; 61 import java.util.function.Function; 62 63 /** 64 * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to 65 * trigger dynamic resize. 66 */ 67 public class PipResizeGestureHandler { 68 69 private static final String TAG = "PipResizeGestureHandler"; 70 71 private static final int INVALID_SYSUI_STATE_MASK = 72 SYSUI_STATE_GLOBAL_ACTIONS_SHOWING 73 | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING 74 | SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED 75 | SYSUI_STATE_BOUNCER_SHOWING 76 | SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED 77 | SYSUI_STATE_BUBBLES_EXPANDED 78 | SYSUI_STATE_QUICK_SETTINGS_EXPANDED; 79 80 private final Context mContext; 81 private final PipBoundsHandler mPipBoundsHandler; 82 private final PipMotionHelper mMotionHelper; 83 private final PipMenuActivityController mMenuController; 84 private final int mDisplayId; 85 private final Executor mMainExecutor; 86 private final SysUiState mSysUiState; 87 private final Region mTmpRegion = new Region(); 88 89 private final PointF mDownPoint = new PointF(); 90 private final Point mMaxSize = new Point(); 91 private final Point mMinSize = new Point(); 92 private final Rect mLastResizeBounds = new Rect(); 93 private final Rect mUserResizeBounds = new Rect(); 94 private final Rect mLastDownBounds = new Rect(); 95 private final Rect mDragCornerSize = new Rect(); 96 private final Rect mTmpTopLeftCorner = new Rect(); 97 private final Rect mTmpTopRightCorner = new Rect(); 98 private final Rect mTmpBottomLeftCorner = new Rect(); 99 private final Rect mTmpBottomRightCorner = new Rect(); 100 private final Rect mDisplayBounds = new Rect(); 101 private final Function<Rect, Rect> mMovementBoundsSupplier; 102 private final Runnable mUpdateMovementBoundsRunnable; 103 104 private int mDelta; 105 private float mTouchSlop; 106 private boolean mAllowGesture; 107 private boolean mIsAttached; 108 private boolean mIsEnabled; 109 private boolean mEnableUserResize; 110 private boolean mThresholdCrossed; 111 112 private InputMonitor mInputMonitor; 113 private InputEventReceiver mInputEventReceiver; 114 private PipTaskOrganizer mPipTaskOrganizer; 115 private PipUiEventLogger mPipUiEventLogger; 116 117 private int mCtrlType; 118 PipResizeGestureHandler(Context context, PipBoundsHandler pipBoundsHandler, PipMotionHelper motionHelper, DeviceConfigProxy deviceConfig, PipTaskOrganizer pipTaskOrganizer, PipMenuActivityController pipMenuController, Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, SysUiState sysUiState, PipUiEventLogger pipUiEventLogger)119 public PipResizeGestureHandler(Context context, PipBoundsHandler pipBoundsHandler, 120 PipMotionHelper motionHelper, DeviceConfigProxy deviceConfig, 121 PipTaskOrganizer pipTaskOrganizer, PipMenuActivityController pipMenuController, 122 Function<Rect, Rect> movementBoundsSupplier, Runnable updateMovementBoundsRunnable, 123 SysUiState sysUiState, PipUiEventLogger pipUiEventLogger) { 124 mContext = context; 125 mDisplayId = context.getDisplayId(); 126 mMainExecutor = context.getMainExecutor(); 127 mPipBoundsHandler = pipBoundsHandler; 128 mMenuController = pipMenuController; 129 mMotionHelper = motionHelper; 130 mPipTaskOrganizer = pipTaskOrganizer; 131 mMovementBoundsSupplier = movementBoundsSupplier; 132 mUpdateMovementBoundsRunnable = updateMovementBoundsRunnable; 133 mSysUiState = sysUiState; 134 mPipUiEventLogger = pipUiEventLogger; 135 136 context.getDisplay().getRealSize(mMaxSize); 137 reloadResources(); 138 139 mEnableUserResize = DeviceConfig.getBoolean( 140 DeviceConfig.NAMESPACE_SYSTEMUI, 141 PIP_USER_RESIZE, 142 /* defaultValue = */ true); 143 deviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI, mMainExecutor, 144 new DeviceConfig.OnPropertiesChangedListener() { 145 @Override 146 public void onPropertiesChanged(DeviceConfig.Properties properties) { 147 if (properties.getKeyset().contains(PIP_USER_RESIZE)) { 148 mEnableUserResize = properties.getBoolean( 149 PIP_USER_RESIZE, /* defaultValue = */ true); 150 } 151 } 152 }); 153 } 154 onConfigurationChanged()155 public void onConfigurationChanged() { 156 reloadResources(); 157 } 158 reloadResources()159 private void reloadResources() { 160 final Resources res = mContext.getResources(); 161 mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size); 162 mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); 163 } 164 resetDragCorners()165 private void resetDragCorners() { 166 mDragCornerSize.set(0, 0, mDelta, mDelta); 167 mTmpTopLeftCorner.set(mDragCornerSize); 168 mTmpTopRightCorner.set(mDragCornerSize); 169 mTmpBottomLeftCorner.set(mDragCornerSize); 170 mTmpBottomRightCorner.set(mDragCornerSize); 171 } 172 disposeInputChannel()173 private void disposeInputChannel() { 174 if (mInputEventReceiver != null) { 175 mInputEventReceiver.dispose(); 176 mInputEventReceiver = null; 177 } 178 if (mInputMonitor != null) { 179 mInputMonitor.dispose(); 180 mInputMonitor = null; 181 } 182 } 183 onActivityPinned()184 void onActivityPinned() { 185 mIsAttached = true; 186 updateIsEnabled(); 187 } 188 onActivityUnpinned()189 void onActivityUnpinned() { 190 mIsAttached = false; 191 mUserResizeBounds.setEmpty(); 192 updateIsEnabled(); 193 } 194 updateIsEnabled()195 private void updateIsEnabled() { 196 boolean isEnabled = mIsAttached && mEnableUserResize; 197 if (isEnabled == mIsEnabled) { 198 return; 199 } 200 mIsEnabled = isEnabled; 201 disposeInputChannel(); 202 203 if (mIsEnabled) { 204 // Register input event receiver 205 mInputMonitor = InputManager.getInstance().monitorGestureInput( 206 "pip-resize", mDisplayId); 207 mInputEventReceiver = new SysUiInputEventReceiver( 208 mInputMonitor.getInputChannel(), Looper.getMainLooper()); 209 } 210 } 211 onInputEvent(InputEvent ev)212 private void onInputEvent(InputEvent ev) { 213 if (ev instanceof MotionEvent) { 214 onMotionEvent((MotionEvent) ev); 215 } 216 } 217 218 /** 219 * Check whether the current x,y coordinate is within the region in which drag-resize should 220 * start. 221 * This consists of 4 small squares on the 4 corners of the PIP window, a quarter of which 222 * overlaps with the PIP window while the rest goes outside of the PIP window. 223 * _ _ _ _ 224 * |_|_|_________|_|_| 225 * |_|_| |_|_| 226 * | PIP | 227 * | WINDOW | 228 * _|_ _|_ 229 * |_|_|_________|_|_| 230 * |_|_| |_|_| 231 */ isWithinTouchRegion(int x, int y)232 public boolean isWithinTouchRegion(int x, int y) { 233 final Rect currentPipBounds = mMotionHelper.getBounds(); 234 if (currentPipBounds == null) { 235 return false; 236 } 237 resetDragCorners(); 238 mTmpTopLeftCorner.offset(currentPipBounds.left - mDelta / 2, 239 currentPipBounds.top - mDelta / 2); 240 mTmpTopRightCorner.offset(currentPipBounds.right - mDelta / 2, 241 currentPipBounds.top - mDelta / 2); 242 mTmpBottomLeftCorner.offset(currentPipBounds.left - mDelta / 2, 243 currentPipBounds.bottom - mDelta / 2); 244 mTmpBottomRightCorner.offset(currentPipBounds.right - mDelta / 2, 245 currentPipBounds.bottom - mDelta / 2); 246 247 mTmpRegion.setEmpty(); 248 mTmpRegion.op(mTmpTopLeftCorner, Region.Op.UNION); 249 mTmpRegion.op(mTmpTopRightCorner, Region.Op.UNION); 250 mTmpRegion.op(mTmpBottomLeftCorner, Region.Op.UNION); 251 mTmpRegion.op(mTmpBottomRightCorner, Region.Op.UNION); 252 253 return mTmpRegion.contains(x, y); 254 } 255 willStartResizeGesture(MotionEvent ev)256 public boolean willStartResizeGesture(MotionEvent ev) { 257 return mEnableUserResize && isInValidSysUiState() 258 && isWithinTouchRegion((int) ev.getRawX(), (int) ev.getRawY()); 259 } 260 setCtrlType(int x, int y)261 private void setCtrlType(int x, int y) { 262 final Rect currentPipBounds = mMotionHelper.getBounds(); 263 264 Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); 265 mDisplayBounds.set(movementBounds.left, 266 movementBounds.top, 267 movementBounds.right + currentPipBounds.width(), 268 movementBounds.bottom + currentPipBounds.height()); 269 270 if (mTmpTopLeftCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top 271 && currentPipBounds.left != mDisplayBounds.left) { 272 mCtrlType |= CTRL_LEFT; 273 mCtrlType |= CTRL_TOP; 274 } 275 if (mTmpTopRightCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top 276 && currentPipBounds.right != mDisplayBounds.right) { 277 mCtrlType |= CTRL_RIGHT; 278 mCtrlType |= CTRL_TOP; 279 } 280 if (mTmpBottomRightCorner.contains(x, y) 281 && currentPipBounds.bottom != mDisplayBounds.bottom 282 && currentPipBounds.right != mDisplayBounds.right) { 283 mCtrlType |= CTRL_RIGHT; 284 mCtrlType |= CTRL_BOTTOM; 285 } 286 if (mTmpBottomLeftCorner.contains(x, y) 287 && currentPipBounds.bottom != mDisplayBounds.bottom 288 && currentPipBounds.left != mDisplayBounds.left) { 289 mCtrlType |= CTRL_LEFT; 290 mCtrlType |= CTRL_BOTTOM; 291 } 292 } 293 isInValidSysUiState()294 private boolean isInValidSysUiState() { 295 return (mSysUiState.getFlags() & INVALID_SYSUI_STATE_MASK) == 0; 296 } 297 onMotionEvent(MotionEvent ev)298 private void onMotionEvent(MotionEvent ev) { 299 int action = ev.getActionMasked(); 300 float x = ev.getX(); 301 float y = ev.getY(); 302 if (action == MotionEvent.ACTION_DOWN) { 303 mLastResizeBounds.setEmpty(); 304 mAllowGesture = isInValidSysUiState() && isWithinTouchRegion((int) x, (int) y); 305 if (mAllowGesture) { 306 setCtrlType((int) x, (int) y); 307 mDownPoint.set(x, y); 308 mLastDownBounds.set(mMotionHelper.getBounds()); 309 } 310 311 } else if (mAllowGesture) { 312 switch (action) { 313 case MotionEvent.ACTION_POINTER_DOWN: 314 // We do not support multi touch for resizing via drag 315 mAllowGesture = false; 316 break; 317 case MotionEvent.ACTION_MOVE: 318 // Capture inputs 319 if (!mThresholdCrossed 320 && Math.hypot(x - mDownPoint.x, y - mDownPoint.y) > mTouchSlop) { 321 mThresholdCrossed = true; 322 // Reset the down to begin resizing from this point 323 mDownPoint.set(x, y); 324 mInputMonitor.pilferPointers(); 325 } 326 if (mThresholdCrossed) { 327 if (mMenuController.isMenuActivityVisible()) { 328 mMenuController.hideMenuWithoutResize(); 329 mMenuController.hideMenu(); 330 } 331 final Rect currentPipBounds = mMotionHelper.getBounds(); 332 mLastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y, 333 mDownPoint.x, mDownPoint.y, currentPipBounds, mCtrlType, mMinSize.x, 334 mMinSize.y, mMaxSize, true, 335 mLastDownBounds.width() > mLastDownBounds.height())); 336 mPipBoundsHandler.transformBoundsToAspectRatio(mLastResizeBounds); 337 mPipTaskOrganizer.scheduleUserResizePip(mLastDownBounds, mLastResizeBounds, 338 null); 339 } 340 break; 341 case MotionEvent.ACTION_UP: 342 case MotionEvent.ACTION_CANCEL: 343 if (!mLastResizeBounds.isEmpty()) { 344 mUserResizeBounds.set(mLastResizeBounds); 345 mPipTaskOrganizer.scheduleFinishResizePip(mLastResizeBounds, 346 (Rect bounds) -> { 347 new Handler(Looper.getMainLooper()).post(() -> { 348 mMotionHelper.synchronizePinnedStackBounds(); 349 mUpdateMovementBoundsRunnable.run(); 350 resetState(); 351 }); 352 }); 353 mPipUiEventLogger.log( 354 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_RESIZE); 355 } else { 356 resetState(); 357 } 358 break; 359 } 360 } 361 } 362 resetState()363 private void resetState() { 364 mCtrlType = CTRL_NONE; 365 mAllowGesture = false; 366 mThresholdCrossed = false; 367 } 368 setUserResizeBounds(Rect bounds)369 void setUserResizeBounds(Rect bounds) { 370 mUserResizeBounds.set(bounds); 371 } 372 invalidateUserResizeBounds()373 void invalidateUserResizeBounds() { 374 mUserResizeBounds.setEmpty(); 375 } 376 getUserResizeBounds()377 Rect getUserResizeBounds() { 378 return mUserResizeBounds; 379 } 380 updateMaxSize(int maxX, int maxY)381 void updateMaxSize(int maxX, int maxY) { 382 mMaxSize.set(maxX, maxY); 383 } 384 updateMinSize(int minX, int minY)385 void updateMinSize(int minX, int minY) { 386 mMinSize.set(minX, minY); 387 } 388 dump(PrintWriter pw, String prefix)389 public void dump(PrintWriter pw, String prefix) { 390 final String innerPrefix = prefix + " "; 391 pw.println(prefix + TAG); 392 pw.println(innerPrefix + "mAllowGesture=" + mAllowGesture); 393 pw.println(innerPrefix + "mIsAttached=" + mIsAttached); 394 pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled); 395 pw.println(innerPrefix + "mEnableUserResize=" + mEnableUserResize); 396 pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed); 397 } 398 399 class SysUiInputEventReceiver extends BatchedInputEventReceiver { SysUiInputEventReceiver(InputChannel channel, Looper looper)400 SysUiInputEventReceiver(InputChannel channel, Looper looper) { 401 super(channel, looper, Choreographer.getSfInstance()); 402 } 403 onInputEvent(InputEvent event)404 public void onInputEvent(InputEvent event) { 405 PipResizeGestureHandler.this.onInputEvent(event); 406 finishInputEvent(event, true); 407 } 408 } 409 } 410