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