diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml deleted file mode 100644 index 29d9b257cc590..0000000000000 --- a/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_gain_animation.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml deleted file mode 100644 index 70f553b896571..0000000000000 --- a/libs/WindowManager/Shell/res/anim/tv_pip_controls_focus_loss_animation.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml deleted file mode 100644 index 29d9b257cc590..0000000000000 --- a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_in_animation.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml b/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml deleted file mode 100644 index 70f553b896571..0000000000000 --- a/libs/WindowManager/Shell/res/anim/tv_pip_menu_fade_out_animation.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml new file mode 100644 index 0000000000000..d8f356164358d --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_move_down.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml new file mode 100644 index 0000000000000..3e0011c65942f --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_move_left.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml new file mode 100644 index 0000000000000..f6b3c72e3cb5f --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_move_right.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml new file mode 100644 index 0000000000000..1a34462495738 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_move_up.xml @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/pip_ic_move_white.xml b/libs/WindowManager/Shell/res/drawable/pip_ic_move_white.xml new file mode 100644 index 0000000000000..37f4c87006baa --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/pip_ic_move_white.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml index 5b90c99c22e0f..b56b114e9a4c7 100644 --- a/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml +++ b/libs/WindowManager/Shell/res/layout/tv_pip_menu.xml @@ -15,57 +15,101 @@ limitations under the License. --> - - + + + + + + + + + + + + + + + + android:alpha="0" + android:layout_margin="@dimen/pip_menu_outer_space_frame" + android:background="@drawable/tv_pip_menu_border"/> - + - + - + - - - - - - - - - - + + diff --git a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml index e41ebc4fb2454..558ec51752b93 100644 --- a/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml +++ b/libs/WindowManager/Shell/res/values-tvdpi/dimen.xml @@ -21,7 +21,14 @@ 20dp 4dp 26dp - 2dp - 0dp + 4dp + 4dp + 24dp + + + 20dp + + 24dp + 5dp diff --git a/libs/WindowManager/Shell/res/values/colors_tv.xml b/libs/WindowManager/Shell/res/values/colors_tv.xml index 17387fa1636f0..08d3cef104283 100644 --- a/libs/WindowManager/Shell/res/values/colors_tv.xml +++ b/libs/WindowManager/Shell/res/values/colors_tv.xml @@ -19,6 +19,6 @@ #E8EAED #80868B #E8EAED - #777777 - #CCE8EAED + #990E0E0F + #E8EAED \ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml index 2dfdcabaa9317..730d80894c419 100644 --- a/libs/WindowManager/Shell/res/values/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values/strings_tv.xml @@ -30,5 +30,8 @@ Full screen + + + Move PIP diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java index 711a0ac76702a..f91d7e2d28e82 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java @@ -30,7 +30,6 @@ import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.legacysplitscreen.LegacySplitScreenController; import com.android.wm.shell.pip.Pip; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipSnapAlgorithm; @@ -39,6 +38,7 @@ 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.PipUiEventLogger; +import com.android.wm.shell.pip.tv.TvPipBoundsAlgorithm; import com.android.wm.shell.pip.tv.TvPipController; import com.android.wm.shell.pip.tv.TvPipMenuController; import com.android.wm.shell.pip.tv.TvPipNotificationController; @@ -61,7 +61,7 @@ public abstract class TvPipModule { static Optional providePip( Context context, PipBoundsState pipBoundsState, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipTaskOrganizer pipTaskOrganizer, TvPipMenuController tvPipMenuController, PipMediaController pipMediaController, @@ -74,7 +74,7 @@ public abstract class TvPipModule { TvPipController.create( context, pipBoundsState, - pipBoundsAlgorithm, + tvPipBoundsAlgorithm, pipTaskOrganizer, pipTransitionController, tvPipMenuController, @@ -93,9 +93,9 @@ public abstract class TvPipModule { @WMSingleton @Provides - static PipBoundsAlgorithm providePipBoundsAlgorithm(Context context, + static TvPipBoundsAlgorithm provideTvPipBoundsAlgorithm(Context context, PipBoundsState pipBoundsState, PipSnapAlgorithm pipSnapAlgorithm) { - return new PipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm); + return new TvPipBoundsAlgorithm(context, pipBoundsState, pipSnapAlgorithm); } @WMSingleton @@ -109,10 +109,11 @@ public abstract class TvPipModule { @Provides static PipTransitionController provideTvPipTransition( Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, - PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm, + PipAnimationController pipAnimationController, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipBoundsState pipBoundsState, TvPipMenuController pipMenuController) { return new TvPipTransition(pipBoundsState, pipMenuController, - pipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); + tvPipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); } @WMSingleton @@ -156,7 +157,7 @@ public abstract class TvPipModule { SyncTransactionQueue syncTransactionQueue, PipBoundsState pipBoundsState, PipTransitionState pipTransitionState, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipAnimationController pipAnimationController, PipTransitionController pipTransitionController, PipSurfaceTransactionHelper pipSurfaceTransactionHelper, @@ -166,7 +167,7 @@ public abstract class TvPipModule { PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer, @ShellMainThread ShellExecutor mainExecutor) { return new PipTaskOrganizer(context, - syncTransactionQueue, pipTransitionState, pipBoundsState, pipBoundsAlgorithm, + syncTransactionQueue, pipTransitionState, pipBoundsState, tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper, pipTransitionController, splitScreenOptional, newSplitScreenOptional, displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java index a4b866aa3f5e6..1a3c51ea4f921 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsAlgorithm.java @@ -44,7 +44,7 @@ public class PipBoundsAlgorithm { private static final String TAG = PipBoundsAlgorithm.class.getSimpleName(); private static final float INVALID_SNAP_FRACTION = -1f; - private final @NonNull PipBoundsState mPipBoundsState; + protected final @NonNull PipBoundsState mPipBoundsState; private final PipSnapAlgorithm mSnapAlgorithm; private float mDefaultSizePercent; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java new file mode 100644 index 0000000000000..33f3bfb7b2668 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipBoundsAlgorithm.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2021 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.tv; + +import android.content.Context; +import android.graphics.Rect; +import android.util.Log; +import android.view.Gravity; + +import androidx.annotation.NonNull; + +import com.android.wm.shell.pip.PipBoundsAlgorithm; +import com.android.wm.shell.pip.PipBoundsState; +import com.android.wm.shell.pip.PipSnapAlgorithm; + +/** + * Contains pip bounds calculations that are specific to TV. + */ +public class TvPipBoundsAlgorithm extends PipBoundsAlgorithm { + + private static final String TAG = TvPipBoundsAlgorithm.class.getSimpleName(); + private static final boolean DEBUG = false; + + public TvPipBoundsAlgorithm(Context context, + @NonNull PipBoundsState pipBoundsState, + @NonNull PipSnapAlgorithm pipSnapAlgorithm) { + super(context, pipBoundsState, pipSnapAlgorithm); + } + + /** + * The normal bounds at a different position on the screen. + */ + public Rect getTvNormalBounds(int gravity) { + Rect normalBounds = getNormalBounds(); + Rect insetBounds = new Rect(); + getInsetBounds(insetBounds); + + if (mPipBoundsState.isImeShowing()) { + if (DEBUG) Log.d(TAG, "IME showing, height: " + mPipBoundsState.getImeHeight()); + insetBounds.bottom -= mPipBoundsState.getImeHeight(); + } + + Rect result = new Rect(); + Gravity.apply(gravity, normalBounds.width(), normalBounds.height(), insetBounds, result); + + if (DEBUG) { + Log.d(TAG, "normalBounds: " + normalBounds.toShortString()); + Log.d(TAG, "insetBounds: " + insetBounds.toShortString()); + Log.d(TAG, "gravity: " + Gravity.toString(gravity)); + Log.d(TAG, "resultBounds: " + result.toShortString()); + } + + return result; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java index b165706bc0380..de53939c01db2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipController.java @@ -18,6 +18,10 @@ package com.android.wm.shell.pip.tv; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.KeyEvent.KEYCODE_DPAD_DOWN; +import static android.view.KeyEvent.KEYCODE_DPAD_LEFT; +import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; +import static android.view.KeyEvent.KEYCODE_DPAD_UP; import android.annotation.IntDef; import android.app.ActivityManager; @@ -33,6 +37,7 @@ import android.graphics.Rect; import android.os.RemoteException; import android.util.Log; import android.view.DisplayInfo; +import android.view.Gravity; import com.android.wm.shell.R; import com.android.wm.shell.WindowManagerShellWrapper; @@ -42,7 +47,6 @@ import com.android.wm.shell.common.TaskStackListenerCallback; import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.pip.PinnedStackListenerForwarder; import com.android.wm.shell.pip.Pip; -import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; import com.android.wm.shell.pip.PipTaskOrganizer; @@ -85,10 +89,12 @@ public class TvPipController implements PipTransitionController.PipTransitionCal */ private static final int STATE_PIP_MENU = 2; + private static final int DEFAULT_GRAVITY = Gravity.BOTTOM | Gravity.RIGHT; + private final Context mContext; private final PipBoundsState mPipBoundsState; - private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm; private final PipTaskOrganizer mPipTaskOrganizer; private final PipMediaController mPipMediaController; private final TvPipNotificationController mPipNotificationController; @@ -97,6 +103,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private final TvPipImpl mImpl = new TvPipImpl(); private @State int mState = STATE_NO_PIP; + private @Gravity.GravityFlags int mGravity = DEFAULT_GRAVITY; private int mPinnedTaskId = NONEXISTENT_TASK_ID; private int mResizeAnimationDuration; @@ -104,7 +111,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal public static Pip create( Context context, PipBoundsState pipBoundsState, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, @@ -116,7 +123,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal return new TvPipController( context, pipBoundsState, - pipBoundsAlgorithm, + tvPipBoundsAlgorithm, pipTaskOrganizer, pipTransitionController, tvPipMenuController, @@ -130,7 +137,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal private TvPipController( Context context, PipBoundsState pipBoundsState, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipTaskOrganizer pipTaskOrganizer, PipTransitionController pipTransitionController, TvPipMenuController tvPipMenuController, @@ -145,7 +152,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mPipBoundsState = pipBoundsState; mPipBoundsState.setDisplayId(context.getDisplayId()); mPipBoundsState.setDisplayLayout(new DisplayLayout(context, context.getDisplay())); - mPipBoundsAlgorithm = pipBoundsAlgorithm; + mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm; mPipMediaController = pipMediaController; @@ -192,24 +199,19 @@ public class TvPipController implements PipTransitionController.PipTransitionCal public void showPictureInPictureMenu() { if (DEBUG) Log.d(TAG, "showPictureInPictureMenu(), state=" + stateToName(mState)); - if (mState != STATE_PIP) { + if (mState == STATE_NO_PIP) { if (DEBUG) Log.d(TAG, " > cannot open Menu from the current state."); return; } setState(STATE_PIP_MENU); - resizePinnedStack(STATE_PIP_MENU); + movePinnedStack(); } - /** - * Moves Pip window to its "normal" position. - */ @Override - public void movePipToNormalPosition() { - if (DEBUG) Log.d(TAG, "movePipToNormalPosition(), state=" + stateToName(mState)); - + public void closeMenu() { + if (DEBUG) Log.d(TAG, "closeMenu(), state before=" + stateToName(mState)); setState(STATE_PIP); - resizePinnedStack(STATE_PIP); } /** @@ -223,6 +225,69 @@ public class TvPipController implements PipTransitionController.PipTransitionCal onPipDisappeared(); } + @Override + public void movePip(int keycode) { + if (updatePosition(keycode)) { + if (DEBUG) Log.d(TAG, "New gravity: " + Gravity.toString(mGravity)); + mTvPipMenuController.updateMenu(mGravity); + movePinnedStack(); + } else { + if (DEBUG) Log.d(TAG, "Position hasn't changed"); + } + } + + @Override + public int getPipGravity() { + return mGravity; + } + + /** + * @return true if position changed + */ + private boolean updatePosition(int keycode) { + if (DEBUG) Log.d(TAG, "updatePosition, keycode: " + keycode); + + int updatedGravity; + switch (keycode) { + case KEYCODE_DPAD_UP: + updatedGravity = (mGravity & (~Gravity.BOTTOM)) | Gravity.TOP; + break; + case KEYCODE_DPAD_DOWN: + updatedGravity = (mGravity & (~Gravity.TOP)) | Gravity.BOTTOM; + break; + case KEYCODE_DPAD_LEFT: + updatedGravity = (mGravity & (~Gravity.RIGHT)) | Gravity.LEFT; + break; + case KEYCODE_DPAD_RIGHT: + updatedGravity = (mGravity & (~Gravity.LEFT)) | Gravity.RIGHT; + break; + default: + updatedGravity = mGravity; + } + + if (updatedGravity != mGravity) { + mGravity = updatedGravity; + return true; + } + return false; + } + + /** + * Animate to the updated position of the PiP based on the state and position of the PiP. + */ + private void movePinnedStack() { + if (mState == STATE_NO_PIP) { + return; + } + + Rect bounds = mTvPipBoundsAlgorithm.getTvNormalBounds(mGravity); + if (DEBUG) Log.d(TAG, "movePinnedStack() - new pip bounds: " + bounds.toShortString()); + mPipTaskOrganizer.scheduleAnimateResizePip(bounds, + mResizeAnimationDuration, rect -> { + if (DEBUG) Log.d(TAG, "movePinnedStack() animation done"); + }); + } + /** * Closes Pip window. */ @@ -234,41 +299,6 @@ public class TvPipController implements PipTransitionController.PipTransitionCal onPipDisappeared(); } - /** - * Resizes the Pip task/window to the appropriate size for the given state. - * This is a legacy API. Now we expect that the state argument passed to it should always match - * the current state of the Controller. If it does not match an {@link IllegalArgumentException} - * will be thrown. However, if the passed state does match - we'll determine the right bounds - * to the state and will move Pip task/window there. - * - * @param state the to determine the Pip bounds. IMPORTANT: should always match the current - * state of the Controller. - */ - private void resizePinnedStack(@State int state) { - if (state != mState) { - throw new IllegalArgumentException("The passed state should match the current state!"); - } - if (DEBUG) Log.d(TAG, "resizePinnedStack() state=" + stateToName(mState)); - - final Rect newBounds; - switch (mState) { - case STATE_PIP_MENU: - case STATE_PIP: - // Let PipBoundsAlgorithm figure out what the correct bounds are at the moment. - // Internally, it will get the "default" bounds from PipBoundsState and adjust them - // as needed to account for things like IME state (will query PipBoundsState for - // this information as well, so it's important to keep PipBoundsState up to date). - newBounds = mPipBoundsAlgorithm.getNormalBounds(); - break; - - case STATE_NO_PIP: - default: - return; - } - - mPipTaskOrganizer.scheduleAnimateResizePip(newBounds, mResizeAnimationDuration, null); - } - private void registerSessionListenerForCurrentUser() { mPipMediaController.registerSessionListenerForCurrentUser(); } @@ -298,6 +328,7 @@ public class TvPipController implements PipTransitionController.PipTransitionCal mPipNotificationController.dismiss(); mTvPipMenuController.hideMenu(); + mGravity = DEFAULT_GRAVITY; setState(STATE_NO_PIP); mPinnedTaskId = NONEXISTENT_TASK_ID; } @@ -384,10 +415,9 @@ public class TvPipController implements PipTransitionController.PipTransitionCal return; } mPipBoundsState.setImeVisibility(imeVisible, imeHeight); - // "Normal" Pip bounds may have changed, so if we are in the "normal" state, - // let's update the bounds. - if (mState == STATE_PIP) { - resizePinnedStack(STATE_PIP); + + if (mState != STATE_NO_PIP) { + movePinnedStack(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipInterpolators.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipInterpolators.java new file mode 100644 index 0000000000000..927c1ec2a8884 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipInterpolators.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 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.tv; + +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; + +/** + * All interpolators needed for TV specific Pip animations + */ +public class TvPipInterpolators { + + /** + * A standard ease-in-out curve reserved for moments of interaction (button and card states). + */ + public static final Interpolator STANDARD = new PathInterpolator(0.2f, 0.1f, 0f, 1f); + + /** + * A sharp ease-out-expo curve created for snappy but fluid browsing between cards and clusters. + */ + public static final Interpolator BROWSE = new PathInterpolator(0.18f, 1f, 0.22f, 1f); + + /** + * A smooth ease-out-expo curve created for incoming elements (forward, back, overlay). + */ + public static final Interpolator ENTER = new PathInterpolator(0.12f, 1f, 0.4f, 1f); + + /** + * A smooth ease-in-out-expo curve created for outgoing elements (forward, back, overlay). + */ + public static final Interpolator EXIT = new PathInterpolator(0.4f, 1f, 0.12f, 1f); + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index 77bfa07a425fc..72ead00233663 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -24,13 +24,18 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ParceledListSlice; +import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.RectF; import android.os.Handler; +import android.os.RemoteException; import android.util.Log; import android.view.SurfaceControl; +import android.view.SyncRtSurfaceTransactionApplier; import androidx.annotation.Nullable; +import com.android.wm.shell.R; import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMediaController; @@ -53,11 +58,33 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private Delegate mDelegate; private SurfaceControl mLeash; - private TvPipMenuView mMenuView; + private TvPipMenuView mPipMenuView; + + // User can actively move the PiP via the DPAD. + private boolean mInMoveMode; private final List mMediaActions = new ArrayList<>(); private final List mAppActions = new ArrayList<>(); + private SyncRtSurfaceTransactionApplier mApplier; + RectF mTmpSourceRectF = new RectF(); + RectF mTmpDestinationRectF = new RectF(); + Matrix mMoveTransform = new Matrix(); + + private final float[] mTmpValues = new float[9]; + private final Runnable mUpdateEmbeddedMatrix = () -> { + if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { + return; + } + mMoveTransform.getValues(mTmpValues); + try { + mPipMenuView.getViewRootImpl().getAccessibilityEmbeddedConnection() + .setScreenMatrix(mTmpValues); + } catch (RemoteException e) { + if (DEBUG) e.printStackTrace(); + } + }; + public TvPipMenuController(Context context, PipBoundsState pipBoundsState, SystemWindows systemWindows, PipMediaController pipMediaController, Handler mainHandler) { @@ -107,13 +134,13 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private void attachPipMenuView() { if (DEBUG) Log.d(TAG, "attachPipMenuView()"); - if (mMenuView != null) { + if (mPipMenuView != null) { detachPipMenuView(); } - mMenuView = new TvPipMenuView(mContext); - mMenuView.setListener(this); - mSystemWindows.addView(mMenuView, + mPipMenuView = new TvPipMenuView(mContext); + mPipMenuView.setListener(this); + mSystemWindows.addView(mPipMenuView, getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), 0, SHELL_ROOT_LAYER_PIP); } @@ -122,38 +149,76 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis public void showMenu() { if (DEBUG) Log.d(TAG, "showMenu()"); - if (mMenuView != null) { - Rect pipBounds = mPipBoundsState.getBounds(); - mSystemWindows.updateViewLayout(mMenuView, getPipMenuLayoutParams( - MENU_WINDOW_TITLE, pipBounds.width(), pipBounds.height())); + if (mPipMenuView != null) { + Rect menuBounds = getMenuBounds(mPipBoundsState.getBounds()); + mSystemWindows.updateViewLayout(mPipMenuView, getPipMenuLayoutParams( + MENU_WINDOW_TITLE, menuBounds.width(), menuBounds.height())); maybeUpdateMenuViewActions(); - SurfaceControl menuSurfaceControl = mSystemWindows.getViewSurface(mMenuView); + SurfaceControl menuSurfaceControl = mSystemWindows.getViewSurface(mPipMenuView); if (menuSurfaceControl != null) { SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - t.setRelativeLayer(mMenuView.getWindowSurfaceControl(), mLeash, 1); - t.setPosition(menuSurfaceControl, pipBounds.left, pipBounds.top); + t.setRelativeLayer(mPipMenuView.getWindowSurfaceControl(), mLeash, 1); + t.setPosition(menuSurfaceControl, menuBounds.left, menuBounds.top); t.apply(); } - mMenuView.show(); + mPipMenuView.show(mInMoveMode, mDelegate.getPipGravity()); } } + void updateMenu(int gravity) { + mPipMenuView.showMovementHints(gravity); + } + + private Rect getMenuBounds(Rect pipBounds) { + int extraSpaceInPx = mContext.getResources() + .getDimensionPixelSize(R.dimen.pip_menu_outer_space); + Rect menuBounds = new Rect(pipBounds); + menuBounds.inset(-extraSpaceInPx, -extraSpaceInPx); + return menuBounds; + } + void hideMenu() { - hideMenu(true); + if (!isMenuVisible()) { + if (DEBUG) Log.d(TAG, "hideMenu() - Menu isn't visible, so don't hide"); + return; + } else { + if (DEBUG) Log.d(TAG, "hideMenu()"); + } + + mPipMenuView.hide(mInMoveMode); + if (!mInMoveMode) { + mDelegate.closeMenu(); + } } - void hideMenu(boolean movePipWindow) { - if (DEBUG) Log.d(TAG, "hideMenu(), movePipWindow=" + movePipWindow); + @Override + public void onEnterMoveMode() { + if (DEBUG) Log.d(TAG, "onEnterMoveMode - " + mInMoveMode); + mInMoveMode = true; + mPipMenuView.showMenuButtons(false); + mPipMenuView.showMovementHints(mDelegate.getPipGravity()); + } - if (!isMenuVisible()) { - return; + @Override + public boolean onExitMoveMode() { + if (DEBUG) Log.d(TAG, "onExitMoveMode - " + mInMoveMode); + if (mInMoveMode) { + mInMoveMode = false; + mPipMenuView.showMenuButtons(true); + mPipMenuView.hideMovementHints(); + return true; } + return false; + } - mMenuView.hide(); - if (movePipWindow) { - mDelegate.movePipToNormalPosition(); + @Override + public boolean onPipMovement(int keycode) { + if (DEBUG) Log.d(TAG, "onPipMovement - " + mInMoveMode); + if (mInMoveMode) { + mDelegate.movePip(keycode); } + return mInMoveMode; } @Override @@ -163,17 +228,6 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mLeash = null; } - private void detachPipMenuView() { - if (DEBUG) Log.d(TAG, "detachPipMenuView()"); - - if (mMenuView == null) { - return; - } - - mSystemWindows.removeView(mMenuView); - mMenuView = null; - } - @Override public void setAppActions(ParceledListSlice actions) { if (DEBUG) Log.d(TAG, "setAppActions()"); @@ -209,24 +263,146 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } private void maybeUpdateMenuViewActions() { - if (mMenuView == null) { + if (mPipMenuView == null) { return; } if (!mAppActions.isEmpty()) { - mMenuView.setAdditionalActions(mAppActions, mMainHandler); + mPipMenuView.setAdditionalActions(mAppActions, mMainHandler); } else { - mMenuView.setAdditionalActions(mMediaActions, mMainHandler); + mPipMenuView.setAdditionalActions(mMediaActions, mMainHandler); } } @Override public boolean isMenuVisible() { - return mMenuView != null && mMenuView.isVisible(); + boolean isVisible = mPipMenuView != null && mPipMenuView.isVisible(); + if (DEBUG) Log.d(TAG, "isMenuVisible: " + isVisible); + return isVisible; + } + + /** + * Does an immediate window crop of the PiP menu. + */ + @Override + public void resizePipMenu(@android.annotation.Nullable SurfaceControl pipLeash, + @android.annotation.Nullable SurfaceControl.Transaction t, + Rect destinationBounds) { + if (DEBUG) Log.d(TAG, "resizePipMenu: " + destinationBounds.toShortString()); + if (destinationBounds.isEmpty()) { + return; + } + + if (!maybeCreateSyncApplier()) { + return; + } + + SurfaceControl surfaceControl = getSurfaceControl(); + SyncRtSurfaceTransactionApplier.SurfaceParams + params = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(surfaceControl) + .withWindowCrop(getMenuBounds(destinationBounds)) + .build(); + if (pipLeash != null && t != null) { + SyncRtSurfaceTransactionApplier.SurfaceParams + pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash) + .withMergeTransaction(t) + .build(); + mApplier.scheduleApply(params, pipParams); + } else { + mApplier.scheduleApply(params); + } + } + + private SurfaceControl getSurfaceControl() { + return mSystemWindows.getViewSurface(mPipMenuView); + } + + @Override + public void movePipMenu(SurfaceControl pipLeash, SurfaceControl.Transaction transaction, + Rect pipDestBounds) { + if (DEBUG) Log.d(TAG, "movePipMenu: " + pipDestBounds.toShortString()); + + if (pipDestBounds.isEmpty()) { + if (transaction == null && DEBUG) Log.d(TAG, "no transaction given"); + return; + } + if (!maybeCreateSyncApplier()) { + return; + } + + Rect menuDestBounds = getMenuBounds(pipDestBounds); + Rect mTmpSourceBounds = new Rect(); + // If there is no pip leash supplied, that means the PiP leash is already finalized + // resizing and the PiP menu is also resized. We then want to do a scale from the current + // new menu bounds. + if (pipLeash != null && transaction != null) { + if (DEBUG) Log.d(TAG, "mTmpSourceBounds based on mPipMenuView.getBoundsOnScreen()"); + mPipMenuView.getBoundsOnScreen(mTmpSourceBounds); + } else { + if (DEBUG) Log.d(TAG, "mTmpSourceBounds based on menu width and height"); + mTmpSourceBounds.set(0, 0, menuDestBounds.width(), menuDestBounds.height()); + } + + mTmpSourceRectF.set(mTmpSourceBounds); + mTmpDestinationRectF.set(menuDestBounds); + mMoveTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); + + SurfaceControl surfaceControl = getSurfaceControl(); + SyncRtSurfaceTransactionApplier.SurfaceParams params = + new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder( + surfaceControl).withMatrix(mMoveTransform).build(); + + if (pipLeash != null && transaction != null) { + SyncRtSurfaceTransactionApplier.SurfaceParams + pipParams = new SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(pipLeash) + .withMergeTransaction(transaction) + .build(); + mApplier.scheduleApply(params, pipParams); + } else { + mApplier.scheduleApply(params); + } + + if (mPipMenuView.getViewRootImpl() != null) { + mPipMenuView.getHandler().removeCallbacks(mUpdateEmbeddedMatrix); + mPipMenuView.getHandler().post(mUpdateEmbeddedMatrix); + } + } + + private boolean maybeCreateSyncApplier() { + if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { + Log.v(TAG, "Not going to move PiP, either menu or its parent is not created."); + return false; + } + + if (mApplier == null) { + mApplier = new SyncRtSurfaceTransactionApplier(mPipMenuView); + } + return true; + } + + private void detachPipMenuView() { + if (mPipMenuView == null) { + return; + } + + mApplier = null; + mSystemWindows.removeView(mPipMenuView); + mPipMenuView = null; + } + + @Override + public void updateMenuBounds(Rect destinationBounds) { + Rect menuBounds = getMenuBounds(destinationBounds); + if (DEBUG) Log.d(TAG, "updateMenuBounds: " + menuBounds.toShortString()); + mSystemWindows.updateViewLayout(mPipMenuView, + getPipMenuLayoutParams(MENU_WINDOW_TITLE, menuBounds.width(), + menuBounds.height())); } @Override public void onBackPress() { - hideMenu(); + if (!onExitMoveMode()) { + hideMenu(); + } } @Override @@ -240,8 +416,14 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis } interface Delegate { - void movePipToNormalPosition(); void movePipToFullscreen(); + + void movePip(int keycode); + + int getPipGravity(); + + void closeMenu(); + void closePip(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java index 4327f1590f534..0141b6a1859e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuView.java @@ -16,17 +16,21 @@ package com.android.wm.shell.pip.tv; -import static android.animation.AnimatorInflater.loadAnimator; import static android.view.KeyEvent.ACTION_UP; import static android.view.KeyEvent.KEYCODE_BACK; +import static android.view.KeyEvent.KEYCODE_DPAD_CENTER; +import static android.view.KeyEvent.KEYCODE_DPAD_DOWN; +import static android.view.KeyEvent.KEYCODE_DPAD_LEFT; +import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT; +import static android.view.KeyEvent.KEYCODE_DPAD_UP; -import android.animation.Animator; import android.app.PendingIntent; import android.app.RemoteAction; import android.content.Context; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; +import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.SurfaceControl; @@ -34,6 +38,7 @@ import android.view.View; import android.view.ViewRootImpl; import android.view.WindowManagerGlobal; import android.widget.FrameLayout; +import android.widget.ImageView; import android.widget.LinearLayout; import androidx.annotation.NonNull; @@ -45,16 +50,14 @@ import java.util.ArrayList; import java.util.List; /** - * A View that represents Pip Menu on TV. It's responsible for displaying 2 ever-present Pip Menu - * actions: Fullscreen and Close, but could also display "additional" actions, that may be set via - * a {@link #setAdditionalActions(List, Handler)} call. + * A View that represents Pip Menu on TV. It's responsible for displaying 3 ever-present Pip Menu + * actions: Fullscreen, Move and Close, but could also display "additional" actions, that may be set + * via a {@link #setAdditionalActions(List, Handler)} call. */ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private static final String TAG = "TvPipMenuView"; private static final boolean DEBUG = TvPipController.DEBUG; - private final Animator mFadeInAnimation; - private final Animator mFadeOutAnimation; @Nullable private Listener mListener; @@ -62,6 +65,11 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { private final View mMenuFrameView; private final List mAdditionalButtons = new ArrayList<>(); + private final ImageView mArrowUp; + private final ImageView mArrowRight; + private final ImageView mArrowDown; + private final ImageView mArrowLeft; + public TvPipMenuView(@NonNull Context context) { this(context, null); } @@ -85,35 +93,68 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { .setOnClickListener(this); mActionButtonsContainer.findViewById(R.id.tv_pip_menu_close_button) .setOnClickListener(this); + mActionButtonsContainer.findViewById(R.id.tv_pip_menu_move_button) + .setOnClickListener(this); mMenuFrameView = findViewById(R.id.tv_pip_menu_frame); - mFadeInAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_in_animation); - mFadeInAnimation.setTarget(mMenuFrameView); - mFadeOutAnimation = loadAnimator(mContext, R.anim.tv_pip_menu_fade_out_animation); - mFadeOutAnimation.setTarget(mMenuFrameView); + mArrowUp = findViewById(R.id.tv_pip_menu_arrow_up); + mArrowRight = findViewById(R.id.tv_pip_menu_arrow_right); + mArrowDown = findViewById(R.id.tv_pip_menu_arrow_down); + mArrowLeft = findViewById(R.id.tv_pip_menu_arrow_left); } void setListener(@Nullable Listener listener) { mListener = listener; } - void show() { - if (DEBUG) Log.d(TAG, "show()"); - - mFadeInAnimation.start(); + void show(boolean inMoveMode, int gravity) { + if (DEBUG) Log.d(TAG, "show(), inMoveMode: " + inMoveMode); grantWindowFocus(true); + + if (inMoveMode) { + showMovementHints(gravity); + } else { + animateAlphaTo(1, mActionButtonsContainer); + } + animateAlphaTo(1, mMenuFrameView); } - void hide() { + void hide(boolean isInMoveMode) { if (DEBUG) Log.d(TAG, "hide()"); + animateAlphaTo(0, mActionButtonsContainer); + animateAlphaTo(0, mMenuFrameView); + hideMovementHints(); - mFadeOutAnimation.start(); - grantWindowFocus(false); + if (!isInMoveMode) { + grantWindowFocus(false); + } + } + + private void animateAlphaTo(float alpha, View view) { + view.animate() + .alpha(alpha) + .setInterpolator(alpha == 0f ? TvPipInterpolators.EXIT : TvPipInterpolators.ENTER) + .setDuration(500) + .withStartAction(() -> { + if (alpha != 0) { + view.setVisibility(VISIBLE); + } + }) + .withEndAction(() -> { + if (alpha == 0) { + view.setVisibility(GONE); + } + }); } boolean isVisible() { - return mMenuFrameView != null && mMenuFrameView.getAlpha() != 0.0f; + return mMenuFrameView.getAlpha() != 0f + || mActionButtonsContainer.getAlpha() != 0f + || mArrowUp.getAlpha() != 0f + || mArrowRight.getAlpha() != 0f + || mArrowDown.getAlpha() != 0f + || mArrowLeft.getAlpha() != 0f; } private void grantWindowFocus(boolean grantFocus) { @@ -188,6 +229,8 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { final int id = v.getId(); if (id == R.id.tv_pip_menu_fullscreen_button) { mListener.onFullscreenButtonClick(); + } else if (id == R.id.tv_pip_menu_move_button) { + mListener.onEnterMoveMode(); } else if (id == R.id.tv_pip_menu_close_button) { mListener.onCloseButtonClick(); } else { @@ -207,17 +250,79 @@ public class TvPipMenuView extends FrameLayout implements View.OnClickListener { @Override public boolean dispatchKeyEvent(KeyEvent event) { - if (event.getAction() == ACTION_UP && event.getKeyCode() == KEYCODE_BACK - && mListener != null) { - mListener.onBackPress(); - return true; + if (DEBUG) { + Log.d(TAG, "dispatchKeyEvent, action: " + event.getAction() + + ", keycode: " + event.getKeyCode()); + } + if (mListener != null && event.getAction() == ACTION_UP) { + switch (event.getKeyCode()) { + case KEYCODE_BACK: + mListener.onBackPress(); + return true; + case KEYCODE_DPAD_UP: + case KEYCODE_DPAD_DOWN: + case KEYCODE_DPAD_LEFT: + case KEYCODE_DPAD_RIGHT: + return mListener.onPipMovement(event.getKeyCode()) || super.dispatchKeyEvent( + event); + case KEYCODE_DPAD_CENTER: + return mListener.onExitMoveMode() || super.dispatchKeyEvent(event); + default: + break; + } } return super.dispatchKeyEvent(event); } + /** + * Shows user hints for moving the PiP, e.g. arrows. + */ + public void showMovementHints(int gravity) { + if (DEBUG) Log.d(TAG, "showMovementHints(), position: " + Gravity.toString(gravity)); + + animateAlphaTo((gravity & Gravity.BOTTOM) == Gravity.BOTTOM ? 1f : 0f, mArrowUp); + animateAlphaTo((gravity & Gravity.TOP) == Gravity.TOP ? 1f : 0f, mArrowDown); + animateAlphaTo((gravity & Gravity.RIGHT) == Gravity.RIGHT ? 1f : 0f, mArrowLeft); + animateAlphaTo((gravity & Gravity.LEFT) == Gravity.LEFT ? 1f : 0f, mArrowRight); + } + + /** + * Hides user hints for moving the PiP, e.g. arrows. + */ + public void hideMovementHints() { + if (DEBUG) Log.d(TAG, "hideMovementHints()"); + animateAlphaTo(0, mArrowUp); + animateAlphaTo(0, mArrowRight); + animateAlphaTo(0, mArrowDown); + animateAlphaTo(0, mArrowLeft); + } + + /** + * Show or hide the pip user actions. + */ + public void showMenuButtons(boolean show) { + if (DEBUG) Log.d(TAG, "showMenuButtons: " + show); + animateAlphaTo(show ? 1 : 0, mActionButtonsContainer); + } + interface Listener { + void onBackPress(); + + void onEnterMoveMode(); + + /** + * @return whether move mode was exited + */ + boolean onExitMoveMode(); + + /** + * @return whether pip movement was handled. + */ + boolean onPipMovement(int keycode); + void onCloseButtonClick(); + void onFullscreenButtonClick(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java index 551476dc9d547..5062cc4364611 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java @@ -29,7 +29,6 @@ import androidx.annotation.Nullable; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.pip.PipBoundsAlgorithm; import com.android.wm.shell.pip.PipBoundsState; import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipTransitionController; @@ -42,11 +41,11 @@ import com.android.wm.shell.transition.Transitions; public class TvPipTransition extends PipTransitionController { public TvPipTransition(PipBoundsState pipBoundsState, PipMenuController pipMenuController, - PipBoundsAlgorithm pipBoundsAlgorithm, + TvPipBoundsAlgorithm tvPipBoundsAlgorithm, PipAnimationController pipAnimationController, Transitions transitions, @NonNull ShellTaskOrganizer shellTaskOrganizer) { - super(pipBoundsState, pipMenuController, pipBoundsAlgorithm, pipAnimationController, + super(pipBoundsState, pipMenuController, tvPipBoundsAlgorithm, pipAnimationController, transitions, shellTaskOrganizer); }