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 com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASHING;
20 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.PIP_STASH_MINIMUM_VELOCITY_THRESHOLD;
21 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_LEFT;
22 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE;
23 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_RIGHT;
24 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
25 import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL;
26 import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE;
27 import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE;
28 
29 import android.annotation.NonNull;
30 import android.annotation.SuppressLint;
31 import android.content.ComponentName;
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.provider.DeviceConfig;
38 import android.util.Size;
39 import android.view.DisplayCutout;
40 import android.view.InputEvent;
41 import android.view.MotionEvent;
42 import android.view.ViewConfiguration;
43 import android.view.accessibility.AccessibilityEvent;
44 import android.view.accessibility.AccessibilityManager;
45 import android.view.accessibility.AccessibilityNodeInfo;
46 import android.view.accessibility.AccessibilityWindowInfo;
47 
48 import com.android.internal.annotations.VisibleForTesting;
49 import com.android.internal.protolog.common.ProtoLog;
50 import com.android.wm.shell.R;
51 import com.android.wm.shell.common.FloatingContentCoordinator;
52 import com.android.wm.shell.common.ShellExecutor;
53 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
54 import com.android.wm.shell.common.pip.PipBoundsState;
55 import com.android.wm.shell.common.pip.PipUiEventLogger;
56 import com.android.wm.shell.common.pip.PipUtils;
57 import com.android.wm.shell.common.pip.SizeSpecSource;
58 import com.android.wm.shell.pip.PipAnimationController;
59 import com.android.wm.shell.pip.PipTaskOrganizer;
60 import com.android.wm.shell.pip.PipTransitionController;
61 import com.android.wm.shell.protolog.ShellProtoLogGroup;
62 import com.android.wm.shell.sysui.ShellInit;
63 
64 import java.io.PrintWriter;
65 
66 /**
67  * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding
68  * the PIP.
69  */
70 public class PipTouchHandler {
71 
72     private static final String TAG = "PipTouchHandler";
73     private static final float DEFAULT_STASH_VELOCITY_THRESHOLD = 18000.f;
74 
75     // Allow PIP to resize to a slightly bigger state upon touch
76     private boolean mEnableResize;
77     private final Context mContext;
78     private final PipBoundsAlgorithm mPipBoundsAlgorithm;
79     @NonNull private final PipBoundsState mPipBoundsState;
80     @NonNull private final SizeSpecSource mSizeSpecSource;
81     private final PipUiEventLogger mPipUiEventLogger;
82     private final PipDismissTargetHandler mPipDismissTargetHandler;
83     private final PipTaskOrganizer mPipTaskOrganizer;
84     private final ShellExecutor mMainExecutor;
85 
86     private PipResizeGestureHandler mPipResizeGestureHandler;
87 
88     private final PhonePipMenuController mMenuController;
89     private final AccessibilityManager mAccessibilityManager;
90 
91     /**
92      * Whether PIP stash is enabled or not. When enabled, if the user flings toward the edge of the
93      * screen, it will be shown in "stashed" mode, where PIP will only show partially.
94      */
95     private boolean mEnableStash = true;
96 
97     private float mStashVelocityThreshold;
98 
99     // The reference inset bounds, used to determine the dismiss fraction
100     private final Rect mInsetBounds = new Rect();
101 
102     // Used to workaround an issue where the WM rotation happens before we are notified, allowing
103     // us to send stale bounds
104     private int mDeferResizeToNormalBoundsUntilRotation = -1;
105     private int mDisplayRotation;
106 
107     private final PipAccessibilityInteractionConnection mConnection;
108 
109     // Behaviour states
110     private int mMenuState = MENU_STATE_NONE;
111     private boolean mIsImeShowing;
112     private int mImeHeight;
113     private int mImeOffset;
114     private boolean mIsShelfShowing;
115     private int mShelfHeight;
116     private int mMovementBoundsExtraOffsets;
117     private int mBottomOffsetBufferPx;
118     private float mSavedSnapFraction = -1f;
119     private boolean mSendingHoverAccessibilityEvents;
120     private boolean mMovementWithinDismiss;
121 
122     // Touch state
123     private final PipTouchState mTouchState;
124     private final FloatingContentCoordinator mFloatingContentCoordinator;
125     private PipMotionHelper mMotionHelper;
126     private PipTouchGesture mGesture;
127 
128     // Temp vars
129     private final Rect mTmpBounds = new Rect();
130 
131     /**
132      * A listener for the PIP menu activity.
133      */
134     private class PipMenuListener implements PhonePipMenuController.Listener {
135         @Override
onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback)136         public void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
137             PipTouchHandler.this.onPipMenuStateChangeStart(menuState, resize, callback);
138         }
139 
140         @Override
onPipMenuStateChangeFinish(int menuState)141         public void onPipMenuStateChangeFinish(int menuState) {
142             setMenuState(menuState);
143         }
144 
145         @Override
onPipExpand()146         public void onPipExpand() {
147             mMotionHelper.expandLeavePip(false /* skipAnimation */);
148         }
149 
150         @Override
onEnterSplit()151         public void onEnterSplit() {
152             mMotionHelper.expandIntoSplit();
153         }
154 
155         @Override
onPipDismiss()156         public void onPipDismiss() {
157             mTouchState.removeDoubleTapTimeoutCallback();
158             mMotionHelper.dismissPip();
159         }
160 
161         @Override
onPipShowMenu()162         public void onPipShowMenu() {
163             mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(),
164                     true /* allowMenuTimeout */, willResizeMenu(), shouldShowResizeHandle());
165         }
166     }
167 
168     @SuppressLint("InflateParams")
PipTouchHandler(Context context, ShellInit shellInit, PhonePipMenuController menuController, PipBoundsAlgorithm pipBoundsAlgorithm, @NonNull PipBoundsState pipBoundsState, @NonNull SizeSpecSource sizeSpecSource, PipTaskOrganizer pipTaskOrganizer, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, PipUiEventLogger pipUiEventLogger, ShellExecutor mainExecutor)169     public PipTouchHandler(Context context,
170             ShellInit shellInit,
171             PhonePipMenuController menuController,
172             PipBoundsAlgorithm pipBoundsAlgorithm,
173             @NonNull PipBoundsState pipBoundsState,
174             @NonNull SizeSpecSource sizeSpecSource,
175             PipTaskOrganizer pipTaskOrganizer,
176             PipMotionHelper pipMotionHelper,
177             FloatingContentCoordinator floatingContentCoordinator,
178             PipUiEventLogger pipUiEventLogger,
179             ShellExecutor mainExecutor) {
180         mContext = context;
181         mMainExecutor = mainExecutor;
182         mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
183         mPipBoundsAlgorithm = pipBoundsAlgorithm;
184         mPipBoundsState = pipBoundsState;
185         mSizeSpecSource = sizeSpecSource;
186         mPipTaskOrganizer = pipTaskOrganizer;
187         mMenuController = menuController;
188         mPipUiEventLogger = pipUiEventLogger;
189         mFloatingContentCoordinator = floatingContentCoordinator;
190         mMenuController.addListener(new PipMenuListener());
191         mGesture = new DefaultPipTouchGesture();
192         mMotionHelper = pipMotionHelper;
193         mPipDismissTargetHandler = new PipDismissTargetHandler(context, pipUiEventLogger,
194                 mMotionHelper, mainExecutor);
195         mTouchState = new PipTouchState(ViewConfiguration.get(context),
196                 () -> {
197                     if (mPipBoundsState.isStashed()) {
198                         animateToUnStashedState();
199                         mPipUiEventLogger.log(
200                                 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED);
201                         mPipBoundsState.setStashed(STASH_TYPE_NONE);
202                     } else {
203                         mMenuController.showMenuWithPossibleDelay(MENU_STATE_FULL,
204                                 mPipBoundsState.getBounds(), true /* allowMenuTimeout */,
205                                 willResizeMenu(),
206                                 shouldShowResizeHandle());
207                     }
208                 },
209                 menuController::hideMenu,
210                 mainExecutor);
211         mPipResizeGestureHandler =
212                 new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState,
213                         mMotionHelper, mTouchState, pipTaskOrganizer, mPipDismissTargetHandler,
214                         this::getMovementBounds, this::updateMovementBounds, pipUiEventLogger,
215                         menuController, mainExecutor);
216         mConnection = new PipAccessibilityInteractionConnection(mContext, pipBoundsState,
217                 mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(),
218                 this::onAccessibilityShowMenu, this::updateMovementBounds,
219                 this::animateToUnStashedState, mainExecutor);
220 
221         // TODO(b/181599115): This should really be initializes as part of the pip controller, but
222         // until all PIP implementations derive from the controller, just initialize the touch handler
223         // if it is needed
224         if (!PipUtils.isPip2ExperimentEnabled()) {
225             shellInit.addInitCallback(this::onInit, this);
226         }
227     }
228 
229     /**
230      * Called when the touch handler is initialized.
231      */
onInit()232     public void onInit() {
233         Resources res = mContext.getResources();
234         mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu);
235         reloadResources();
236 
237         mMotionHelper.init();
238         mPipResizeGestureHandler.init();
239         mPipDismissTargetHandler.init();
240 
241         mEnableStash = DeviceConfig.getBoolean(
242                 DeviceConfig.NAMESPACE_SYSTEMUI,
243                 PIP_STASHING,
244                 /* defaultValue = */ true);
245         DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
246                 mMainExecutor,
247                 properties -> {
248                     if (properties.getKeyset().contains(PIP_STASHING)) {
249                         mEnableStash = properties.getBoolean(
250                                 PIP_STASHING, /* defaultValue = */ true);
251                     }
252                 });
253         mStashVelocityThreshold = DeviceConfig.getFloat(
254                 DeviceConfig.NAMESPACE_SYSTEMUI,
255                 PIP_STASH_MINIMUM_VELOCITY_THRESHOLD,
256                 DEFAULT_STASH_VELOCITY_THRESHOLD);
257         DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_SYSTEMUI,
258                 mMainExecutor,
259                 properties -> {
260                     if (properties.getKeyset().contains(PIP_STASH_MINIMUM_VELOCITY_THRESHOLD)) {
261                         mStashVelocityThreshold = properties.getFloat(
262                                 PIP_STASH_MINIMUM_VELOCITY_THRESHOLD,
263                                 DEFAULT_STASH_VELOCITY_THRESHOLD);
264                     }
265                 });
266     }
267 
getTransitionHandler()268     public PipTransitionController getTransitionHandler() {
269         return mPipTaskOrganizer.getTransitionController();
270     }
271 
reloadResources()272     private void reloadResources() {
273         final Resources res = mContext.getResources();
274         mBottomOffsetBufferPx = res.getDimensionPixelSize(R.dimen.pip_bottom_offset_buffer);
275         mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
276         mPipDismissTargetHandler.updateMagneticTargetSize();
277     }
278 
onOverlayChanged()279     public void onOverlayChanged() {
280         // onOverlayChanged is triggered upon theme change, update the dismiss target accordingly.
281         mPipDismissTargetHandler.init();
282     }
283 
shouldShowResizeHandle()284     private boolean shouldShowResizeHandle() {
285         return false;
286     }
287 
setTouchGesture(PipTouchGesture gesture)288     public void setTouchGesture(PipTouchGesture gesture) {
289         mGesture = gesture;
290     }
291 
setTouchEnabled(boolean enabled)292     public void setTouchEnabled(boolean enabled) {
293         mTouchState.setAllowTouches(enabled);
294     }
295 
showPictureInPictureMenu()296     public void showPictureInPictureMenu() {
297         // Only show the menu if the user isn't currently interacting with the PiP
298         if (!mTouchState.isUserInteracting()) {
299             mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(),
300                     false /* allowMenuTimeout */, willResizeMenu(),
301                     shouldShowResizeHandle());
302         }
303     }
304 
onActivityPinned()305     public void onActivityPinned() {
306         mPipDismissTargetHandler.createOrUpdateDismissTarget();
307 
308         mPipResizeGestureHandler.onActivityPinned();
309         mFloatingContentCoordinator.onContentAdded(mMotionHelper);
310     }
311 
onActivityUnpinned(ComponentName topPipActivity)312     public void onActivityUnpinned(ComponentName topPipActivity) {
313         if (topPipActivity == null) {
314             // Clean up state after the last PiP activity is removed
315             mPipDismissTargetHandler.cleanUpDismissTarget();
316 
317             mFloatingContentCoordinator.onContentRemoved(mMotionHelper);
318         }
319         mPipResizeGestureHandler.onActivityUnpinned();
320     }
321 
onPinnedStackAnimationEnded( @ipAnimationController.TransitionDirection int direction)322     public void onPinnedStackAnimationEnded(
323             @PipAnimationController.TransitionDirection int direction) {
324         // Always synchronize the motion helper bounds once PiP animations finish
325         mMotionHelper.synchronizePinnedStackBounds();
326         updateMovementBounds();
327         if (direction == TRANSITION_DIRECTION_TO_PIP) {
328             // Set the initial bounds as the user resize bounds.
329             mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds());
330         }
331     }
332 
onConfigurationChanged()333     public void onConfigurationChanged() {
334         mPipResizeGestureHandler.onConfigurationChanged();
335         mMotionHelper.synchronizePinnedStackBounds();
336         reloadResources();
337 
338         if (mPipTaskOrganizer.isInPip()) {
339             // Recreate the dismiss target for the new orientation.
340             mPipDismissTargetHandler.createOrUpdateDismissTarget();
341         }
342     }
343 
onImeVisibilityChanged(boolean imeVisible, int imeHeight)344     public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
345         mIsImeShowing = imeVisible;
346         mImeHeight = imeHeight;
347     }
348 
onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight)349     public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) {
350         mIsShelfShowing = shelfVisible;
351         mShelfHeight = shelfHeight;
352     }
353 
354     /**
355      * Called when SysUI state changed.
356      *
357      * @param isSysUiStateValid Is SysUI valid or not.
358      */
onSystemUiStateChanged(boolean isSysUiStateValid)359     public void onSystemUiStateChanged(boolean isSysUiStateValid) {
360         mPipResizeGestureHandler.onSystemUiStateChanged(isSysUiStateValid);
361     }
362 
adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds)363     public void adjustBoundsForRotation(Rect outBounds, Rect curBounds, Rect insetBounds) {
364         final Rect toMovementBounds = new Rect();
365         mPipBoundsAlgorithm.getMovementBounds(outBounds, insetBounds, toMovementBounds, 0);
366         final int prevBottom = mPipBoundsState.getMovementBounds().bottom
367                 - mMovementBoundsExtraOffsets;
368         if ((prevBottom - mBottomOffsetBufferPx) <= curBounds.top) {
369             outBounds.offsetTo(outBounds.left, toMovementBounds.bottom);
370         }
371     }
372 
373     /**
374      * Responds to IPinnedStackListener on resetting aspect ratio for the pinned window.
375      */
onAspectRatioChanged()376     public void onAspectRatioChanged() {
377         mPipResizeGestureHandler.invalidateUserResizeBounds();
378     }
379 
onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation)380     public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect curBounds,
381             boolean fromImeAdjustment, boolean fromShelfAdjustment, int displayRotation) {
382         // Set the user resized bounds equal to the new normal bounds in case they were
383         // invalidated (e.g. by an aspect ratio change).
384         if (mPipResizeGestureHandler.getUserResizeBounds().isEmpty()) {
385             mPipResizeGestureHandler.setUserResizeBounds(normalBounds);
386         }
387 
388         final int bottomOffset = mIsImeShowing ? mImeHeight : 0;
389         final boolean fromDisplayRotationChanged = (mDisplayRotation != displayRotation);
390         if (fromDisplayRotationChanged) {
391             mTouchState.reset();
392         }
393 
394         // Re-calculate the expanded bounds
395         Rect normalMovementBounds = new Rect();
396         mPipBoundsAlgorithm.getMovementBounds(normalBounds, insetBounds,
397                 normalMovementBounds, bottomOffset);
398 
399         if (mPipBoundsState.getMovementBounds().isEmpty()) {
400             // mMovementBounds is not initialized yet and a clean movement bounds without
401             // bottom offset shall be used later in this function.
402             mPipBoundsAlgorithm.getMovementBounds(curBounds, insetBounds,
403                     mPipBoundsState.getMovementBounds(), 0 /* bottomOffset */);
404         }
405 
406         // Calculate the expanded size
407         float aspectRatio = (float) normalBounds.width() / normalBounds.height();
408         Size expandedSize = mSizeSpecSource.getDefaultSize(aspectRatio);
409         mPipBoundsState.setExpandedBounds(
410                 new Rect(0, 0, expandedSize.getWidth(), expandedSize.getHeight()));
411         Rect expandedMovementBounds = new Rect();
412         mPipBoundsAlgorithm.getMovementBounds(
413                 mPipBoundsState.getExpandedBounds(), insetBounds, expandedMovementBounds,
414                 bottomOffset);
415 
416         updatePipSizeConstraints(normalBounds, aspectRatio);
417 
418         // The extra offset does not really affect the movement bounds, but are applied based on the
419         // current state (ime showing, or shelf offset) when we need to actually shift
420         int extraOffset = Math.max(
421                 mIsImeShowing ? mImeOffset : 0,
422                 !mIsImeShowing && mIsShelfShowing ? mShelfHeight : 0);
423 
424         // Update the movement bounds after doing the calculations based on the old movement bounds
425         // above
426         mPipBoundsState.setNormalMovementBounds(normalMovementBounds);
427         mPipBoundsState.setExpandedMovementBounds(expandedMovementBounds);
428         mDisplayRotation = displayRotation;
429         mInsetBounds.set(insetBounds);
430         updateMovementBounds();
431         mMovementBoundsExtraOffsets = extraOffset;
432         mConnection.onMovementBoundsChanged(normalBounds, mPipBoundsState.getExpandedBounds(),
433                 mPipBoundsState.getNormalMovementBounds(),
434                 mPipBoundsState.getExpandedMovementBounds());
435 
436         // If we have a deferred resize, apply it now
437         if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) {
438             mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction,
439                     mPipBoundsState.getNormalMovementBounds(), mPipBoundsState.getMovementBounds(),
440                     true /* immediate */);
441             mSavedSnapFraction = -1f;
442             mDeferResizeToNormalBoundsUntilRotation = -1;
443         }
444     }
445 
446     /**
447      * Update the values for min/max allowed size of picture in picture window based on the aspect
448      * ratio.
449      * @param aspectRatio aspect ratio to use for the calculation of min/max size
450      */
updateMinMaxSize(float aspectRatio)451     public void updateMinMaxSize(float aspectRatio) {
452         updatePipSizeConstraints(mPipBoundsState.getNormalBounds(),
453                 aspectRatio);
454     }
455 
updatePipSizeConstraints(Rect normalBounds, float aspectRatio)456     private void updatePipSizeConstraints(Rect normalBounds,
457             float aspectRatio) {
458         if (mPipResizeGestureHandler.isUsingPinchToZoom()) {
459             updatePinchResizeSizeConstraints(aspectRatio);
460         } else {
461             mPipResizeGestureHandler.updateMinSize(normalBounds.width(), normalBounds.height());
462             mPipResizeGestureHandler.updateMaxSize(mPipBoundsState.getExpandedBounds().width(),
463                     mPipBoundsState.getExpandedBounds().height());
464         }
465     }
466 
updatePinchResizeSizeConstraints(float aspectRatio)467     private void updatePinchResizeSizeConstraints(float aspectRatio) {
468         final int minWidth, minHeight, maxWidth, maxHeight;
469 
470         minWidth = mSizeSpecSource.getMinSize(aspectRatio).getWidth();
471         minHeight = mSizeSpecSource.getMinSize(aspectRatio).getHeight();
472         maxWidth = mSizeSpecSource.getMaxSize(aspectRatio).getWidth();
473         maxHeight = mSizeSpecSource.getMaxSize(aspectRatio).getHeight();
474 
475         mPipResizeGestureHandler.updateMinSize(minWidth, minHeight);
476         mPipResizeGestureHandler.updateMaxSize(maxWidth, maxHeight);
477         mPipBoundsState.setMaxSize(maxWidth, maxHeight);
478         mPipBoundsState.setMinSize(minWidth, minHeight);
479     }
480 
481     /**
482      * TODO Add appropriate description
483      */
onRegistrationChanged(boolean isRegistered)484     public void onRegistrationChanged(boolean isRegistered) {
485         if (isRegistered) {
486             mConnection.register(mAccessibilityManager);
487         } else {
488             mAccessibilityManager.setPictureInPictureActionReplacingConnection(null);
489         }
490         if (!isRegistered && mTouchState.isUserInteracting()) {
491             // If the input consumer is unregistered while the user is interacting, then we may not
492             // get the final TOUCH_UP event, so clean up the dismiss target as well
493             mPipDismissTargetHandler.cleanUpDismissTarget();
494         }
495     }
496 
onAccessibilityShowMenu()497     private void onAccessibilityShowMenu() {
498         mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(),
499                 true /* allowMenuTimeout */, willResizeMenu(),
500                 shouldShowResizeHandle());
501     }
502 
503     /**
504      * TODO Add appropriate description
505      */
handleTouchEvent(InputEvent inputEvent)506     public boolean handleTouchEvent(InputEvent inputEvent) {
507         // Skip any non motion events
508         if (!(inputEvent instanceof MotionEvent)) {
509             return true;
510         }
511 
512         // do not process input event if not allowed
513         if (!mTouchState.getAllowInputEvents()) {
514             return true;
515         }
516 
517         MotionEvent ev = (MotionEvent) inputEvent;
518         if (!mPipBoundsState.isStashed() && mPipResizeGestureHandler.willStartResizeGesture(ev)) {
519             // Initialize the touch state for the gesture, but immediately reset to invalidate the
520             // gesture
521             mTouchState.onTouchEvent(ev);
522             mTouchState.reset();
523             return true;
524         }
525 
526         if (mPipResizeGestureHandler.hasOngoingGesture()) {
527             mPipDismissTargetHandler.hideDismissTargetMaybe();
528             return true;
529         }
530 
531         if ((ev.getAction() == MotionEvent.ACTION_DOWN || mTouchState.isUserInteracting())
532                 && mPipDismissTargetHandler.maybeConsumeMotionEvent(ev)) {
533             // If the first touch event occurs within the magnetic field, pass the ACTION_DOWN event
534             // to the touch state. Touch state needs a DOWN event in order to later process MOVE
535             // events it'll receive if the object is dragged out of the magnetic field.
536             if (ev.getAction() == MotionEvent.ACTION_DOWN) {
537                 mTouchState.onTouchEvent(ev);
538             }
539 
540             // Continue tracking velocity when the object is in the magnetic field, since we want to
541             // respect touch input velocity if the object is dragged out and then flung.
542             mTouchState.addMovementToVelocityTracker(ev);
543 
544             return true;
545         }
546 
547         // Update the touch state
548         mTouchState.onTouchEvent(ev);
549 
550         boolean shouldDeliverToMenu = mMenuState != MENU_STATE_NONE;
551 
552         switch (ev.getAction()) {
553             case MotionEvent.ACTION_DOWN: {
554                 mGesture.onDown(mTouchState);
555                 break;
556             }
557             case MotionEvent.ACTION_MOVE: {
558                 if (mGesture.onMove(mTouchState)) {
559                     break;
560                 }
561 
562                 shouldDeliverToMenu = !mTouchState.isDragging();
563                 break;
564             }
565             case MotionEvent.ACTION_UP: {
566                 // Update the movement bounds again if the state has changed since the user started
567                 // dragging (ie. when the IME shows)
568                 updateMovementBounds();
569 
570                 if (mGesture.onUp(mTouchState)) {
571                     break;
572                 }
573 
574                 // Fall through to clean up
575             }
576             case MotionEvent.ACTION_CANCEL: {
577                 shouldDeliverToMenu = !mTouchState.startedDragging() && !mTouchState.isDragging();
578                 mTouchState.reset();
579                 break;
580             }
581             case MotionEvent.ACTION_HOVER_ENTER:
582                 // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably
583                 // on and changing MotionEvents into HoverEvents.
584                 // Let's not enable menu show/hide for a11y services.
585                 if (!mAccessibilityManager.isTouchExplorationEnabled()) {
586                     mTouchState.removeHoverExitTimeoutCallback();
587                     mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(),
588                             false /* allowMenuTimeout */, false /* willResizeMenu */,
589                             shouldShowResizeHandle());
590                 }
591             case MotionEvent.ACTION_HOVER_MOVE: {
592                 if (!shouldDeliverToMenu && !mSendingHoverAccessibilityEvents) {
593                     sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
594                     mSendingHoverAccessibilityEvents = true;
595                 }
596                 break;
597             }
598             case MotionEvent.ACTION_HOVER_EXIT: {
599                 // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably
600                 // on and changing MotionEvents into HoverEvents.
601                 // Let's not enable menu show/hide for a11y services.
602                 if (!mAccessibilityManager.isTouchExplorationEnabled()) {
603                     mTouchState.scheduleHoverExitTimeoutCallback();
604                 }
605                 if (!shouldDeliverToMenu && mSendingHoverAccessibilityEvents) {
606                     sendAccessibilityHoverEvent(AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
607                     mSendingHoverAccessibilityEvents = false;
608                 }
609                 break;
610             }
611         }
612 
613         shouldDeliverToMenu &= !mPipBoundsState.isStashed();
614 
615         // Deliver the event to PipMenuActivity to handle button click if the menu has shown.
616         if (shouldDeliverToMenu) {
617             final MotionEvent cloneEvent = MotionEvent.obtain(ev);
618             // Send the cancel event and cancel menu timeout if it starts to drag.
619             if (mTouchState.startedDragging()) {
620                 cloneEvent.setAction(MotionEvent.ACTION_CANCEL);
621                 mMenuController.pokeMenu();
622             }
623 
624             mMenuController.handlePointerEvent(cloneEvent);
625             cloneEvent.recycle();
626         }
627 
628         return true;
629     }
630 
sendAccessibilityHoverEvent(int type)631     private void sendAccessibilityHoverEvent(int type) {
632         if (!mAccessibilityManager.isEnabled()) {
633             return;
634         }
635 
636         AccessibilityEvent event = AccessibilityEvent.obtain(type);
637         event.setImportantForAccessibility(true);
638         event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID);
639         event.setWindowId(
640                 AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
641         mAccessibilityManager.sendAccessibilityEvent(event);
642     }
643 
644     /**
645      * Called when the PiP menu state is in the process of animating/changing from one to another.
646      */
onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback)647     private void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
648         if (mMenuState == menuState && !resize) {
649             return;
650         }
651 
652         if (menuState == MENU_STATE_FULL && mMenuState != MENU_STATE_FULL) {
653             // Save the current snap fraction and if we do not drag or move the PiP, then
654             // we store back to this snap fraction.  Otherwise, we'll reset the snap
655             // fraction and snap to the closest edge.
656             if (resize) {
657                 // PIP is too small to show the menu actions and thus needs to be resized to a
658                 // size that can fit them all. Resize to the default size.
659                 animateToNormalSize(callback);
660             }
661         } else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) {
662             // Try and restore the PiP to the closest edge, using the saved snap fraction
663             // if possible
664             if (resize && !mPipResizeGestureHandler.isResizing()) {
665                 if (mDeferResizeToNormalBoundsUntilRotation == -1) {
666                     // This is a very special case: when the menu is expanded and visible,
667                     // navigating to another activity can trigger auto-enter PiP, and if the
668                     // revealed activity has a forced rotation set, then the controller will get
669                     // updated with the new rotation of the display. However, at the same time,
670                     // SystemUI will try to hide the menu by creating an animation to the normal
671                     // bounds which are now stale.  In such a case we defer the animation to the
672                     // normal bounds until after the next onMovementBoundsChanged() call to get the
673                     // bounds in the new orientation
674                     int displayRotation = mContext.getDisplay().getRotation();
675                     if (mDisplayRotation != displayRotation) {
676                         mDeferResizeToNormalBoundsUntilRotation = displayRotation;
677                     }
678                 }
679 
680                 if (mDeferResizeToNormalBoundsUntilRotation == -1) {
681                     animateToUnexpandedState(getUserResizeBounds());
682                 }
683             } else {
684                 mSavedSnapFraction = -1f;
685             }
686         }
687     }
688 
setMenuState(int menuState)689     private void setMenuState(int menuState) {
690         mMenuState = menuState;
691         updateMovementBounds();
692         // If pip menu has dismissed, we should register the A11y ActionReplacingConnection for pip
693         // as well, or it can't handle a11y focus and pip menu can't perform any action.
694         onRegistrationChanged(menuState == MENU_STATE_NONE);
695         if (menuState == MENU_STATE_NONE) {
696             mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_HIDE_MENU);
697         } else if (menuState == MENU_STATE_FULL) {
698             mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_MENU);
699         }
700     }
701 
animateToMaximizedState(Runnable callback)702     private void animateToMaximizedState(Runnable callback) {
703         Rect maxMovementBounds = new Rect();
704         Rect maxBounds = new Rect(0, 0, mPipBoundsState.getMaxSize().x,
705                 mPipBoundsState.getMaxSize().y);
706         mPipBoundsAlgorithm.getMovementBounds(maxBounds, mInsetBounds, maxMovementBounds,
707                 mIsImeShowing ? mImeHeight : 0);
708         mSavedSnapFraction = mMotionHelper.animateToExpandedState(maxBounds,
709                 mPipBoundsState.getMovementBounds(), maxMovementBounds,
710                 callback);
711     }
712 
animateToNormalSize(Runnable callback)713     private void animateToNormalSize(Runnable callback) {
714         // Save the current bounds as the user-resize bounds.
715         mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds());
716 
717         final Size minMenuSize = mMenuController.getEstimatedMinMenuSize();
718         final Rect normalBounds = mPipBoundsState.getNormalBounds();
719         final Rect destBounds = mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds,
720                 minMenuSize);
721         Rect restoredMovementBounds = new Rect();
722         mPipBoundsAlgorithm.getMovementBounds(destBounds,
723                 mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0);
724         mSavedSnapFraction = mMotionHelper.animateToExpandedState(destBounds,
725                 mPipBoundsState.getMovementBounds(), restoredMovementBounds, callback);
726     }
727 
animateToUnexpandedState(Rect restoreBounds)728     private void animateToUnexpandedState(Rect restoreBounds) {
729         Rect restoredMovementBounds = new Rect();
730         mPipBoundsAlgorithm.getMovementBounds(restoreBounds,
731                 mInsetBounds, restoredMovementBounds, mIsImeShowing ? mImeHeight : 0);
732         mMotionHelper.animateToUnexpandedState(restoreBounds, mSavedSnapFraction,
733                 restoredMovementBounds, mPipBoundsState.getMovementBounds(), false /* immediate */);
734         mSavedSnapFraction = -1f;
735     }
736 
animateToUnStashedState()737     private void animateToUnStashedState() {
738         final Rect pipBounds = mPipBoundsState.getBounds();
739         final boolean onLeftEdge = pipBounds.left < mPipBoundsState.getDisplayBounds().left;
740         final Rect unStashedBounds = new Rect(0, pipBounds.top, 0, pipBounds.bottom);
741         unStashedBounds.left = onLeftEdge ? mInsetBounds.left
742                 : mInsetBounds.right - pipBounds.width();
743         unStashedBounds.right = onLeftEdge ? mInsetBounds.left + pipBounds.width()
744                 : mInsetBounds.right;
745         mMotionHelper.animateToUnStashedBounds(unStashedBounds);
746     }
747 
748     /**
749      * @return the motion helper.
750      */
751     public PipMotionHelper getMotionHelper() {
752         return mMotionHelper;
753     }
754 
755     @VisibleForTesting
756     public PipResizeGestureHandler getPipResizeGestureHandler() {
757         return mPipResizeGestureHandler;
758     }
759 
760     @VisibleForTesting
761     public void setPipResizeGestureHandler(PipResizeGestureHandler pipResizeGestureHandler) {
762         mPipResizeGestureHandler = pipResizeGestureHandler;
763     }
764 
765     @VisibleForTesting
766     public void setPipMotionHelper(PipMotionHelper pipMotionHelper) {
767         mMotionHelper = pipMotionHelper;
768     }
769 
770     Rect getUserResizeBounds() {
771         return mPipResizeGestureHandler.getUserResizeBounds();
772     }
773 
774     /**
775      * Resizes the pip window and updates user resized bounds
776      *
777      * @param bounds target bounds to resize to
778      * @param snapFraction snap fraction to apply after resizing
779      */
780     void userResizeTo(Rect bounds, float snapFraction) {
781         mPipResizeGestureHandler.userResizeTo(bounds, snapFraction);
782     }
783 
784     /**
785      * Gesture controlling normal movement of the PIP.
786      */
787     private class DefaultPipTouchGesture extends PipTouchGesture {
788         private final Point mStartPosition = new Point();
789         private final PointF mDelta = new PointF();
790         private boolean mShouldHideMenuAfterFling;
791 
792         @Override
793         public void onDown(PipTouchState touchState) {
794             if (!touchState.isUserInteracting()) {
795                 return;
796             }
797 
798             Rect bounds = getPossiblyMotionBounds();
799             mDelta.set(0f, 0f);
800             mStartPosition.set(bounds.left, bounds.top);
801             mMovementWithinDismiss = touchState.getDownTouchPosition().y
802                     >= mPipBoundsState.getMovementBounds().bottom;
803             mMotionHelper.setSpringingToTouch(false);
804             mPipDismissTargetHandler.setTaskLeash(mPipTaskOrganizer.getSurfaceControl());
805 
806             // If the menu is still visible then just poke the menu
807             // so that it will timeout after the user stops touching it
808             if (mMenuState != MENU_STATE_NONE && !mPipBoundsState.isStashed()) {
809                 mMenuController.pokeMenu();
810             }
811         }
812 
813         @Override
onMove(PipTouchState touchState)814         public boolean onMove(PipTouchState touchState) {
815             if (!touchState.isUserInteracting()) {
816                 return false;
817             }
818 
819             if (touchState.startedDragging()) {
820                 mSavedSnapFraction = -1f;
821                 mPipDismissTargetHandler.showDismissTargetMaybe();
822             }
823 
824             if (touchState.isDragging()) {
825                 mPipBoundsState.setHasUserMovedPip(true);
826 
827                 // Move the pinned stack freely
828                 final PointF lastDelta = touchState.getLastTouchDelta();
829                 float lastX = mStartPosition.x + mDelta.x;
830                 float lastY = mStartPosition.y + mDelta.y;
831                 float left = lastX + lastDelta.x;
832                 float top = lastY + lastDelta.y;
833 
834                 // Add to the cumulative delta after bounding the position
835                 mDelta.x += left - lastX;
836                 mDelta.y += top - lastY;
837 
838                 mTmpBounds.set(getPossiblyMotionBounds());
839                 mTmpBounds.offsetTo((int) left, (int) top);
840                 mMotionHelper.movePip(mTmpBounds, true /* isDragging */);
841 
842                 final PointF curPos = touchState.getLastTouchPosition();
843                 if (mMovementWithinDismiss) {
844                     // Track if movement remains near the bottom edge to identify swipe to dismiss
845                     mMovementWithinDismiss = curPos.y >= mPipBoundsState.getMovementBounds().bottom;
846                 }
847                 return true;
848             }
849             return false;
850         }
851 
852         @Override
onUp(PipTouchState touchState)853         public boolean onUp(PipTouchState touchState) {
854             mPipDismissTargetHandler.hideDismissTargetMaybe();
855             mPipDismissTargetHandler.setTaskLeash(null);
856 
857             if (!touchState.isUserInteracting()) {
858                 return false;
859             }
860 
861             final PointF vel = touchState.getVelocity();
862 
863             if (touchState.isDragging()) {
864                 if (mMenuState != MENU_STATE_NONE) {
865                     // If the menu is still visible, then just poke the menu so that
866                     // it will timeout after the user stops touching it
867                     mMenuController.showMenu(mMenuState, mPipBoundsState.getBounds(),
868                             true /* allowMenuTimeout */, willResizeMenu(),
869                             shouldShowResizeHandle());
870                 }
871                 mShouldHideMenuAfterFling = mMenuState == MENU_STATE_NONE;
872 
873                 // Reset the touch state on up before the fling settles
874                 mTouchState.reset();
875                 if (mEnableStash && shouldStash(vel, getPossiblyMotionBounds())) {
876                     mMotionHelper.stashToEdge(vel.x, vel.y, this::stashEndAction /* endAction */);
877                 } else {
878                     if (mPipBoundsState.isStashed()) {
879                         // Reset stashed state if previously stashed
880                         mPipUiEventLogger.log(
881                                 PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED);
882                         mPipBoundsState.setStashed(STASH_TYPE_NONE);
883                     }
884                     mMotionHelper.flingToSnapTarget(vel.x, vel.y,
885                             this::flingEndAction /* endAction */);
886                 }
887             } else if (mTouchState.isDoubleTap() && !mPipBoundsState.isStashed()
888                     && mMenuState != MENU_STATE_FULL) {
889                 // If using pinch to zoom, double-tap functions as resizing between max/min size
890                 if (mPipResizeGestureHandler.isUsingPinchToZoom()) {
891                     final boolean toExpand = mPipBoundsState.getBounds().width()
892                             < mPipBoundsState.getMaxSize().x
893                             && mPipBoundsState.getBounds().height()
894                             < mPipBoundsState.getMaxSize().y;
895                     if (mMenuController.isMenuVisible()) {
896                         mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */);
897                     }
898 
899                     // the size to toggle to after a double tap
900                     int nextSize = PipDoubleTapHelper
901                             .nextSizeSpec(mPipBoundsState, getUserResizeBounds());
902 
903                     // actually toggle to the size chosen
904                     if (nextSize == PipDoubleTapHelper.SIZE_SPEC_MAX) {
905                         mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds());
906                         animateToMaximizedState(null);
907                     } else if (nextSize == PipDoubleTapHelper.SIZE_SPEC_DEFAULT) {
908                         mPipResizeGestureHandler.setUserResizeBounds(mPipBoundsState.getBounds());
909                         animateToNormalSize(null);
910                     } else {
911                         animateToUnexpandedState(getUserResizeBounds());
912                     }
913                 } else {
914                     // Expand to fullscreen if this is a double tap
915                     // the PiP should be frozen until the transition ends
916                     setTouchEnabled(false);
917                     mMotionHelper.expandLeavePip(false /* skipAnimation */);
918                 }
919             } else if (mMenuState != MENU_STATE_FULL) {
920                 if (mPipBoundsState.isStashed()) {
921                     // Unstash immediately if stashed, and don't wait for the double tap timeout
922                     animateToUnStashedState();
923                     mPipUiEventLogger.log(
924                             PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_UNSTASHED);
925                     mPipBoundsState.setStashed(STASH_TYPE_NONE);
926                     mTouchState.removeDoubleTapTimeoutCallback();
927                 } else if (!mTouchState.isWaitingForDoubleTap()) {
928                     // User has stalled long enough for this not to be a drag or a double tap,
929                     // just expand the menu
930                     mMenuController.showMenu(MENU_STATE_FULL, mPipBoundsState.getBounds(),
931                             true /* allowMenuTimeout */, willResizeMenu(),
932                             shouldShowResizeHandle());
933                 } else {
934                     // Next touch event _may_ be the second tap for the double-tap, schedule a
935                     // fallback runnable to trigger the menu if no touch event occurs before the
936                     // next tap
937                     mTouchState.scheduleDoubleTapTimeoutCallback();
938                 }
939             }
940             return true;
941         }
942 
943         private void stashEndAction() {
944             if (mPipBoundsState.getBounds().left < 0
945                     && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT) {
946                 mPipUiEventLogger.log(
947                         PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_LEFT);
948                 mPipBoundsState.setStashed(STASH_TYPE_LEFT);
949             } else if (mPipBoundsState.getBounds().left >= 0
950                     && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT) {
951                 mPipUiEventLogger.log(
952                         PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_STASH_RIGHT);
953                 mPipBoundsState.setStashed(STASH_TYPE_RIGHT);
954             }
955             mMenuController.hideMenu();
956         }
957 
958         private void flingEndAction() {
959             if (mShouldHideMenuAfterFling) {
960                 // If the menu is not visible, then we can still be showing the activity for the
961                 // dismiss overlay, so just finish it after the animation completes
962                 mMenuController.hideMenu();
963             }
964         }
965 
966         private boolean shouldStash(PointF vel, Rect motionBounds) {
967             final boolean flingToLeft = vel.x < -mStashVelocityThreshold;
968             final boolean flingToRight = vel.x > mStashVelocityThreshold;
969             final int offset = motionBounds.width() / 2;
970             final boolean droppingOnLeft =
971                     motionBounds.left < mPipBoundsState.getDisplayBounds().left - offset;
972             final boolean droppingOnRight =
973                     motionBounds.right > mPipBoundsState.getDisplayBounds().right + offset;
974 
975             // Do not allow stash if the destination edge contains display cutout. We only
976             // compare the left and right edges since we do not allow stash on top / bottom.
977             final DisplayCutout displayCutout =
978                     mPipBoundsState.getDisplayLayout().getDisplayCutout();
979             if (displayCutout != null) {
980                 if ((flingToLeft || droppingOnLeft)
981                         && !displayCutout.getBoundingRectLeft().isEmpty()) {
982                     return false;
983                 } else if ((flingToRight || droppingOnRight)
984                         && !displayCutout.getBoundingRectRight().isEmpty()) {
985                     return false;
986                 }
987             }
988 
989             // If user flings the PIP window above the minimum velocity, stash PIP.
990             // Only allow stashing to the edge if PIP wasn't previously stashed on the opposite
991             // edge.
992             final boolean stashFromFlingToEdge =
993                     (flingToLeft && mPipBoundsState.getStashedState() != STASH_TYPE_RIGHT)
994                     || (flingToRight && mPipBoundsState.getStashedState() != STASH_TYPE_LEFT);
995 
996             // If User releases the PIP window while it's out of the display bounds, put
997             // PIP into stashed mode.
998             final boolean stashFromDroppingOnEdge = droppingOnLeft || droppingOnRight;
999 
1000             return stashFromFlingToEdge || stashFromDroppingOnEdge;
1001         }
1002     }
1003 
1004     /**
1005      * Updates the current movement bounds based on whether the menu is currently visible and
1006      * resized.
1007      */
1008     private void updateMovementBounds() {
1009         mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(),
1010                 mInsetBounds, mPipBoundsState.getMovementBounds(), mIsImeShowing ? mImeHeight : 0);
1011         mMotionHelper.onMovementBoundsChanged();
1012     }
1013 
1014     private Rect getMovementBounds(Rect curBounds) {
1015         Rect movementBounds = new Rect();
1016         mPipBoundsAlgorithm.getMovementBounds(curBounds, mInsetBounds,
1017                 movementBounds, mIsImeShowing ? mImeHeight : 0);
1018         return movementBounds;
1019     }
1020 
1021     /**
1022      * @return {@code true} if the menu should be resized on tap because app explicitly specifies
1023      * PiP window size that is too small to hold all the actions.
1024      */
1025     private boolean willResizeMenu() {
1026         if (!mEnableResize) {
1027             return false;
1028         }
1029         final Size estimatedMinMenuSize = mMenuController.getEstimatedMinMenuSize();
1030         if (estimatedMinMenuSize == null) {
1031             ProtoLog.wtf(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
1032                     "%s: Failed to get estimated menu size", TAG);
1033             return false;
1034         }
1035         final Rect currentBounds = mPipBoundsState.getBounds();
1036         return currentBounds.width() < estimatedMinMenuSize.getWidth()
1037                 || currentBounds.height() < estimatedMinMenuSize.getHeight();
1038     }
1039 
1040     /**
1041      * Returns the PIP bounds if we're not in the middle of a motion operation, or the current,
1042      * temporary motion bounds otherwise.
1043      */
1044     Rect getPossiblyMotionBounds() {
1045         return mPipBoundsState.getMotionBoundsState().isInMotion()
1046                 ? mPipBoundsState.getMotionBoundsState().getBoundsInMotion()
1047                 : mPipBoundsState.getBounds();
1048     }
1049 
1050     void setOhmOffset(int offset) {
1051         mPipResizeGestureHandler.setOhmOffset(offset);
1052     }
1053 
1054     public void dump(PrintWriter pw, String prefix) {
1055         final String innerPrefix = prefix + "  ";
1056         pw.println(prefix + TAG);
1057         pw.println(innerPrefix + "mMenuState=" + mMenuState);
1058         pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing);
1059         pw.println(innerPrefix + "mImeHeight=" + mImeHeight);
1060         pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing);
1061         pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight);
1062         pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction);
1063         pw.println(innerPrefix + "mMovementBoundsExtraOffsets=" + mMovementBoundsExtraOffsets);
1064         mPipBoundsAlgorithm.dump(pw, innerPrefix);
1065         mTouchState.dump(pw, innerPrefix);
1066         if (mPipResizeGestureHandler != null) {
1067             mPipResizeGestureHandler.dump(pw, innerPrefix);
1068         }
1069     }
1070 
1071 }
1072