/* * Copyright (C) 2016 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.server.wm; import static android.app.ActivityManager.StackId.PINNED_STACK_ID; import static android.util.TypedValue.COMPLEX_UNIT_DIP; import static android.view.Display.DEFAULT_DISPLAY; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.animation.ValueAnimator; import android.content.res.Resources; import android.graphics.Point; import android.graphics.Rect; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.util.DisplayMetrics; import android.util.Log; import android.util.Size; import android.util.Slog; import android.util.TypedValue; import android.view.DisplayInfo; import android.view.Gravity; import android.view.IPinnedStackController; import android.view.IPinnedStackListener; import com.android.internal.os.BackgroundThread; import com.android.internal.policy.PipMotionHelper; import com.android.internal.policy.PipSnapAlgorithm; import java.io.PrintWriter; /** * Holds the common state of the pinned stack between the system and SystemUI. */ class PinnedStackController { private static final String TAG = TAG_WITH_CLASS_NAME ? "PinnedStackController" : TAG_WM; private final WindowManagerService mService; private final DisplayContent mDisplayContent; private final Handler mHandler = new Handler(); private IPinnedStackListener mPinnedStackListener; private final PinnedStackListenerDeathHandler mPinnedStackListenerDeathHandler = new PinnedStackListenerDeathHandler(); private final PinnedStackControllerCallback mCallbacks = new PinnedStackControllerCallback(); private final PipSnapAlgorithm mSnapAlgorithm; private final PipMotionHelper mMotionHelper; // States that affect how the PIP can be manipulated private boolean mInInteractiveMode; private boolean mIsImeShowing; private int mImeHeight; private ValueAnimator mBoundsAnimator = null; // Used to calculate stack bounds across rotations private final DisplayInfo mDisplayInfo = new DisplayInfo(); // The size and position information that describes where the pinned stack will go by default. private int mDefaultStackGravity; private Size mDefaultStackSize; private Point mScreenEdgeInsets; // Temp vars for calculation private final DisplayMetrics mTmpMetrics = new DisplayMetrics(); private final Rect mTmpInsets = new Rect(); private final Rect mTmpRect = new Rect(); /** * The callback object passed to listeners for them to notify the controller of state changes. */ private class PinnedStackControllerCallback extends IPinnedStackController.Stub { @Override public void setInInteractiveMode(final boolean inInteractiveMode) { mHandler.post(() -> { // Cancel any existing animations on the PIP once the user starts dragging it if (mBoundsAnimator != null && inInteractiveMode) { mBoundsAnimator.cancel(); } mInInteractiveMode = inInteractiveMode; }); } @Override public void setSnapToEdge(final boolean snapToEdge) { mHandler.post(() -> { mSnapAlgorithm.setSnapToEdge(snapToEdge); }); } } /** * Handler for the case where the listener dies. */ private class PinnedStackListenerDeathHandler implements IBinder.DeathRecipient { @Override public void binderDied() { // Clean up the state if the listener dies mInInteractiveMode = false; mPinnedStackListener = null; } } PinnedStackController(WindowManagerService service, DisplayContent displayContent) { mService = service; mDisplayContent = displayContent; mSnapAlgorithm = new PipSnapAlgorithm(service.mContext); mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler()); mDisplayInfo.copyFrom(mDisplayContent.getDisplayInfo()); reloadResources(); } void onConfigurationChanged() { reloadResources(); } /** * Reloads all the resources for the current configuration. */ void reloadResources() { final Resources res = mService.mContext.getResources(); final Size defaultSizeDp = Size.parseSize(res.getString( com.android.internal.R.string.config_defaultPictureInPictureSize)); final Size screenEdgeInsetsDp = Size.parseSize(res.getString( com.android.internal.R.string.config_defaultPictureInPictureScreenEdgeInsets)); mDefaultStackGravity = res.getInteger( com.android.internal.R.integer.config_defaultPictureInPictureGravity); mDisplayContent.getDisplay().getRealMetrics(mTmpMetrics); mDefaultStackSize = new Size(dpToPx(defaultSizeDp.getWidth(), mTmpMetrics), dpToPx(defaultSizeDp.getHeight(), mTmpMetrics)); mScreenEdgeInsets = new Point(dpToPx(screenEdgeInsetsDp.getWidth(), mTmpMetrics), dpToPx(screenEdgeInsetsDp.getHeight(), mTmpMetrics)); } /** * Registers a pinned stack listener. */ void registerPinnedStackListener(IPinnedStackListener listener) { try { listener.asBinder().linkToDeath(mPinnedStackListenerDeathHandler, 0); listener.onListenerRegistered(mCallbacks); mPinnedStackListener = listener; notifyBoundsChanged(mIsImeShowing); } catch (RemoteException e) { Log.e(TAG, "Failed to register pinned stack listener", e); } } /** * @return the default bounds to show the PIP when there is no active PIP. */ Rect getDefaultBounds() { final Rect insetBounds = new Rect(); getInsetBounds(insetBounds); final Rect defaultBounds = new Rect(); Gravity.apply(mDefaultStackGravity, mDefaultStackSize.getWidth(), mDefaultStackSize.getHeight(), insetBounds, 0, 0, defaultBounds); return defaultBounds; } /** * @return the movement bounds for the given {@param stackBounds} and the current state of the * controller. */ Rect getMovementBounds(Rect stackBounds) { return getMovementBounds(stackBounds, true /* adjustForIme */); } /** * @return the movement bounds for the given {@param stackBounds} and the current state of the * controller. */ Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) { final Rect movementBounds = new Rect(); getInsetBounds(movementBounds); // Adjust the right/bottom to ensure the stack bounds never goes offscreen movementBounds.right = Math.max(movementBounds.left, movementBounds.right - stackBounds.width()); movementBounds.bottom = Math.max(movementBounds.top, movementBounds.bottom - stackBounds.height()); // Apply the movement bounds adjustments based on the current state if (adjustForIme) { if (mIsImeShowing) { movementBounds.bottom -= mImeHeight; } } return movementBounds; } /** * @return the repositioned PIP bounds given it's pre-change bounds, and the new display info. */ Rect onDisplayChanged(Rect preChangeStackBounds, DisplayInfo displayInfo) { final Rect postChangeStackBounds = new Rect(preChangeStackBounds); if (!mDisplayInfo.equals(displayInfo)) { // Calculate the snap fraction of the current stack along the old movement bounds, and // then update the stack bounds to the same fraction along the rotated movement bounds. final Rect preChangeMovementBounds = getMovementBounds(preChangeStackBounds); final float snapFraction = mSnapAlgorithm.getSnapFraction(preChangeStackBounds, preChangeMovementBounds); mDisplayInfo.copyFrom(displayInfo); final Rect postChangeMovementBounds = getMovementBounds(preChangeStackBounds, false /* adjustForIme */); mSnapAlgorithm.applySnapFraction(postChangeStackBounds, postChangeMovementBounds, snapFraction); } return postChangeStackBounds; } /** * Sets the Ime state and height. */ void setAdjustedForIme(boolean adjustedForIme, int imeHeight) { // Return early if there is no state change if (mIsImeShowing == adjustedForIme && mImeHeight == imeHeight) { return; } final Rect stackBounds = new Rect(); mService.getStackBounds(PINNED_STACK_ID, stackBounds); final Rect prevMovementBounds = getMovementBounds(stackBounds); mIsImeShowing = adjustedForIme; mImeHeight = imeHeight; if (mInInteractiveMode) { // If the user is currently interacting with the PIP and the ime state changes, then // don't adjust the bounds and defer that to after the interaction notifyBoundsChanged(adjustedForIme /* adjustedForIme */); } else { // Otherwise, we can move the PIP to a sane location to ensure that it does not block // the user from interacting with the IME final Rect movementBounds = getMovementBounds(stackBounds); final Rect toBounds = new Rect(stackBounds); if (adjustedForIme) { // IME visible toBounds.offset(0, Math.min(0, movementBounds.bottom - stackBounds.top)); } else { // IME hidden if (stackBounds.top == prevMovementBounds.bottom) { // If the PIP is resting on top of the IME, then adjust it with the hiding IME toBounds.offsetTo(toBounds.left, movementBounds.bottom); } } if (!toBounds.equals(stackBounds)) { if (mBoundsAnimator != null) { mBoundsAnimator.cancel(); } mBoundsAnimator = mMotionHelper.createAnimationToBounds(stackBounds, toBounds); mBoundsAnimator.start(); } } } /** * Sends a broadcast that the PIP movement bounds have changed. */ private void notifyBoundsChanged(boolean adjustedForIme) { if (mPinnedStackListener != null) { try { mPinnedStackListener.onBoundsChanged(adjustedForIme); } catch (RemoteException e) { Slog.e(TAG_WM, "Error delivering bounds changed event.", e); } } } /** * @return the bounds on the screen that the PIP can be visible in. */ private void getInsetBounds(Rect outRect) { mService.mPolicy.getStableInsetsLw(mDisplayInfo.rotation, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight, mTmpInsets); outRect.set(mTmpInsets.left + mScreenEdgeInsets.x, mTmpInsets.top + mScreenEdgeInsets.y, mDisplayInfo.logicalWidth - mTmpInsets.right - mScreenEdgeInsets.x, mDisplayInfo.logicalHeight - mTmpInsets.bottom - mScreenEdgeInsets.y); } /** * @return the pixels for a given dp value. */ private int dpToPx(float dpValue, DisplayMetrics dm) { return (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, dpValue, dm); } void dump(String prefix, PrintWriter pw) { pw.println(prefix + "PinnedStackController"); pw.print(prefix + " defaultBounds="); getDefaultBounds().printShortString(pw); pw.println(); mService.getStackBounds(PINNED_STACK_ID, mTmpRect); pw.print(prefix + " movementBounds="); getMovementBounds(mTmpRect).printShortString(pw); pw.println(); pw.println(prefix + " mIsImeShowing=" + mIsImeShowing); pw.println(prefix + " mInInteractiveMode=" + mInInteractiveMode); } }