| /* |
| * Copyright (C) 2020 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.wm.shell.pip.phone; |
| |
| import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; |
| import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; |
| import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE; |
| import static android.view.WindowManager.INPUT_CONSUMER_PIP; |
| |
| import static com.android.internal.jank.InteractionJankMonitor.CUJ_PIP_TRANSITION; |
| import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission; |
| import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; |
| import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND; |
| import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; |
| import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN; |
| import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; |
| import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SAME; |
| import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_SNAP_AFTER_RESIZE; |
| import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; |
| import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_USER_RESIZE; |
| import static com.android.wm.shell.pip.PipAnimationController.isOutPipDirection; |
| import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_PIP; |
| |
| import android.app.ActivityManager; |
| import android.app.ActivityTaskManager; |
| import android.app.PictureInPictureParams; |
| import android.app.RemoteAction; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.pm.ActivityInfo; |
| import android.content.res.Configuration; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.os.RemoteException; |
| import android.os.SystemProperties; |
| import android.util.Pair; |
| import android.util.Size; |
| import android.view.DisplayInfo; |
| import android.view.InsetsState; |
| import android.view.SurfaceControl; |
| import android.view.WindowManagerGlobal; |
| import android.window.WindowContainerTransaction; |
| |
| import androidx.annotation.BinderThread; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.jank.InteractionJankMonitor; |
| import com.android.internal.protolog.common.ProtoLog; |
| import com.android.wm.shell.R; |
| import com.android.wm.shell.WindowManagerShellWrapper; |
| import com.android.wm.shell.common.DisplayChangeController; |
| import com.android.wm.shell.common.DisplayController; |
| import com.android.wm.shell.common.DisplayInsetsController; |
| import com.android.wm.shell.common.DisplayLayout; |
| import com.android.wm.shell.common.ExternalInterfaceBinder; |
| import com.android.wm.shell.common.RemoteCallable; |
| import com.android.wm.shell.common.ShellExecutor; |
| import com.android.wm.shell.common.SingleInstanceRemoteListener; |
| import com.android.wm.shell.common.TabletopModeController; |
| import com.android.wm.shell.common.TaskStackListenerCallback; |
| import com.android.wm.shell.common.TaskStackListenerImpl; |
| import com.android.wm.shell.onehanded.OneHandedController; |
| import com.android.wm.shell.onehanded.OneHandedTransitionCallback; |
| import com.android.wm.shell.pip.IPip; |
| import com.android.wm.shell.pip.IPipAnimationListener; |
| import com.android.wm.shell.pip.PinnedStackListenerForwarder; |
| import com.android.wm.shell.pip.Pip; |
| import com.android.wm.shell.pip.PipAnimationController; |
| import com.android.wm.shell.pip.PipAppOpsListener; |
| import com.android.wm.shell.pip.PipBoundsAlgorithm; |
| import com.android.wm.shell.pip.PipBoundsState; |
| import com.android.wm.shell.pip.PipDisplayLayoutState; |
| import com.android.wm.shell.pip.PipKeepClearAlgorithmInterface; |
| import com.android.wm.shell.pip.PipMediaController; |
| import com.android.wm.shell.pip.PipParamsChangedForwarder; |
| import com.android.wm.shell.pip.PipSnapAlgorithm; |
| import com.android.wm.shell.pip.PipTaskOrganizer; |
| import com.android.wm.shell.pip.PipTransitionController; |
| import com.android.wm.shell.pip.PipTransitionState; |
| import com.android.wm.shell.pip.PipUtils; |
| import com.android.wm.shell.protolog.ShellProtoLogGroup; |
| import com.android.wm.shell.sysui.ConfigurationChangeListener; |
| import com.android.wm.shell.sysui.KeyguardChangeListener; |
| import com.android.wm.shell.sysui.ShellCommandHandler; |
| import com.android.wm.shell.sysui.ShellController; |
| import com.android.wm.shell.sysui.ShellInit; |
| import com.android.wm.shell.sysui.UserChangeListener; |
| import com.android.wm.shell.transition.Transitions; |
| |
| import java.io.PrintWriter; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.function.Consumer; |
| |
| /** |
| * Manages the picture-in-picture (PIP) UI and states for Phones. |
| */ |
| public class PipController implements PipTransitionController.PipTransitionCallback, |
| RemoteCallable<PipController>, ConfigurationChangeListener, KeyguardChangeListener, |
| UserChangeListener { |
| private static final String TAG = "PipController"; |
| |
| private static final String LAUNCHER_KEEP_CLEAR_AREA_TAG = "hotseat"; |
| |
| private static final long PIP_KEEP_CLEAR_AREAS_DELAY = |
| SystemProperties.getLong("persist.wm.debug.pip_keep_clear_areas_delay", 200); |
| |
| private boolean mEnablePipKeepClearAlgorithm = |
| SystemProperties.getBoolean("persist.wm.debug.enable_pip_keep_clear_algorithm", true); |
| |
| @VisibleForTesting |
| void setEnablePipKeepClearAlgorithm(boolean value) { |
| mEnablePipKeepClearAlgorithm = value; |
| } |
| |
| private Context mContext; |
| protected ShellExecutor mMainExecutor; |
| private DisplayController mDisplayController; |
| private PipInputConsumer mPipInputConsumer; |
| private WindowManagerShellWrapper mWindowManagerShellWrapper; |
| private PipAnimationController mPipAnimationController; |
| private PipAppOpsListener mAppOpsListener; |
| private PipMediaController mMediaController; |
| private PipBoundsAlgorithm mPipBoundsAlgorithm; |
| private PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm; |
| private PipBoundsState mPipBoundsState; |
| private PipSizeSpecHandler mPipSizeSpecHandler; |
| private PipDisplayLayoutState mPipDisplayLayoutState; |
| private PipMotionHelper mPipMotionHelper; |
| private PipTouchHandler mTouchHandler; |
| private PipTransitionController mPipTransitionController; |
| private TaskStackListenerImpl mTaskStackListener; |
| private PipParamsChangedForwarder mPipParamsChangedForwarder; |
| private DisplayInsetsController mDisplayInsetsController; |
| private TabletopModeController mTabletopModeController; |
| private Optional<OneHandedController> mOneHandedController; |
| private final ShellCommandHandler mShellCommandHandler; |
| private final ShellController mShellController; |
| protected final PipImpl mImpl; |
| |
| private final Rect mTmpInsetBounds = new Rect(); |
| private final int mEnterAnimationDuration; |
| |
| private final Runnable mMovePipInResponseToKeepClearAreasChangeCallback = |
| this::onKeepClearAreasChangedCallback; |
| |
| private void onKeepClearAreasChangedCallback() { |
| if (mIsKeyguardShowingOrAnimating) { |
| // early bail out if the change was caused by keyguard showing up |
| return; |
| } |
| if (!mEnablePipKeepClearAlgorithm) { |
| // early bail out if the keep clear areas feature is disabled |
| return; |
| } |
| if (mPipBoundsState.isStashed()) { |
| // don't move when stashed |
| return; |
| } |
| // if there is another animation ongoing, wait for it to finish and try again |
| if (mPipAnimationController.isAnimating()) { |
| mMainExecutor.removeCallbacks( |
| mMovePipInResponseToKeepClearAreasChangeCallback); |
| mMainExecutor.executeDelayed( |
| mMovePipInResponseToKeepClearAreasChangeCallback, |
| PIP_KEEP_CLEAR_AREAS_DELAY); |
| return; |
| } |
| updatePipPositionForKeepClearAreas(); |
| } |
| |
| private void updatePipPositionForKeepClearAreas() { |
| if (!mEnablePipKeepClearAlgorithm) { |
| // early bail out if the keep clear areas feature is disabled |
| return; |
| } |
| if (mIsKeyguardShowingOrAnimating) { |
| // early bail out if the change was caused by keyguard showing up |
| return; |
| } |
| // only move if we're in PiP or transitioning into PiP |
| if (!mPipTransitionState.shouldBlockResizeRequest()) { |
| Rect destBounds = mPipKeepClearAlgorithm.adjust(mPipBoundsState, |
| mPipBoundsAlgorithm); |
| // only move if the bounds are actually different |
| if (!destBounds.equals(mPipBoundsState.getBounds())) { |
| if (mPipTransitionState.hasEnteredPip()) { |
| // if already in PiP, schedule separate animation |
| mPipTaskOrganizer.scheduleAnimateResizePip(destBounds, |
| mEnterAnimationDuration, null); |
| } else if (mPipTransitionState.isEnteringPip()) { |
| // while entering PiP we just need to update animator bounds |
| mPipTaskOrganizer.updateAnimatorBounds(destBounds); |
| } |
| } |
| } |
| } |
| |
| private boolean mIsInFixedRotation; |
| private PipAnimationListener mPinnedStackAnimationRecentsCallback; |
| |
| protected PhonePipMenuController mMenuController; |
| protected PipTaskOrganizer mPipTaskOrganizer; |
| private PipTransitionState mPipTransitionState; |
| protected PinnedStackListenerForwarder.PinnedTaskListener mPinnedTaskListener = |
| new PipControllerPinnedTaskListener(); |
| |
| private boolean mIsKeyguardShowingOrAnimating; |
| |
| private Consumer<Boolean> mOnIsInPipStateChangedListener; |
| |
| @VisibleForTesting |
| interface PipAnimationListener { |
| /** |
| * Notifies the listener that the Pip animation is started. |
| */ |
| void onPipAnimationStarted(); |
| |
| /** |
| * Notifies the listener about PiP resource dimensions changed. |
| * Listener can expect an immediate callback the first time they attach. |
| * |
| * @param cornerRadius the pixel value of the corner radius, zero means it's disabled. |
| * @param shadowRadius the pixel value of the shadow radius, zero means it's disabled. |
| */ |
| void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius); |
| |
| /** |
| * Notifies the listener that user leaves PiP by tapping on the expand button. |
| */ |
| void onExpandPip(); |
| } |
| |
| /** |
| * Handler for display rotation changes. |
| */ |
| private final DisplayChangeController.OnDisplayChangingListener mRotationController = ( |
| displayId, fromRotation, toRotation, newDisplayAreaInfo, t) -> { |
| if (mPipTransitionController.handleRotateDisplay(fromRotation, toRotation, t)) { |
| return; |
| } |
| if (mPipBoundsState.getDisplayLayout().rotation() == toRotation) { |
| // The same rotation may have been set by auto PiP-able or fixed rotation. So notify |
| // the change with fromRotation=false to apply the rotated destination bounds from |
| // PipTaskOrganizer#onMovementBoundsChanged. |
| updateMovementBounds(null, false /* fromRotation */, |
| false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t); |
| return; |
| } |
| if (!mPipTaskOrganizer.isInPip() || mPipTaskOrganizer.isEntryScheduled()) { |
| // Update display layout and bounds handler if we aren't in PIP or haven't actually |
| // entered PIP yet. |
| onDisplayRotationChangedNotInPip(mContext, toRotation); |
| // do not forget to update the movement bounds as well. |
| updateMovementBounds(mPipBoundsState.getNormalBounds(), true /* fromRotation */, |
| false /* fromImeAdjustment */, false /* fromShelfAdjustment */, t); |
| mPipTaskOrganizer.onDisplayRotationSkipped(); |
| return; |
| } |
| // If there is an animation running (ie. from a shelf offset), then ensure that we calculate |
| // the bounds for the next orientation using the destination bounds of the animation |
| // TODO: Technically this should account for movement animation bounds as well |
| Rect currentBounds = mPipTaskOrganizer.getCurrentOrAnimatingBounds(); |
| final Rect outBounds = new Rect(); |
| final boolean changed = onDisplayRotationChanged(mContext, outBounds, currentBounds, |
| mTmpInsetBounds, displayId, fromRotation, toRotation, t); |
| if (changed) { |
| // If the pip was in the offset zone earlier, adjust the new bounds to the bottom of the |
| // movement bounds |
| mTouchHandler.adjustBoundsForRotation(outBounds, mPipBoundsState.getBounds(), |
| mTmpInsetBounds); |
| |
| // The bounds are being applied to a specific snap fraction, so reset any known offsets |
| // for the previous orientation before updating the movement bounds. |
| // We perform the resets if and only if this callback is due to screen rotation but |
| // not during the fixed rotation. In fixed rotation case, app is about to enter PiP |
| // and we need the offsets preserved to calculate the destination bounds. |
| if (!mIsInFixedRotation) { |
| // Update the shelf visibility without updating the movement bounds. We're already |
| // updating them below with the |fromRotation| flag set, which is more accurate |
| // than using the |fromShelfAdjustment|. |
| mPipBoundsState.setShelfVisibility(false /* showing */, 0 /* height */, |
| false /* updateMovementBounds */); |
| mPipBoundsState.setImeVisibility(false /* showing */, 0 /* height */); |
| mTouchHandler.onShelfVisibilityChanged(false, 0); |
| mTouchHandler.onImeVisibilityChanged(false, 0); |
| } |
| |
| updateMovementBounds(outBounds, true /* fromRotation */, false /* fromImeAdjustment */, |
| false /* fromShelfAdjustment */, t); |
| } |
| }; |
| |
| @VisibleForTesting |
| final DisplayController.OnDisplaysChangedListener mDisplaysChangedListener = |
| new DisplayController.OnDisplaysChangedListener() { |
| @Override |
| public void onFixedRotationStarted(int displayId, int newRotation) { |
| mIsInFixedRotation = true; |
| } |
| |
| @Override |
| public void onFixedRotationFinished(int displayId) { |
| mIsInFixedRotation = false; |
| } |
| |
| @Override |
| public void onDisplayAdded(int displayId) { |
| if (displayId != mPipDisplayLayoutState.getDisplayId()) { |
| return; |
| } |
| onDisplayChanged(mDisplayController.getDisplayLayout(displayId), |
| false /* saveRestoreSnapFraction */); |
| } |
| |
| @Override |
| public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { |
| if (displayId != mPipDisplayLayoutState.getDisplayId()) { |
| return; |
| } |
| onDisplayChanged(mDisplayController.getDisplayLayout(displayId), |
| true /* saveRestoreSnapFraction */); |
| } |
| |
| @Override |
| public void onKeepClearAreasChanged(int displayId, Set<Rect> restricted, |
| Set<Rect> unrestricted) { |
| if (mPipDisplayLayoutState.getDisplayId() == displayId) { |
| if (mEnablePipKeepClearAlgorithm) { |
| mPipBoundsState.setKeepClearAreas(restricted, unrestricted); |
| |
| mMainExecutor.removeCallbacks( |
| mMovePipInResponseToKeepClearAreasChangeCallback); |
| mMainExecutor.executeDelayed( |
| mMovePipInResponseToKeepClearAreasChangeCallback, |
| PIP_KEEP_CLEAR_AREAS_DELAY); |
| } |
| } |
| } |
| }; |
| |
| /** |
| * Handler for messages from the PIP controller. |
| */ |
| private class PipControllerPinnedTaskListener extends |
| PinnedStackListenerForwarder.PinnedTaskListener { |
| @Override |
| public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { |
| mPipBoundsState.setImeVisibility(imeVisible, imeHeight); |
| mTouchHandler.onImeVisibilityChanged(imeVisible, imeHeight); |
| if (imeVisible) { |
| updatePipPositionForKeepClearAreas(); |
| } |
| } |
| |
| @Override |
| public void onMovementBoundsChanged(boolean fromImeAdjustment) { |
| updateMovementBounds(null /* toBounds */, |
| false /* fromRotation */, fromImeAdjustment, false /* fromShelfAdjustment */, |
| null /* windowContainerTransaction */); |
| } |
| |
| @Override |
| public void onActivityHidden(ComponentName componentName) { |
| if (componentName.equals(mPipBoundsState.getLastPipComponentName())) { |
| // The activity was removed, we don't want to restore to the reentry state |
| // saved for this component anymore. |
| mPipBoundsState.setLastPipComponentName(null); |
| } |
| } |
| } |
| |
| /** |
| * Instantiates {@link PipController}, returns {@code null} if the feature not supported. |
| */ |
| @Nullable |
| public static Pip create(Context context, |
| ShellInit shellInit, |
| ShellCommandHandler shellCommandHandler, |
| ShellController shellController, |
| DisplayController displayController, |
| PipAnimationController pipAnimationController, |
| PipAppOpsListener pipAppOpsListener, |
| PipBoundsAlgorithm pipBoundsAlgorithm, |
| PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, |
| PipBoundsState pipBoundsState, |
| PipSizeSpecHandler pipSizeSpecHandler, |
| PipDisplayLayoutState pipDisplayLayoutState, |
| PipMotionHelper pipMotionHelper, |
| PipMediaController pipMediaController, |
| PhonePipMenuController phonePipMenuController, |
| PipTaskOrganizer pipTaskOrganizer, |
| PipTransitionState pipTransitionState, |
| PipTouchHandler pipTouchHandler, |
| PipTransitionController pipTransitionController, |
| WindowManagerShellWrapper windowManagerShellWrapper, |
| TaskStackListenerImpl taskStackListener, |
| PipParamsChangedForwarder pipParamsChangedForwarder, |
| DisplayInsetsController displayInsetsController, |
| TabletopModeController pipTabletopController, |
| Optional<OneHandedController> oneHandedController, |
| ShellExecutor mainExecutor) { |
| if (!context.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { |
| ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, |
| "%s: Device doesn't support Pip feature", TAG); |
| return null; |
| } |
| |
| return new PipController(context, shellInit, shellCommandHandler, shellController, |
| displayController, pipAnimationController, pipAppOpsListener, |
| pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipSizeSpecHandler, |
| pipDisplayLayoutState, pipMotionHelper, pipMediaController, phonePipMenuController, |
| pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController, |
| windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder, |
| displayInsetsController, pipTabletopController, oneHandedController, mainExecutor) |
| .mImpl; |
| } |
| |
| protected PipController(Context context, |
| ShellInit shellInit, |
| ShellCommandHandler shellCommandHandler, |
| ShellController shellController, |
| DisplayController displayController, |
| PipAnimationController pipAnimationController, |
| PipAppOpsListener pipAppOpsListener, |
| PipBoundsAlgorithm pipBoundsAlgorithm, |
| PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, |
| @NonNull PipBoundsState pipBoundsState, |
| PipSizeSpecHandler pipSizeSpecHandler, |
| @NonNull PipDisplayLayoutState pipDisplayLayoutState, |
| PipMotionHelper pipMotionHelper, |
| PipMediaController pipMediaController, |
| PhonePipMenuController phonePipMenuController, |
| PipTaskOrganizer pipTaskOrganizer, |
| PipTransitionState pipTransitionState, |
| PipTouchHandler pipTouchHandler, |
| PipTransitionController pipTransitionController, |
| WindowManagerShellWrapper windowManagerShellWrapper, |
| TaskStackListenerImpl taskStackListener, |
| PipParamsChangedForwarder pipParamsChangedForwarder, |
| DisplayInsetsController displayInsetsController, |
| TabletopModeController tabletopModeController, |
| Optional<OneHandedController> oneHandedController, |
| ShellExecutor mainExecutor |
| ) { |
| |
| |
| mContext = context; |
| mShellCommandHandler = shellCommandHandler; |
| mShellController = shellController; |
| mImpl = new PipImpl(); |
| mWindowManagerShellWrapper = windowManagerShellWrapper; |
| mDisplayController = displayController; |
| mPipBoundsAlgorithm = pipBoundsAlgorithm; |
| mPipKeepClearAlgorithm = pipKeepClearAlgorithm; |
| mPipBoundsState = pipBoundsState; |
| mPipSizeSpecHandler = pipSizeSpecHandler; |
| mPipDisplayLayoutState = pipDisplayLayoutState; |
| mPipMotionHelper = pipMotionHelper; |
| mPipTaskOrganizer = pipTaskOrganizer; |
| mPipTransitionState = pipTransitionState; |
| mMainExecutor = mainExecutor; |
| mMediaController = pipMediaController; |
| mMenuController = phonePipMenuController; |
| mTouchHandler = pipTouchHandler; |
| mPipAnimationController = pipAnimationController; |
| mAppOpsListener = pipAppOpsListener; |
| mOneHandedController = oneHandedController; |
| mPipTransitionController = pipTransitionController; |
| mTaskStackListener = taskStackListener; |
| |
| mEnterAnimationDuration = mContext.getResources() |
| .getInteger(R.integer.config_pipEnterAnimationDuration); |
| mPipParamsChangedForwarder = pipParamsChangedForwarder; |
| mDisplayInsetsController = displayInsetsController; |
| mTabletopModeController = tabletopModeController; |
| |
| shellInit.addInitCallback(this::onInit, this); |
| } |
| |
| private void onInit() { |
| mShellCommandHandler.addDumpCallback(this::dump, this); |
| mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(), |
| INPUT_CONSUMER_PIP, mMainExecutor); |
| mPipTransitionController.registerPipTransitionCallback(this); |
| mPipTaskOrganizer.registerOnDisplayIdChangeCallback((int displayId) -> { |
| mPipDisplayLayoutState.setDisplayId(displayId); |
| onDisplayChanged(mDisplayController.getDisplayLayout(displayId), |
| false /* saveRestoreSnapFraction */); |
| }); |
| mPipTransitionState.addOnPipTransitionStateChangedListener((oldState, newState) -> { |
| if (mOnIsInPipStateChangedListener != null) { |
| final boolean wasInPip = PipTransitionState.isInPip(oldState); |
| final boolean nowInPip = PipTransitionState.isInPip(newState); |
| if (nowInPip != wasInPip) { |
| mOnIsInPipStateChangedListener.accept(nowInPip); |
| } |
| } |
| }); |
| mPipBoundsState.setOnMinimalSizeChangeCallback( |
| () -> { |
| // The minimal size drives the normal bounds, so they need to be recalculated. |
| updateMovementBounds(null /* toBounds */, false /* fromRotation */, |
| false /* fromImeAdjustment */, false /* fromShelfAdjustment */, |
| null /* wct */); |
| }); |
| mPipBoundsState.setOnShelfVisibilityChangeCallback( |
| (isShowing, height, updateMovementBounds) -> { |
| mTouchHandler.onShelfVisibilityChanged(isShowing, height); |
| if (updateMovementBounds) { |
| updateMovementBounds(mPipBoundsState.getBounds(), |
| false /* fromRotation */, false /* fromImeAdjustment */, |
| true /* fromShelfAdjustment */, |
| null /* windowContainerTransaction */); |
| } |
| }); |
| if (mTouchHandler != null) { |
| // Register the listener for input consumer touch events. Only for Phone |
| mPipInputConsumer.setInputListener(mTouchHandler::handleTouchEvent); |
| mPipInputConsumer.setRegistrationListener(mTouchHandler::onRegistrationChanged); |
| } |
| mDisplayController.addDisplayChangingController(mRotationController); |
| mDisplayController.addDisplayWindowListener(mDisplaysChangedListener); |
| |
| // Ensure that we have the display info in case we get calls to update the bounds before the |
| // listener calls back |
| mPipDisplayLayoutState.setDisplayId(mContext.getDisplayId()); |
| |
| DisplayLayout layout = new DisplayLayout(mContext, mContext.getDisplay()); |
| mPipDisplayLayoutState.setDisplayLayout(layout); |
| |
| try { |
| mWindowManagerShellWrapper.addPinnedStackListener(mPinnedTaskListener); |
| } catch (RemoteException e) { |
| ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, |
| "%s: Failed to register pinned stack listener, %s", TAG, e); |
| } |
| |
| try { |
| ActivityTaskManager.RootTaskInfo taskInfo = ActivityTaskManager.getService() |
| .getRootTaskInfo(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); |
| if (taskInfo != null) { |
| // If SystemUI restart, and it already existed a pinned stack, |
| // register the pip input consumer to ensure touch can send to it. |
| mPipInputConsumer.registerInputConsumer(); |
| } |
| } catch (RemoteException | UnsupportedOperationException e) { |
| ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, |
| "%s: Failed to register pinned stack listener, %s", TAG, e); |
| e.printStackTrace(); |
| } |
| |
| // Handle for system task stack changes. |
| mTaskStackListener.addListener( |
| new TaskStackListenerCallback() { |
| @Override |
| public void onActivityPinned(String packageName, int userId, int taskId, |
| int stackId) { |
| mTouchHandler.onActivityPinned(); |
| mMediaController.onActivityPinned(); |
| mAppOpsListener.onActivityPinned(packageName); |
| mPipInputConsumer.registerInputConsumer(); |
| } |
| |
| @Override |
| public void onActivityUnpinned() { |
| final Pair<ComponentName, Integer> topPipActivityInfo = |
| PipUtils.getTopPipActivity(mContext); |
| final ComponentName topActivity = topPipActivityInfo.first; |
| mTouchHandler.onActivityUnpinned(topActivity); |
| mAppOpsListener.onActivityUnpinned(); |
| mPipInputConsumer.unregisterInputConsumer(); |
| } |
| |
| @Override |
| public void onActivityRestartAttempt(ActivityManager.RunningTaskInfo task, |
| boolean homeTaskVisible, boolean clearedTask, boolean wasVisible) { |
| if (task.getWindowingMode() != WINDOWING_MODE_PINNED) { |
| return; |
| } |
| if (mPipTaskOrganizer.isLaunchToSplit(task)) { |
| mTouchHandler.getMotionHelper().expandIntoSplit(); |
| } else { |
| mTouchHandler.getMotionHelper().expandLeavePip( |
| clearedTask /* skipAnimation */); |
| } |
| } |
| }); |
| |
| mPipParamsChangedForwarder.addListener( |
| new PipParamsChangedForwarder.PipParamsChangedCallback() { |
| @Override |
| public void onAspectRatioChanged(float ratio) { |
| mPipBoundsState.setAspectRatio(ratio); |
| |
| final Rect destinationBounds = |
| mPipBoundsAlgorithm.getAdjustedDestinationBounds( |
| mPipBoundsState.getBounds(), |
| mPipBoundsState.getAspectRatio()); |
| Objects.requireNonNull(destinationBounds, "Missing destination bounds"); |
| if (!destinationBounds.equals(mPipBoundsState.getBounds())) { |
| mPipTaskOrganizer.scheduleAnimateResizePip(destinationBounds, |
| mEnterAnimationDuration, |
| null /* updateBoundsCallback */); |
| mTouchHandler.onAspectRatioChanged(); |
| updateMovementBounds(null /* toBounds */, false /* fromRotation */, |
| false /* fromImeAdjustment */, false /* fromShelfAdjustment */, |
| null /* windowContainerTransaction */); |
| } else { |
| // when we enter pip for the first time, the destination bounds and pip |
| // bounds will already match, since they are calculated prior to |
| // starting the animation, so we only need to update the min/max size |
| // that is used for e.g. double tap to maximized state |
| mTouchHandler.updateMinMaxSize(ratio); |
| } |
| } |
| |
| @Override |
| public void onActionsChanged(List<RemoteAction> actions, |
| RemoteAction closeAction) { |
| mMenuController.setAppActions(actions, closeAction); |
| } |
| }); |
| |
| mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(), |
| new DisplayInsetsController.OnInsetsChangedListener() { |
| @Override |
| public void insetsChanged(InsetsState insetsState) { |
| DisplayLayout pendingLayout = mDisplayController |
| .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()); |
| if (mIsInFixedRotation |
| || mIsKeyguardShowingOrAnimating |
| || pendingLayout.rotation() |
| != mPipBoundsState.getDisplayLayout().rotation()) { |
| // bail out if there is a pending rotation or fixed rotation change or |
| // there's a keyguard present |
| return; |
| } |
| int oldMaxMovementBound = mPipBoundsState.getMovementBounds().bottom; |
| onDisplayChangedUncheck(mDisplayController |
| .getDisplayLayout(mPipDisplayLayoutState.getDisplayId()), |
| false /* saveRestoreSnapFraction */); |
| int newMaxMovementBound = mPipBoundsState.getMovementBounds().bottom; |
| if (!mEnablePipKeepClearAlgorithm) { |
| // offset PiP to adjust for bottom inset change |
| int pipTop = mPipBoundsState.getBounds().top; |
| int diff = newMaxMovementBound - oldMaxMovementBound; |
| if (diff < 0 && pipTop > newMaxMovementBound) { |
| // bottom inset has increased, move PiP up if it is too low |
| mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(), |
| newMaxMovementBound - pipTop); |
| } |
| if (diff > 0 && oldMaxMovementBound == pipTop) { |
| // bottom inset has decreased, move PiP down if it was by the edge |
| mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(), diff); |
| } |
| } |
| } |
| }); |
| |
| mTabletopModeController.registerOnTabletopModeChangedListener((isInTabletopMode) -> { |
| if (!mTabletopModeController.enableMoveFloatingWindowInTabletop()) return; |
| final String tag = "tabletop-mode"; |
| if (!isInTabletopMode) { |
| mPipBoundsState.removeNamedUnrestrictedKeepClearArea(tag); |
| return; |
| } |
| |
| // To prepare for the entry bounds. |
| final Rect displayBounds = mPipBoundsState.getDisplayBounds(); |
| if (mTabletopModeController.getPreferredHalfInTabletopMode() |
| == TabletopModeController.PREFERRED_TABLETOP_HALF_TOP) { |
| // Prefer top, avoid the bottom half of the display. |
| mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( |
| displayBounds.left, displayBounds.centerY(), |
| displayBounds.right, displayBounds.bottom)); |
| } else { |
| // Prefer bottom, avoid the top half of the display. |
| mPipBoundsState.addNamedUnrestrictedKeepClearArea(tag, new Rect( |
| displayBounds.left, displayBounds.top, |
| displayBounds.right, displayBounds.centerY())); |
| } |
| |
| // Try to move the PiP window if we have entered PiP mode. |
| if (mPipTransitionState.hasEnteredPip()) { |
| final Rect pipBounds = mPipBoundsState.getBounds(); |
| final Point edgeInsets = mPipSizeSpecHandler.getScreenEdgeInsets(); |
| if ((pipBounds.height() + 2 * edgeInsets.y) > (displayBounds.height() / 2)) { |
| // PiP bounds is too big to fit either half, bail early. |
| return; |
| } |
| mMainExecutor.removeCallbacks(mMovePipInResponseToKeepClearAreasChangeCallback); |
| mMainExecutor.execute(mMovePipInResponseToKeepClearAreasChangeCallback); |
| } |
| }); |
| |
| mOneHandedController.ifPresent(controller -> { |
| controller.registerTransitionCallback( |
| new OneHandedTransitionCallback() { |
| @Override |
| public void onStartFinished(Rect bounds) { |
| mTouchHandler.setOhmOffset(bounds.top); |
| } |
| |
| @Override |
| public void onStopFinished(Rect bounds) { |
| mTouchHandler.setOhmOffset(bounds.top); |
| } |
| }); |
| }); |
| |
| mMediaController.registerSessionListenerForCurrentUser(); |
| |
| mShellController.addConfigurationChangeListener(this); |
| mShellController.addKeyguardChangeListener(this); |
| mShellController.addUserChangeListener(this); |
| mShellController.addExternalInterface(KEY_EXTRA_SHELL_PIP, |
| this::createExternalInterface, this); |
| } |
| |
| private ExternalInterfaceBinder createExternalInterface() { |
| return new IPipImpl(this); |
| } |
| |
| @Override |
| public Context getContext() { |
| return mContext; |
| } |
| |
| @Override |
| public ShellExecutor getRemoteCallExecutor() { |
| return mMainExecutor; |
| } |
| |
| @Override |
| public void onUserChanged(int newUserId, @NonNull Context userContext) { |
| // Re-register the media session listener when switching users |
| mMediaController.registerSessionListenerForCurrentUser(); |
| } |
| |
| @Override |
| public void onConfigurationChanged(Configuration newConfig) { |
| mPipBoundsAlgorithm.onConfigurationChanged(mContext); |
| mTouchHandler.onConfigurationChanged(); |
| mPipBoundsState.onConfigurationChanged(); |
| mPipSizeSpecHandler.onConfigurationChanged(); |
| } |
| |
| @Override |
| public void onDensityOrFontScaleChanged() { |
| mPipTaskOrganizer.onDensityOrFontScaleChanged(mContext); |
| onPipResourceDimensionsChanged(); |
| } |
| |
| @Override |
| public void onThemeChanged() { |
| mTouchHandler.onOverlayChanged(); |
| onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay()), |
| false /* saveRestoreSnapFraction */); |
| } |
| |
| private void onDisplayChanged(DisplayLayout layout, boolean saveRestoreSnapFraction) { |
| if (!mPipDisplayLayoutState.getDisplayLayout().isSameGeometry(layout)) { |
| PipAnimationController.PipTransitionAnimator animator = |
| mPipAnimationController.getCurrentAnimator(); |
| if (animator != null && animator.isRunning()) { |
| // cancel any running animator, as it is using stale display layout information |
| animator.cancel(); |
| } |
| onDisplayChangedUncheck(layout, saveRestoreSnapFraction); |
| } |
| } |
| |
| private void onDisplayChangedUncheck(DisplayLayout layout, boolean saveRestoreSnapFraction) { |
| Runnable updateDisplayLayout = () -> { |
| final boolean fromRotation = Transitions.ENABLE_SHELL_TRANSITIONS |
| && mPipDisplayLayoutState.getDisplayLayout().rotation() != layout.rotation(); |
| |
| // update the internal state of objects subscribed to display changes |
| mPipDisplayLayoutState.setDisplayLayout(layout); |
| |
| final WindowContainerTransaction wct = |
| fromRotation ? new WindowContainerTransaction() : null; |
| updateMovementBounds(null /* toBounds */, |
| fromRotation, false /* fromImeAdjustment */, |
| false /* fromShelfAdjustment */, |
| wct /* windowContainerTransaction */); |
| if (wct != null) { |
| mPipTaskOrganizer.applyFinishBoundsResize(wct, TRANSITION_DIRECTION_SAME, |
| false /* wasPipTopLeft */); |
| } |
| }; |
| |
| if (mPipTaskOrganizer.isInPip() && saveRestoreSnapFraction) { |
| mMenuController.attachPipMenuView(); |
| // Calculate the snap fraction of the current stack along the old movement bounds |
| final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm(); |
| final Rect postChangeBounds = new Rect(mPipBoundsState.getBounds()); |
| final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeBounds, |
| mPipBoundsAlgorithm.getMovementBounds(postChangeBounds), |
| mPipBoundsState.getStashedState()); |
| |
| // Scale PiP on density dpi change, so it appears to be the same size physically. |
| final boolean densityDpiChanged = |
| mPipDisplayLayoutState.getDisplayLayout().densityDpi() != 0 |
| && (mPipDisplayLayoutState.getDisplayLayout().densityDpi() |
| != layout.densityDpi()); |
| if (densityDpiChanged) { |
| final float scale = (float) layout.densityDpi() |
| / mPipDisplayLayoutState.getDisplayLayout().densityDpi(); |
| postChangeBounds.set(0, 0, |
| (int) (postChangeBounds.width() * scale), |
| (int) (postChangeBounds.height() * scale)); |
| } |
| |
| updateDisplayLayout.run(); |
| |
| // Calculate the PiP bounds in the new orientation based on same fraction along the |
| // rotated movement bounds. |
| final Rect postChangeMovementBounds = mPipBoundsAlgorithm.getMovementBounds( |
| postChangeBounds, false /* adjustForIme */); |
| pipSnapAlgorithm.applySnapFraction(postChangeBounds, postChangeMovementBounds, |
| snapFraction, mPipBoundsState.getStashedState(), |
| mPipBoundsState.getStashOffset(), |
| mPipDisplayLayoutState.getDisplayBounds(), |
| mPipDisplayLayoutState.getDisplayLayout().stableInsets()); |
| |
| if (densityDpiChanged) { |
| // Using PipMotionHelper#movePip directly here may cause race condition since |
| // the app content in PiP mode may or may not be updated for the new density dpi. |
| final int duration = mContext.getResources().getInteger( |
| R.integer.config_pipEnterAnimationDuration); |
| mPipTaskOrganizer.scheduleAnimateResizePip( |
| postChangeBounds, duration, null /* updateBoundsCallback */); |
| } else { |
| // Directly move PiP to its final destination bounds without animation. |
| mPipTaskOrganizer.scheduleFinishResizePip(postChangeBounds); |
| } |
| |
| // if the pip window size is beyond allowed bounds user resize to normal bounds |
| if (mPipBoundsState.getBounds().width() < mPipBoundsState.getMinSize().x |
| || mPipBoundsState.getBounds().width() > mPipBoundsState.getMaxSize().x |
| || mPipBoundsState.getBounds().height() < mPipBoundsState.getMinSize().y |
| || mPipBoundsState.getBounds().height() > mPipBoundsState.getMaxSize().y) { |
| mTouchHandler.userResizeTo(mPipBoundsState.getNormalBounds(), snapFraction); |
| } |
| |
| } else { |
| updateDisplayLayout.run(); |
| } |
| } |
| |
| private void onSystemUiStateChanged(boolean isValidState, int flag) { |
| mTouchHandler.onSystemUiStateChanged(isValidState); |
| } |
| |
| /** |
| * Expands the PIP. |
| */ |
| public void expandPip() { |
| mTouchHandler.getMotionHelper().expandLeavePip(false /* skipAnimation */); |
| } |
| |
| /** |
| * Hides the PIP menu. |
| */ |
| public void hidePipMenu(Runnable onStartCallback, Runnable onEndCallback) { |
| mMenuController.hideMenu(onStartCallback, onEndCallback); |
| } |
| |
| /** |
| * Sent from KEYCODE_WINDOW handler in PhoneWindowManager, to request the menu to be shown. |
| */ |
| public void showPictureInPictureMenu() { |
| mTouchHandler.showPictureInPictureMenu(); |
| } |
| |
| /** |
| * If {@param keyguardShowing} is {@code false} and {@param animating} is {@code true}, |
| * we would wait till the dismissing animation of keyguard and surfaces behind to be |
| * finished first to reset the visibility of PiP window. |
| * See also {@link #onKeyguardDismissAnimationFinished()} |
| */ |
| @Override |
| public void onKeyguardVisibilityChanged(boolean visible, boolean occluded, |
| boolean animatingDismiss) { |
| if (!mPipTransitionState.hasEnteredPip()) { |
| return; |
| } |
| if (visible) { |
| mIsKeyguardShowingOrAnimating = true; |
| hidePipMenu(null /* onStartCallback */, null /* onEndCallback */); |
| mPipTaskOrganizer.setPipVisibility(false); |
| } else if (!animatingDismiss) { |
| mIsKeyguardShowingOrAnimating = false; |
| mPipTaskOrganizer.setPipVisibility(true); |
| } |
| } |
| |
| @Override |
| public void onKeyguardDismissAnimationFinished() { |
| if (mPipTaskOrganizer.isInPip()) { |
| mIsKeyguardShowingOrAnimating = false; |
| mPipTaskOrganizer.setPipVisibility(true); |
| } |
| } |
| |
| /** |
| * Sets a customized touch gesture that replaces the default one. |
| */ |
| public void setTouchGesture(PipTouchGesture gesture) { |
| mTouchHandler.setTouchGesture(gesture); |
| } |
| |
| /** |
| * Sets both shelf visibility and its height. |
| */ |
| private void setShelfHeight(boolean visible, int height) { |
| if (!mIsKeyguardShowingOrAnimating) { |
| setShelfHeightLocked(visible, height); |
| } |
| } |
| |
| private void setLauncherKeepClearAreaHeight(boolean visible, int height) { |
| if (visible) { |
| Rect rect = new Rect( |
| 0, mPipBoundsState.getDisplayBounds().bottom - height, |
| mPipBoundsState.getDisplayBounds().right, |
| mPipBoundsState.getDisplayBounds().bottom); |
| mPipBoundsState.addNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG, rect); |
| updatePipPositionForKeepClearAreas(); |
| } else { |
| mPipBoundsState.removeNamedUnrestrictedKeepClearArea(LAUNCHER_KEEP_CLEAR_AREA_TAG); |
| // postpone moving in response to hide of Launcher in case there's another change |
| mMainExecutor.removeCallbacks(mMovePipInResponseToKeepClearAreasChangeCallback); |
| mMainExecutor.executeDelayed( |
| mMovePipInResponseToKeepClearAreasChangeCallback, |
| PIP_KEEP_CLEAR_AREAS_DELAY); |
| } |
| } |
| |
| private void setLauncherAppIconSize(int iconSizePx) { |
| mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx); |
| } |
| |
| private void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { |
| mOnIsInPipStateChangedListener = callback; |
| if (mOnIsInPipStateChangedListener != null) { |
| callback.accept(mPipTransitionState.isInPip()); |
| } |
| } |
| |
| private void setShelfHeightLocked(boolean visible, int height) { |
| final int shelfHeight = visible ? height : 0; |
| mPipBoundsState.setShelfVisibility(visible, shelfHeight); |
| } |
| |
| @VisibleForTesting |
| void setPinnedStackAnimationListener(PipAnimationListener callback) { |
| mPinnedStackAnimationRecentsCallback = callback; |
| onPipResourceDimensionsChanged(); |
| } |
| |
| @VisibleForTesting |
| boolean hasPinnedStackAnimationListener() { |
| return mPinnedStackAnimationRecentsCallback != null; |
| } |
| |
| private void onPipResourceDimensionsChanged() { |
| if (mPinnedStackAnimationRecentsCallback != null) { |
| mPinnedStackAnimationRecentsCallback.onPipResourceDimensionsChanged( |
| mContext.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius), |
| mContext.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius)); |
| } |
| } |
| |
| private Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, |
| PictureInPictureParams pictureInPictureParams, |
| int launcherRotation, Rect hotseatKeepClearArea) { |
| |
| if (mEnablePipKeepClearAlgorithm) { |
| // pre-emptively add the keep clear area for Hotseat, so that it is taken into account |
| // when calculating the entry destination bounds of PiP window |
| mPipBoundsState.getRestrictedKeepClearAreas().add(hotseatKeepClearArea); |
| } else { |
| int shelfHeight = hotseatKeepClearArea.height(); |
| setShelfHeightLocked(shelfHeight > 0 /* visible */, shelfHeight); |
| } |
| onDisplayRotationChangedNotInPip(mContext, launcherRotation); |
| final Rect entryBounds = mPipTaskOrganizer.startSwipePipToHome(componentName, activityInfo, |
| pictureInPictureParams); |
| // sync mPipBoundsState with the newly calculated bounds. |
| mPipBoundsState.setNormalBounds(entryBounds); |
| return entryBounds; |
| } |
| |
| private void stopSwipePipToHome(int taskId, ComponentName componentName, Rect destinationBounds, |
| SurfaceControl overlay) { |
| mPipTaskOrganizer.stopSwipePipToHome(taskId, componentName, destinationBounds, overlay); |
| } |
| |
| private String getTransitionTag(int direction) { |
| switch (direction) { |
| case TRANSITION_DIRECTION_TO_PIP: |
| return "TRANSITION_TO_PIP"; |
| case TRANSITION_DIRECTION_LEAVE_PIP: |
| return "TRANSITION_LEAVE_PIP"; |
| case TRANSITION_DIRECTION_LEAVE_PIP_TO_SPLIT_SCREEN: |
| return "TRANSITION_LEAVE_PIP_TO_SPLIT_SCREEN"; |
| case TRANSITION_DIRECTION_REMOVE_STACK: |
| return "TRANSITION_REMOVE_STACK"; |
| case TRANSITION_DIRECTION_SNAP_AFTER_RESIZE: |
| return "TRANSITION_SNAP_AFTER_RESIZE"; |
| case TRANSITION_DIRECTION_USER_RESIZE: |
| return "TRANSITION_USER_RESIZE"; |
| case TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND: |
| return "TRANSITION_EXPAND_OR_UNEXPAND"; |
| default: |
| return "TRANSITION_LEAVE_UNKNOWN"; |
| } |
| } |
| |
| @Override |
| public void onPipTransitionStarted(int direction, Rect pipBounds) { |
| // Begin InteractionJankMonitor with PIP transition CUJs |
| final InteractionJankMonitor.Configuration.Builder builder = |
| InteractionJankMonitor.Configuration.Builder.withSurface( |
| CUJ_PIP_TRANSITION, mContext, mPipTaskOrganizer.getSurfaceControl()) |
| .setTag(getTransitionTag(direction)) |
| .setTimeout(2000); |
| InteractionJankMonitor.getInstance().begin(builder); |
| |
| if (isOutPipDirection(direction)) { |
| // Exiting PIP, save the reentry state to restore to when re-entering. |
| saveReentryState(pipBounds); |
| } |
| // Disable touches while the animation is running |
| mTouchHandler.setTouchEnabled(false); |
| if (mPinnedStackAnimationRecentsCallback != null) { |
| mPinnedStackAnimationRecentsCallback.onPipAnimationStarted(); |
| if (direction == TRANSITION_DIRECTION_LEAVE_PIP) { |
| mPinnedStackAnimationRecentsCallback.onExpandPip(); |
| } |
| } |
| } |
| |
| /** Save the state to restore to on re-entry. */ |
| public void saveReentryState(Rect pipBounds) { |
| float snapFraction = mPipBoundsAlgorithm.getSnapFraction(pipBounds); |
| |
| if (!mPipBoundsState.hasUserResizedPip()) { |
| mPipBoundsState.saveReentryState(null /* bounds */, snapFraction); |
| return; |
| } |
| |
| Size reentrySize = new Size(pipBounds.width(), pipBounds.height()); |
| |
| // TODO: b/279937014 Investigate why userResizeBounds are empty with shell transitions on |
| // fallback to using the userResizeBounds if userResizeBounds are not empty |
| if (!mTouchHandler.getUserResizeBounds().isEmpty()) { |
| Rect userResizeBounds = mTouchHandler.getUserResizeBounds(); |
| reentrySize = new Size(userResizeBounds.width(), userResizeBounds.height()); |
| } |
| |
| mPipBoundsState.saveReentryState(reentrySize, snapFraction); |
| } |
| |
| @Override |
| public void onPipTransitionFinished(int direction) { |
| onPipTransitionFinishedOrCanceled(direction); |
| } |
| |
| @Override |
| public void onPipTransitionCanceled(int direction) { |
| onPipTransitionFinishedOrCanceled(direction); |
| } |
| |
| private void onPipTransitionFinishedOrCanceled(int direction) { |
| // End InteractionJankMonitor with PIP transition by CUJs |
| InteractionJankMonitor.getInstance().end(CUJ_PIP_TRANSITION); |
| |
| // Re-enable touches after the animation completes |
| mTouchHandler.setTouchEnabled(true); |
| mTouchHandler.onPinnedStackAnimationEnded(direction); |
| } |
| |
| private void updateMovementBounds(@Nullable Rect toBounds, boolean fromRotation, |
| boolean fromImeAdjustment, boolean fromShelfAdjustment, |
| WindowContainerTransaction wct) { |
| // Populate inset / normal bounds and DisplayInfo from mPipBoundsHandler before |
| // passing to mTouchHandler/mPipTaskOrganizer |
| final Rect outBounds = new Rect(toBounds); |
| final int rotation = mPipDisplayLayoutState.getDisplayLayout().rotation(); |
| |
| mPipBoundsAlgorithm.getInsetBounds(mTmpInsetBounds); |
| mPipBoundsState.setNormalBounds(mPipBoundsAlgorithm.getNormalBounds()); |
| if (outBounds.isEmpty()) { |
| outBounds.set(mPipBoundsAlgorithm.getDefaultBounds()); |
| } |
| |
| // mTouchHandler would rely on the bounds populated from mPipTaskOrganizer |
| mPipTaskOrganizer.onMovementBoundsChanged(outBounds, fromRotation, fromImeAdjustment, |
| fromShelfAdjustment, wct); |
| mPipTaskOrganizer.finishResizeForMenu(outBounds); |
| mTouchHandler.onMovementBoundsChanged(mTmpInsetBounds, mPipBoundsState.getNormalBounds(), |
| outBounds, fromImeAdjustment, fromShelfAdjustment, rotation); |
| } |
| |
| /** |
| * Updates the display info and display layout on rotation change. This is needed even when we |
| * aren't in PIP because the rotation layout is used to calculate the proper insets for the |
| * next enter animation into PIP. |
| */ |
| private void onDisplayRotationChangedNotInPip(Context context, int toRotation) { |
| // Update the display layout, note that we have to do this on every rotation even if we |
| // aren't in PIP since we need to update the display layout to get the right resources |
| mPipDisplayLayoutState.rotateTo(toRotation); |
| } |
| |
| /** |
| * Updates the display info, calculating and returning the new stack and movement bounds in the |
| * new orientation of the device if necessary. |
| * |
| * @return {@code true} if internal {@link DisplayInfo} is rotated, {@code false} otherwise. |
| */ |
| private boolean onDisplayRotationChanged(Context context, Rect outBounds, Rect oldBounds, |
| Rect outInsetBounds, |
| int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) { |
| // Bail early if the event is not sent to current display |
| if ((displayId != mPipDisplayLayoutState.getDisplayId()) || (fromRotation == toRotation)) { |
| return false; |
| } |
| |
| // Bail early if the pinned task is staled. |
| final ActivityTaskManager.RootTaskInfo pinnedTaskInfo; |
| try { |
| pinnedTaskInfo = ActivityTaskManager.getService() |
| .getRootTaskInfo(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED); |
| if (pinnedTaskInfo == null) return false; |
| } catch (RemoteException e) { |
| ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, |
| "%s: Failed to get RootTaskInfo for pinned task, %s", TAG, e); |
| return false; |
| } |
| final PipSnapAlgorithm pipSnapAlgorithm = mPipBoundsAlgorithm.getSnapAlgorithm(); |
| |
| // Calculate the snap fraction of the current stack along the old movement bounds |
| final Rect postChangeStackBounds = new Rect(oldBounds); |
| final float snapFraction = pipSnapAlgorithm.getSnapFraction(postChangeStackBounds, |
| mPipBoundsAlgorithm.getMovementBounds(postChangeStackBounds), |
| mPipBoundsState.getStashedState()); |
| |
| // Update the display layout |
| mPipDisplayLayoutState.rotateTo(toRotation); |
| |
| // Calculate the stack bounds in the new orientation based on same fraction along the |
| // rotated movement bounds. |
| final Rect postChangeMovementBounds = mPipBoundsAlgorithm.getMovementBounds( |
| postChangeStackBounds, false /* adjustForIme */); |
| pipSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds, |
| snapFraction, mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(), |
| mPipDisplayLayoutState.getDisplayBounds(), |
| mPipDisplayLayoutState.getDisplayLayout().stableInsets()); |
| |
| mPipBoundsAlgorithm.getInsetBounds(outInsetBounds); |
| outBounds.set(postChangeStackBounds); |
| t.setBounds(pinnedTaskInfo.token, outBounds); |
| return true; |
| } |
| |
| private void dump(PrintWriter pw, String prefix) { |
| final String innerPrefix = " "; |
| pw.println(TAG); |
| mMenuController.dump(pw, innerPrefix); |
| mTouchHandler.dump(pw, innerPrefix); |
| mPipBoundsAlgorithm.dump(pw, innerPrefix); |
| mPipTaskOrganizer.dump(pw, innerPrefix); |
| mPipBoundsState.dump(pw, innerPrefix); |
| mPipInputConsumer.dump(pw, innerPrefix); |
| mPipSizeSpecHandler.dump(pw, innerPrefix); |
| mPipDisplayLayoutState.dump(pw, innerPrefix); |
| } |
| |
| /** |
| * The interface for calls from outside the Shell, within the host process. |
| */ |
| private class PipImpl implements Pip { |
| @Override |
| public void expandPip() { |
| mMainExecutor.execute(() -> { |
| PipController.this.expandPip(); |
| }); |
| } |
| |
| @Override |
| public void onSystemUiStateChanged(boolean isSysUiStateValid, int flag) { |
| mMainExecutor.execute(() -> { |
| PipController.this.onSystemUiStateChanged(isSysUiStateValid, flag); |
| }); |
| } |
| |
| @Override |
| public void setOnIsInPipStateChangedListener(Consumer<Boolean> callback) { |
| mMainExecutor.execute(() -> { |
| PipController.this.setOnIsInPipStateChangedListener(callback); |
| }); |
| } |
| |
| @Override |
| public void addPipExclusionBoundsChangeListener(Consumer<Rect> listener) { |
| mMainExecutor.execute(() -> { |
| mPipBoundsState.addPipExclusionBoundsChangeCallback(listener); |
| }); |
| } |
| |
| @Override |
| public void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { |
| mMainExecutor.execute(() -> { |
| mPipBoundsState.removePipExclusionBoundsChangeCallback(listener); |
| }); |
| } |
| |
| @Override |
| public void showPictureInPictureMenu() { |
| mMainExecutor.execute(() -> { |
| PipController.this.showPictureInPictureMenu(); |
| }); |
| } |
| } |
| |
| /** |
| * The interface for calls from outside the host process. |
| */ |
| @BinderThread |
| private static class IPipImpl extends IPip.Stub implements ExternalInterfaceBinder { |
| private PipController mController; |
| private final SingleInstanceRemoteListener<PipController, |
| IPipAnimationListener> mListener; |
| private final PipAnimationListener mPipAnimationListener = new PipAnimationListener() { |
| @Override |
| public void onPipAnimationStarted() { |
| mListener.call(l -> l.onPipAnimationStarted()); |
| } |
| |
| @Override |
| public void onPipResourceDimensionsChanged(int cornerRadius, int shadowRadius) { |
| mListener.call(l -> l.onPipResourceDimensionsChanged(cornerRadius, shadowRadius)); |
| } |
| |
| @Override |
| public void onExpandPip() { |
| mListener.call(l -> l.onExpandPip()); |
| } |
| }; |
| |
| IPipImpl(PipController controller) { |
| mController = controller; |
| mListener = new SingleInstanceRemoteListener<>(mController, |
| c -> c.setPinnedStackAnimationListener(mPipAnimationListener), |
| c -> c.setPinnedStackAnimationListener(null)); |
| } |
| |
| /** |
| * Invalidates this instance, preventing future calls from updating the controller. |
| */ |
| @Override |
| public void invalidate() { |
| mController = null; |
| // Unregister the listener to ensure any registered binder death recipients are unlinked |
| mListener.unregister(); |
| } |
| |
| @Override |
| public Rect startSwipePipToHome(ComponentName componentName, ActivityInfo activityInfo, |
| PictureInPictureParams pictureInPictureParams, int launcherRotation, |
| Rect keepClearArea) { |
| Rect[] result = new Rect[1]; |
| executeRemoteCallWithTaskPermission(mController, "startSwipePipToHome", |
| (controller) -> { |
| result[0] = controller.startSwipePipToHome(componentName, activityInfo, |
| pictureInPictureParams, launcherRotation, keepClearArea); |
| }, true /* blocking */); |
| return result[0]; |
| } |
| |
| @Override |
| public void stopSwipePipToHome(int taskId, ComponentName componentName, |
| Rect destinationBounds, SurfaceControl overlay) { |
| if (overlay != null) { |
| overlay.setUnreleasedWarningCallSite("PipController.stopSwipePipToHome"); |
| } |
| executeRemoteCallWithTaskPermission(mController, "stopSwipePipToHome", |
| (controller) -> controller.stopSwipePipToHome( |
| taskId, componentName, destinationBounds, overlay)); |
| } |
| |
| @Override |
| public void setShelfHeight(boolean visible, int height) { |
| executeRemoteCallWithTaskPermission(mController, "setShelfHeight", |
| (controller) -> controller.setShelfHeight(visible, height)); |
| } |
| |
| @Override |
| public void setLauncherKeepClearAreaHeight(boolean visible, int height) { |
| executeRemoteCallWithTaskPermission(mController, "setLauncherKeepClearAreaHeight", |
| (controller) -> controller.setLauncherKeepClearAreaHeight(visible, height)); |
| } |
| |
| @Override |
| public void setLauncherAppIconSize(int iconSizePx) { |
| executeRemoteCallWithTaskPermission(mController, "setLauncherAppIconSize", |
| (controller) -> controller.setLauncherAppIconSize(iconSizePx)); |
| } |
| |
| @Override |
| public void setPipAnimationListener(IPipAnimationListener listener) { |
| executeRemoteCallWithTaskPermission(mController, "setPipAnimationListener", |
| (controller) -> { |
| if (listener != null) { |
| mListener.register(listener); |
| } else { |
| mListener.unregister(); |
| } |
| }); |
| } |
| |
| @Override |
| public void setPipAnimationTypeToAlpha() { |
| executeRemoteCallWithTaskPermission(mController, "setPipAnimationTypeToAlpha", |
| (controller) -> controller.mPipAnimationController.setOneShotEnterAnimationType( |
| ANIM_TYPE_ALPHA)); |
| } |
| } |
| } |